The Sims 4 Modern Python Modding: Part 4 — Replacing Class Code
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
andcost_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 ServiceNpcHireabledef 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 costServiceNpcHireable.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 codewhimsical_charges.enable
but doesn’t enter something after it, thenenable
will beNone
. Here we’re checking if the player entered an argument at all and we want to skip usingenable
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 meantrue
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
orwhimsical_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 forfalse
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 typewhimsical_charges.enable nuh-uh
and it’ll be correctly disabled sincenuh-uh
is not in the list fortrue
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_TRUEwhimsical_charges_enabled = True@Command('whimsical_charges.enable', command_type=CommandType.Live)
def _whimsical_charges_enable(enable: str = None, _connection=None):
global whimsical_charges_enabledif enable is not None:
whimsical_charges_enabled = enable.lower() in BOOL_TRUEoutput = 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.01cost = time_worked_in_hours * cls.cost_hourly * whimsical_percent
if include_up_front_cost:
cost += cls.cost_up_front * whimsical_percentreturn 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.