The Sims 4 Modern Python Modding: Part 6— Adding Interactions

June Hanabi
12 min readOct 5, 2020

--

In this part we’re going to cover adding custom interactions to existing objects.

Make sure you start at part 1

By now I assume your up-to-speed on how to mod for the Sims 4 using python scripting which I already covered in part 1 & part 2. If you’re new to all of this I really recommend starting in part 1 and progressing through each part in order. At the very least the first 2 parts.

We’re starting to leave the beginner stage

By this point I assume you know the basics, up until now that’s what we’ve been covering and we’ve been taking things really slowly because both of us are new to modding for this game. However there’s only so many overly simple projects we can make before it becomes repetitive and monotonous. I feel we’re at that point where we can start to do more, not a lot more, just a little more.

I’m going to start bringing in tuning files and other aspects to creating Sims 4 mods. Pure scripting mods are great but most mods create something and add scripting and other stuff to it and to do that there’s a few extra things that go into it which we will explorer over time in the intermediate parts.

We need to gather a list of objects to amend

Download Sims 4 Studio if you don’t already have it and install it. This can be rather intimidating at first but it’s actually fairly easy. There’s plenty of tutorials on how to use it online so I’m just going to cover what we need to know for this tutorial.

Sims 4 Studio is used for everything but Python scripting. There’s a ton of additional stuff besides scripting that can go into a mod and this program invaluably takes care of all it.

We need to gather a list of objects that our script will insert the custom interaction to. It can be anything but I decided on objects you sit on, because why not? This includes chairs, couches, etc… As long as it’s an object, though, you can pick anything under the sun, you don’t have to copy me.

Select Create 3D Mesh and then click on Object . We’re not actually interested in creating anything here, this is just temporary so that we can get a list of ids for our script.

I always select the Base Game Game Pack and Content Game because I don’t have dlc’s at the moment so I want to filter dlc content and any custom content.

Now search for anything you want, I’m going to search for some kind of sitting. We only need 1 single item because we’re going to use the item we pick out to find other items like it. This is just a starting point.

I’ve selected a random chair, lets proceed on. Click next.

It’s going to ask you to save it, remember this isn’t important at all so I just picked a gibberish name and dropped it in my Downloads folder.

Now either the Studio tab will be open or the Warehouse tab. I think it defaults to the Studio tab so this is probably what you’re seeing now if you’re copying me.

You need to be in the Warehouse tab though.

  • Studio is the tab you go to most of the time because it makes things super easy. A package file often contains many files and you don’t really have to think about them much because Studio will abstract all of that away and provide you with a pretty interface. You really have to admire the work that went into this program to make that happen.
  • Warehouse is the tab that shows you what’s really in the package file. It can be a lot more intimidating and complicating but to access the Tuning ID we need to go there.

So already you can see what I’m talking about, lets not get lost though and scroll down to Object Definition.

On the right we can find the Tuning Name. You’ll probably need to scroll down for it as well.

Here we see object_sitDiningQA_01 . What this means is EA has created this chair and a tuning file that tells the game in great detail what to do with this chair and a bunch of other stuff. It then links the two together by pointing the chair to the tuning file name and id. In this case we only care about the name.

In the programming world we refer to this as Configuration over code. This is one of the major competing programming patterns and one that EA has chosen for The Sims 4. The idea is that, instead of coding every little detail of every object, you write configuration of what you want to happen and the game handles all the coding parts for you. The Configuration in this case is Tuning files. This saves you from a ton of scripting and allows you to script only what is necessary. It also allows you to re-use other game components and scripts without much of any effort or coding.

Now that we know the Tuning name lets look up similar objects. Go to Tools > Extract Tuning…

Lets search for just the first part of the tuning name, object_sit. I bet searching that will find many other objects related to sitting.

Fantastic, this is what we want! Lets prefix it with object/ just to help narrow it down further and ensure we stay within objects.

In this case, I’m just going to select all the barstools.

Select the ones and then click Add to current package, afterwards close the window.

You should now see many Object Tuning Files in the Warehouse

Lets go through them one by one and note down the Tuning ID.

For each tuning file, in the Data tab, you’ll see a TuningID. Lets write these down individually.

Here’s the actual list of all of the Tuning IDs of all of those objects for your convenience that you can easily copy. Remember, you don’t have to copy me, perhaps you want to amend showers, counters, mirrors, appliances, etc… This is your mod, I’m just writing a tutorial.

77175, 36704, 36800, 77167, 177804, 36816, 36710, 36801, 36690, 36797, 38472, 157486, 206821, 36697, 36699, 36814,
36685, 36669, 36674, 243653, 36694, 36693, 36670, 36682, 178288, 36713, 36701, 177446, 77177, 36689, 36806, 36675,
36799, 36809, 38207, 240819, 36718, 210101, 77170, 36691, 177777, 36702, 36708, 36719, 36671, 178309, 36805, 36803,
36673, 40537, 36684, 36709, 36817, 190222, 36808, 36707, 77178, 36818, 36717, 107641, 38224, 36698, 177806, 38306,
38470, 36711, 212063, 36804, 190007, 38473, 36815, 245629, 36700, 211712, 245627, 36686, 77176, 36798, 36712,
180550, 36692, 36813, 38474, 36812, 107642, 36811, 77168, 77169, 36688, 177808, 77174, 14841, 27248, 144846, 135520,
99157

There’s one more thing we want to know, that’s the interaction id of the interaction we’re going to add to all the objects.

Go back to Extract Tuning... and type in interaction/. Scroll down until you see what you may be interested in adding. For me I’m using art_View.

It’s important to note that most interactions require special tags or tuning on the object, art_View is one that does not. You’re welcome to pick any interaction but if you run into errors or it doesn’t show up in the game this could be the reason. In this case I recommend to stick with art_View

Select the interaction you’re interested in and click the Add to current package button like before.

art_View has an id of 33623.

We have everything we need, lets start coding

Close out of Sims 4 Studio, and don’t bother saving anything. Create a new project either your own way or from my project template.

Add a file named anything, as usual mine is called main.py .

We’re going to monkey patch a function the game uses to load data into different objects, interactions, etc… Monkey patching is what we call replacing a function while the game is running with another function so that the game will use our code instead. It was initially covered in part 3.

In our case we want to stash away the original function so lets go ahead and save it.

from sims4.tuning.instance_manager import InstanceManagerorig_load_data_into_class_instances = InstanceManager.load_data_into_class_instances

We’re essentially assigning the function load_data_into_class_instances inside of InstanceManager to a variable that we can call even after we replace the function which is pretty cool.

Now lets add in our tuning file ids

# Tuning ID of interactions
interaction_id = 33623

# Tuning id of objects
object_ids = (
77175, 36704, 36800, 77167, 177804, 36816, 36710, 36801, 36690, 36797, 38472, 157486, 206821, 36697, 36699, 36814,
36685, 36669, 36674, 243653, 36694, 36693, 36670, 36682, 178288, 36713, 36701, 177446, 77177, 36689, 36806, 36675,
36799, 36809, 38207, 240819, 36718, 210101, 77170, 36691, 177777, 36702, 36708, 36719, 36671, 178309, 36805, 36803,
36673, 40537, 36684, 36709, 36817, 190222, 36808, 36707, 77178, 36818, 36717, 107641, 38224, 36698, 177806, 38306,
38470, 36711, 212063, 36804, 190007, 38473, 36815, 245629, 36700, 211712, 245627, 36686, 77176, 36798, 36712,
180550, 36692, 36813, 38474, 36812, 107642, 36811, 77168, 77169, 36688, 177808, 77174, 14841, 27248, 144846, 135520,
99157)

Just a giant list of the object tuning files and our interaction tuning file we noted earlier.

Unlike in part 3, we’re not going to copy the original function code. We’re only interested in doing something after the function runs, we’re catching the end of the function. This is why we saved the original function. This is what I mean:

def _load_data_into_class_instances(self: InstanceManager) -> None:
# Call original function
orig_load_data_into_class_instances(self)
InstanceManager.load_data_into_class_instances = _load_data_into_class_instances

Here we have our function and all it does it call the original function. Below that we monkey patch/replace the games function with our own. So when this function is called, it calls our function and all our function does right now is call the original function.

Game calls our function > Our function calls the original > The original function does whatever it's supposed to do.

So lets do more stuff after we call the original function. Much of this code is based on and inspired from Navystylz’s post here. Thank you Navystylz! I don’t fully understand all of it but lets expand the function.

This function is called once in the game for each type of game asset, I’m presuming when it needs to load Tuning files into game assets near the start of the game. Lets check to make sure we’re dealing with object tuning files.

from sims4.resources import Typesif self.TYPE == Types.OBJECT:

We need to get a reference to the games code in charge of affordances. Affordances, in this case, is what we call interactions.

import servicesaffordance_manager = services.affordance_manager()

This gets the code that manages all interaction Tuning files. Now we need to dig through and retrieve the one interaction we’re interested in.

import sims4.resourceskey = sims4.resources.get_resource_key(interaction_id, Types.INTERACTION)
sa_tuning = affordance_manager.get(key)

This is a 2-part process. First we need to get the “key”. The “key” is essentially the name in a table of contents of a giant book that points to our particular interaction. First we need to lookup the “name” of the key and retrieve it, then we can use the key to pull a reference to the actual tuning file.

Now we have the actual tuning file in a variable called sa_tuning, lets loop through all the object ids we created earlier and add this interaction to them. The process is actually very similar to how we retrieved the interaction tuning file.

for obj_id in object_ids:
key = sims4.resources.get_resource_key(obj_id, Types.OBJECT)
obj_tuning = self._tuned_classes.get(key)

Here we’re looping through each object id and we do the same thing. We get the “table of contents name” aka the key and use that to get the particular tuning file for that particular object. The only difference is we’re getting the object tuning file from the InstanceManager class that called this function and not a manager class like the Affordance Manager. The end result is the same.

If there is an error in retrieving the class, it’ll return None. Lets do a quick check to make sure there weren’t any errors. In my case we’re just going to ignore errors and skip to the next object but in your mod you might want to handle things differently, it’s up to you.

if obj_tuning is not None:

We’re at the very final part lets add the interaction to the object’s affordances.

obj_tuning._super_affordances = obj_tuning._super_affordances + tuple([sa_tuning])

This accesses _super_affordances and adds the interaction to it. We first convert the interaction to an array of 1 element and then to a tuple.

That’s it, here’s the whole file because I know we talked about a lot of different code.

import services
import sims4.resources

from sims4.tuning.instance_manager import InstanceManager
from sims4.resources import Types

# Grab original function because we're going to replace it and we still want to be able to call the original
orig_load_data_into_class_instances = InstanceManager.load_data_into_class_instances

# Tuning ID of interaction
interaction_id = 33623

# Tuning id of objects
object_ids = (
77175, 36704, 36800, 77167, 177804, 36816, 36710, 36801, 36690, 36797, 38472, 157486, 206821, 36697, 36699, 36814,
36685, 36669, 36674, 243653, 36694, 36693, 36670, 36682, 178288, 36713, 36701, 177446, 77177, 36689, 36806, 36675,
36799, 36809, 38207, 240819, 36718, 210101, 77170, 36691, 177777, 36702, 36708, 36719, 36671, 178309, 36805, 36803,
36673, 40537, 36684, 36709, 36817, 190222, 36808, 36707, 77178, 36818, 36717, 107641, 38224, 36698, 177806, 38306,
38470, 36711, 212063, 36804, 190007, 38473, 36815, 245629, 36700, 211712, 245627, 36686, 77176, 36798, 36712,
180550, 36692, 36813, 38474, 36812, 107642, 36811, 77168, 77169, 36688, 177808, 77174, 14841, 27248, 144846, 135520,
99157
)

def _load_data_into_class_instances(self: InstanceManager) -> None:
# Call original function
orig_load_data_into_class_instances(self)

if self.TYPE == Types.OBJECT:
# First, get a tuple containing the tunings for all the super affordances...
affordance_manager = services.affordance_manager()

key = sims4.resources.get_resource_key(interaction_id, Types.INTERACTION)
sa_tuning = affordance_manager.get(key)

# Now update the tunings for all the objects in our object list
for obj_id in object_ids:
key = sims4.resources.get_resource_key(obj_id, Types.OBJECT)
obj_tuning = self._tuned_classes.get(key)
if obj_tuning is not None:
obj_tuning._super_affordances = obj_tuning._super_affordances + tuple([sa_tuning])

InstanceManager.load_data_into_class_instances = _load_data_into_class_instances

This is actually safer to do to ensure compatibility with other mods

As I said in part 5. Two mods can often both modify the same object but they cannot both replace the same object. The common way to do what we just did here is to create a duplicate Tuning file for each of those objects and override the in-game ones to use yours instead and to add the same interaction to each of the tuning files. This is how most people do it.

This is obviously bad because we’re then replacing the entire tuning file for everyone of those objects just to add an extra interaction. It means if a player wants to use your mod then they likely can’t use any other mod that may touch those tuning files or objects. In a probably bad analogy, it’s like wanting to add a new menu item to a restaurant menu so you take exclusive and permanent control of the entire kitchen. It’s overkill and it just causes too much problems.

This is a much better solution by far and large, but there’s too much we don’t know. EA has nothing documented and everything is pretty complicated. Hopefully in the future we can workout and understand more of the code so that more modders can create cleaner and saner mods that use better modding practices.

Next Part?

Check in from time to time for part 7

--

--

June Hanabi

I just love creating art, taking photographs, programming, and teaching new skills to people.