The Sims 4 Modern Python Modding: Part 4 — Replacing Class Code

June Hanabi
The Startup
Published in
10 min readSep 26, 2020

What we’re going to do is replace another piece of code to get more practice at it and we’re going to step it up a small bit to make thing just a tiny bit more challenging.

This tutorial was fairly difficult to make happen because it took a long time to track down the needed code and then go through the many road blocks that happened. I’d say it took me close to three days to get to this point and quite a bit of frustration, just remember patience and practice is key to mastering Sims 4 python scripting.

I feel this tutorial is a bit dense. The program we’re making is a little over 25 lines of code, however it really goes into a lot of explanation and breaks everything down properly. Please take it slow to understand each section and ask questions if you have any.

Make sure you start at part 1

The projects here assume your up-to-speed on how to mod for the Sims 4 using python scripting which I already covered. If you’re new to this series I encourage you to start at part 1.

What we’re going to make

We are going to be creating a mod called “Whimsical Charges” where the service pricing for maids, repair people, and even pizza are a bit more looser than advertised.

If it sounds complicated don’t worry it’s actually not especially if you’ve been following along up until now. In part 3 you learned how to replace regular function code and here you’re going to learn how to replace class code specifically marked @ classmethod which has to be replaced a tad differently. We’re also going to cover managing a variable and allowing it to be changed from the cheat console.

Where to start?

To save you hours of code searching, we’re going to start in simulation/situations/service_npcs/service_npc_tuning.py which translates to the package situations.service_npcs.service_npc_tuning. Inside that file is code related to Service NPCs and we’re interested in a class called ServiceNpcHireable

There’s one little method inside this class which is what we’re interested in replacing.

A few things to note:

  • It begins with an @ classmethod meaning normal code replacement won’t work because the function is tied to the class in a special way. For us to replace the code we have to replicate that connection to the class which is still very easy to do.
  • cls refers to the class, “Class Methods” like this one usually start with that instead of “self”. Looking at the code we can see cost_up_front and cost_hourly are inside cls.
  • The other parameters refer to the amount of time worked in hours (It’s a float number (decimal number) so it calculates tiny fractions of an hour as well and whether to include a “Base Price” in addition to the hourly charge.
  • We can see it calculates this as we think it would, multiplying hourly wage by hours worked and adding the base price if requested then returning the amount. It’s clear it wants to return an integer (Whole number) so we can presume not doing so may result in an error of some sort. (It’s important when replacing code to not change the expected behavior)

Copying the code over

Copy the code over to a file, as usual I call mine main.py

def get_cost(cls, time_worked_in_hours, include_up_front_cost=True):
cost = int(time_worked_in_hours * cls.cost_hourly)
if include_up_front_cost:
cost += cls.cost_up_front
return cost

Looking over the code carefully we can see we actually don’t have any dependency imports to copy over from the same file. The code just deals with numbers (whole numbers and fractions) making this unexpectedly even simpler. (In the previous part, part 3, we had a couple of things to copy over for comparison to this)

Lets go ahead and import the file though so that when the game starts and our mod is loaded we can replace the code with our own copy.

from situations.service_npcs.service_npc_tuning import ServiceNpcHireable

and lets tell the game to entirely replace EA’s code with our code. The method we’re replacing is a classmethod. Python provides a special piece of code to do exactly what we’re wanting to do so lets import it.

from types import MethodType

and with that here is our end result

from types import MethodType
from situations.service_npcs.service_npc_tuning import ServiceNpcHireable
def get_cost(cls, time_worked_in_hours, include_up_front_cost=True):
cost = int(time_worked_in_hours * cls.cost_hourly)
if include_up_front_cost:
cost += cls.cost_up_front
return cost
ServiceNpcHireable.get_cost = MethodType(get_cost, ServiceNpcHireable)

Congrats, you’ve made a functioning mod that replaces EA’s code with the same code from EA. Although comically useless right now it’s now opened the door to do more stuff.

To explain that last line, MethodType returns a method that’s specially tied to a class. We give MethodType the name of our copy of the function and the name of the class we want to replace the original code in. It returns the same function but tied to the class so it’s safe to replace the original with. If we didn’t use MethodType then it would replace the original code with a normal plain method that has no special ties to the class and your mod would end up crashing at the start of the game with a weird and confusing error.

Some more imports

Lets add the rest of the imports and get them out of the way, we’ll use them as the tutorial progresses.

import random
from sims4.commands import Command, CheatOutput, CommandType, BOOL_TRUE

The first line imports python standard code for randomization and the 2nd line imports a number of game code related to the cheat console.

A single global variable

Next, lets create a global variable

whimsical_charges_enabled = True

We’re going to give the player the ability to enable and disable our mod while the game is running using the cheat console and to do that we need to keep track of whether it’s enabled or not.

Adding in the code to use a cheat code

This section is dense with information as I explain each line carefully and fully. Please don’t get overwhelmed and take it slowly making sure you fully understand it on each line. Ask questions if need be.

Next we’re going to create a cheat code that can be used to enable and disable our mod for the players convenience. Registering a cheat command was covered in part 2 and we’re going to cover it in more detail here.

@Command('whimsical_charges.enable', command_type=CommandType.Live)
def _whimsical_charges_enable(enable: str = None, _connection=None):
global whimsical_charges_enabled

if enable is not None:
whimsical_charges_enabled = enable.lower() in BOOL_TRUE

output = CheatOutput(_connection)
output('whimsical_charges.enable = ' + str(whimsical_charges_enabled))

Lets break this down

@Command('whimsical_charges.enable', command_type=CommandType.Live)

The first line tells the game to add this cheat to the game as a valid cheat code. We give the actual cheat code whimsical_charges.enable and categorize it as a normal cheat code (not requiring testingcheats to be enabled) and to only be used on a playable lot.

When the user types in the cheat code, anything they type after it separated by spaces can be sent to your function as strings if you allow for it. For example, intestingcheats true the first word tells the game to call the associated function for testingcheats and the 2nd word "true" is sent to that function as a string argument. In our case we’re going to take advantage of this as well so that the player can specifically enable or disable the mod.

def _whimsical_charges_enable(enable: str = None, _connection=None):

On the 2nd line we make room for one argument called enable . Since it’s always going to be a string we tell the editor this and default it to None so it’s optional. We want the player to not always have to type something after the command. For example, what if the player just wants to type whimsical_charges.enable by itself. Perhaps we can return if it’s currently enabled or not and if the player specifically wants to set it being enabled or not can do so like this whimsical_charges.enable true.

Also something not covered fully in part 2, when the player enters a cheat code, the game calls your function with the arguments if the player provided any along with a special number that’s not important to know but important to use. That number allows you to print messages to the cheat console. We default it to None because we want all the arguments to be optional to ensure a smooth error-free experience but that number will always be there. If it’s not, then this is an error within the game itself so you don’t have to worry about it.

global whimsical_charges_enabled

The 3rd line tells python to be able to both read and write to the global variable we created earlier (so that we can change whether the mod is enabled or not). If we didn’t add this line, python would only be able to read the global variable.

if enable is not None:
whimsical_charges_enabled = enable.lower() in BOOL_TRUE

So a few things are happening here:

  • We defaulted the optional argument to None meaning if the player enters the cheat code whimsical_charges.enable but doesn’t enter something after it, then enable will be None. Here we’re checking if the player entered an argument at all and we want to skip using enable if the player never set it to begin with.
  • If the player did enter something after the cheat code we move to the next line. We take what the player entered and make it lower case (so it’s not case-sensitive) then check to see if it’s in the EA official list for values considered to be true. This means someone can enter any of these ‘t’, ‘true’, ‘on’, ‘1’, ‘yes’, ‘y’, or ‘enable’ because EA considers all of them to mean true and different players have different preferences. If it’s not in the list then it’s automatically false.
  • This means the player can enter whimsical_charges.enable yes or whimsical_charges.enable 1 and our code will enable the mod because both of those answers are in the official list. We don’t import the official list for false answers because there’s no need to, if it’s not true it’s presumed to be false (Keep it simple). This also comically means the player can type whimsical_charges.enable nuh-uh and it’ll be correctly disabled since nuh-uh is not in the list for true values.
output = CheatOutput(_connection)
output('whimsical_charges.enable = ' + str(whimsical_charges_enabled))

Finally we have this, it takes the special number the game gave us and grabs the cheat textbox. It then writes a message to the player informing them of what the enable/disable status currently is for the mod. so if the player types whimsical_charges.enable yes our code writes whimsical_charges.enable = True to let the player know it’s enabled.

Remember how we made the enable/disable optional above? This allows the player to type in just whimsical_charges.enable which will just print out whether it’s enabled or not which is handy.

Creating the core functionality with our duplicate function

These are the last lines of code. It’s a duplicate of the code at the start of the tutorial but tailored to be how we want it to.

def get_cost(cls, time_worked_in_hours, include_up_front_cost=True):
whimsical_percent = 1.00
if whimsical_charges_enabled:
whimsical_percent = random.randrange(0, 400) * 0.01

cost = time_worked_in_hours * cls.cost_hourly * whimsical_percent
if include_up_front_cost:
cost += cls.cost_up_front * whimsical_percent

return int(cost)

I’m going to break it down into detail to ensure you understand everything.

def get_cost(cls, time_worked_in_hours, include_up_front_cost=True):

This is just a blatant copy of the original, in almost all cases you want the function header to be a blatant copy because changing the function header could really create many issues given the other game code still expect it to be the same way and will continue to work with it as such.

whimsical_percent = 1.00

This is the random percent to adjust the service price by. We default it to 1.00 because if the mod is disabled we want to multiply by 1.00 instead so that no adjustment happens.

if whimsical_charges_enabled:
whimsical_percent = random.randrange(0, 400) * 0.01

Here we reference the global value whimsical_charges_enabled we don’t use the global keyword because we only need to read the global variable. If our mod is enabled then we calculate a random adjustment percent. Here we calculate between a free service (0.0) up until 4 times the normal price (4.0).

Python likes to pick a random range between whole numbers so we convert the whole number to a decimal at the end.

cost = time_worked_in_hours * cls.cost_hourly * whimsical_percent
if include_up_front_cost:
cost += cls.cost_up_front * whimsical_percent

This is really the same as the original code, the only difference is we multiply 1 additional number, the adjustment percent, to both.

return int(cost)

Finally we return the number back to the game, since the game expects an int (whole number) it’s imperative after all this multiplying against decimal numbers and percents that we drop everything off after the decimal and make it a whole number. I’m doing this once at the very end for simplicity.

That’s it

Here is the finished code

import random
from types import MethodType
from situations.service_npcs.service_npc_tuning import ServiceNpcHireable
from sims4.commands import Command, CheatOutput, CommandType, BOOL_TRUE
whimsical_charges_enabled = True@Command('whimsical_charges.enable', command_type=CommandType.Live)
def _whimsical_charges_enable(enable: str = None, _connection=None):
global whimsical_charges_enabled
if enable is not None:
whimsical_charges_enabled = enable.lower() in BOOL_TRUE
output = CheatOutput(_connection)
output('whimsical_charges.enable = ' + str(whimsical_charges_enabled))
def get_cost(cls, time_worked_in_hours, include_up_front_cost=True):
whimsical_percent = 1.00
if whimsical_charges_enabled:
whimsical_percent = random.randrange(0, 400) * 0.01
cost = time_worked_in_hours * cls.cost_hourly * whimsical_percent
if include_up_front_cost:
cost += cls.cost_up_front * whimsical_percent
return int(cost)ServiceNpcHireable.get_cost = MethodType(get_cost, ServiceNpcHireable)

It’s only 29 lines of code, not that big of a step up, but I’m sure it probably feels that way. We covered a whole lot and I’d really recommend you re-read over it closely. Take your time with this and don’t go too fast.

Really the best way to learn this is to goof off and play around with it on your own. Have fun, make mistakes, and ask questions or use google to get yourself out of issues. That’s really the best way to really understand this. If you only read and follow what I type out here you’re never going to get it as well.

The End Result

A maid stopped by the house, put a dirty dish in the sink, and then left after deciding to charge me 3 times the normal price when the very day prior the maid barely charged me anything. I have mixed feelings on this mod lol.

Publication to Mod the Sims

This has been accepted into Mod The Sims as a properly packaged mod. Link Here.

Next part?

When you’re ready, you can proceed to part 5.

--

--

June Hanabi
The Startup

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