Sims 4 Modding Wiki
Advertisement

This section holds loose leaf scripting examples that other creators have written, such as code pieces with no files, etc. You are free to use the codes here to create your mods; just remember to rename to your preference.

If you are looking for the main page to Scripting Examples, look here.

Injecting Custom Crafting Ingredients into Tag Tuning (Scumbumbo)[]

from crafting.crafting_ingredients import IngredientTuning
from sims4.collections import frozendict
from sims4.localization import TunableLocalizedStringFactory
from tag import TagCategory, Tag

def add_tag_elements(kvp):
    with Tag.make_mutable():
        for k,v in kvp.items():
            if not hasattr(Tag, k):
                Tag._add_new_enum_value(k, v)

# Add the tag elements, values are 32-bit hashes of the 'Func_NAME' tags
TAG_ELEMENTS = {'Func_Ingredient_Roadkill': 3814098884, 'Func_Ingredient_Pepper': 4087606450, 'Func_Ingredient_TreeBark': 3875362104}
add_tag_elements(TAG_ELEMENTS)

# Convert the frozendict to a dict for updating
INGREDIENT_TAG_DISPLAY_MAPPING = dict(IngredientTuning.INGREDIENT_TAG_DISPLAY_MAPPING)

# Add the ingredient strings to the INGREDIENT_TAG_DISPLAY_MAPPING for the newly created Tags
INGREDIENT_TAG_DISPLAY_MAPPING[Tag.Func_Ingredient_Roadkill] = TunableLocalizedStringFactory()._convert_to_value(0x6F5C78E1)  # 0x6F5C78E1 = "Any Roadkill"
INGREDIENT_TAG_DISPLAY_MAPPING[Tag.Func_Ingredient_Pepper] = TunableLocalizedStringFactory()._convert_to_value(0xCC5B8A72)  # 0xCC5B8A72 = "Any Peppers"
INGREDIENT_TAG_DISPLAY_MAPPING[Tag.Func_Ingredient_TreeBark] = TunableLocalizedStringFactory()._convert_to_value(0xF6E73813)  # 0xF6E73813 = "Any Tree Bark"

# Convert back to a frozendict and jam it painfully back into the IngredientTuning
IngredientTuning.INGREDIENT_TAG_DISPLAY_MAPPING = frozendict(INGREDIENT_TAG_DISPLAY_MAPPING)

Injecting Custom Tags into Tag Tuning (Scumbumbo)[]

from tag import TagCategory, Tag

def add_tag_categories(kvp):
    with TagCategory.make_mutable():
        for k,v in kvp.items():
            TagCategory._add_new_enum_value(k, v)

def add_tag_elements(kvp):
    with Tag.make_mutable():
        for k,v in kvp.items():
            Tag._add_new_enum_value(k, v)

# Use 32-bit hashes of the names for the values
TAG_CATEGORIES = {'DingoColor': 100001, 'WingSize': 100002, 'FlatulenceLevel': 100003}
TAG_ELEMENTS = {'Func_Hair_Removal': 100001, 'Func_NoseStraightener': 100002, 'Func_ChickenPlucker': 100003}

add_tag_categories(TAG_CATEGORIES)
add_tag_elements(TAG_ELEMENTS)

Injecting Tags (Frankk)[]

def do_tag_injections():
    with Tag.make_mutable():
        Tag._add_new_enum_value('TAG_NAME', 123456)
        Tag._add_new_enum_value('OTHER_TAG', 654321)


do_tag_injections()

Making a Configuration File for Your Mods (Frankk)[]

(The following was a transcript from the Discord Server: Creator Musings)

After battling this process for multiple hours, I finally figured out how to make and use a config file in a mod. If you don't know, a config file can be used to store settings and allow users to edit them on their own. Here are my tips if you're looking to do the same: 1. Create a .cfg file in your mod's directory. You can name it whatever you want, but modname_settings.cfg is probably a good idea. 2. Read about the syntax of the config file here if you aren't already familiar with it: https://docs.python.org/3/library/configparser.html (NOTE: Don't bother reading the configparser documentation. The Sims team wrote their own configparser.py that overrides this one. Just read the syntax of the config file, as it's the same for both) 3. Parse your config file in a script with the following:

import os
import configparser
from pathlib import Path

# `Path(__file__).resolve()` finds the path to the current file
# The first `.parent` is to step above this file, which is the .ts4script
# The second `.parent` is to step above the .ts4script and into the directory alongside your mod
# Add one more `.parent` for each package that this file is nested in, if any
config_dir = Path(__file__).resolve().parent.parent
config_name = 'modname_settings.cfg'  # change this to your file's name
file_path = os.path.join(config_dir, config_name)

config = configparser.ConfigParser()
with open(file_path) as file:
    config.read_file(file)

And there you go! Now you have the config file loaded into your script. If your config file looks like this:

[header]
key = value
some other key = some other value

[another header]
key = another value

You can get the values like this:

config.get("header", "key")  # this returns "value"
config.get("header", "some other key")  # this returns "some other value"
config.get("another header", "key")  # this returns "another value"

Just as an additional example to show you what this can actually be used for, here is the config file that I will be including in Language Barriers:

[REGIONS]
Brindleton Bay     = Simlish
Britechester       = Simlish
Del Sol Valley     = Simlish
Evergreen Harbor   = Simlish
Forgotten Hollow   = Simlish
Glimmerbrook       = Simlish
Magnolia Promenade = Simlish
Newcrest           = Simlish
Oasis Springs      = Simlish
San Myshuno        = Simlish
Strangerville      = Simlish
Willow Creek       = Simlish

If a user wants to make Oasis Springs speak Selvadoradian, then they just need to replace that line with:

Oasis Springs      = Selvadoradian

This can be super helpful for people looking to give users customization options!

Getting Client Cheat Commands to Run in XML (TwelfthDoctor1)[]

(The following was a transcript from the Discord Server: Creator Musings)

I just found out that inside sims4.commands, there is a output type called client_cheat(). client_cheat() is basically the console you use to type cheats in. Unlike execute(), client_cheat() is used to execute commands that are not in Simulation such as bb.showhiddenobjects. I found that in developer_commands, commands like bb.showwipobjects and rr.toggletime 12 aren't found in Simulation. It could be in the Core, but I know that some files are not fully decompiled (well, based on one of my scripts when I decompile it from TS4SCRIPT it only got 2 of 4 commands.).

So based on the developer_commands code, I wrote up this:

@Command('td1devaccess.call_client_command')
def call_client_command(identifier:str, state:bool, _connection=None):
    output = CheatOutput(_connection)
    identifier_lc = identifier.lower()
    result = get_command_and_sort_from_list(identifier_lc, state, _connection)
    output('{}'.format(result))


def get_command_and_sort_from_list(identifier, state, _connection):
    if identifier is None:
        result = '[FAILURE] Command Identifier Missing. Please specify command.'
        return result
    if state is None:
        state = False
    for (enable_cmd, disable_cmd, is_client) in commands_list:
        if enable_cmd.find(identifier) != -1:
            if is_client is True:
                if state is True:
                    client_cheat(enable_cmd, _connection)
                    result = '[SUCCESS] Client Cheat: ' + str(enable_cmd) + ' is now enabled.'
                    return result
                else:
                    client_cheat(disable_cmd, _connection)
                    result = '[SUCCESS] Client Cheat: ' + str(disable_cmd) + ' is now disabled.'
                    return result
            else:
                if state is True:
                    execute(enable_cmd, _connection)
                    result = '[SUCCESS] Command Cheat: ' + str(enable_cmd) + ' is now enabled.'
                    return result
                else:
                    execute(disable_cmd, _connection)
                    result = '[SUCCESS] Command Cheat: ' + str(disable_cmd) + ' is now disabled.'
                    return result
    result = '[FAILURE] Command: ' + str(identifier) + ' is not within TD1 Developer Access Panel Command List.'
    return result

Also there needs to be a list with a tuple along with it:

commands_list = [
    (
        # Testing Cheats
        'testingcheats true', 'testingcheats false', False
    ),
    (
        # Free Build Mode: For Generally Uneditable Lots
        'bb.enablefreebuild', '', True
    ),
    (
        # Move Objects
        'bb.moveobjects on', 'bb.moveobjects off', True
    ),
    (
        # Show Debug Objects
        'bb.showhiddenobjects', '', True
    ),
    (
        # Show Live Edit Objects
        'bb.showliveeditobjects', '', True
    ),
    (
        # Unlock Gameplay Objects
        'bb.ignoregameplayunlocksentitlement', '', True
    ),
    (
        # Show Work In Progress Objects: ???
        'bb.showwipobjects', '', True
    ),
    (
        # Shorten Crafting Phases
        'crafting.shorten_phases on', 'crafting.shorten_phases off', False
    ),
]

To make things easy, we start at the Command. td1devaccess.call_client_command has 2 params: identifier and state. identifier is the string command that is used to check against the enable command inside the list-tuple. (i.e. bb.enablefreebuild contains enablefreebuild). state is to determine if it is enabled or disabled. Now the list-tuple has 3 things: the enable command, disable command and the modifier to tell if it is a client command (basically not in Simulation). Each one will be used to test and be applied accordingly. Example: You want to run the bb.showhiddenobjects , and based from the list, it is a client_cheat. So the identifier (bb.showhiddenobjects) will be matched against enable_cmd, it will continue doing this until it matches for one or fails, in this case it finds it. So we check if it needs client_cheat based on is_client, if yes we check the state, True for enable, False for disable and then it will execute the command. (Note that non client is still the same just that the mode of execution is different, thats all.) Actually if you want it SIMPLE, this will suffice:

@Command('td1devaccess.call_client_command')
def call_client_command(command:str, _connection=None):
    output = CheatOutput(_connection)
    client_cheat(command, _connection)
    output('Client Command: {} has been executed.'.format(command))

Well just saying that this quick command script here cannot tell you whether the command has worked. So you will need to double check before using it, most likely in XML.

ConfigParser - Auto Making, Appending and Changing Values In-Game (TwelfthDoctor1)[]

The reader should know some understanding of Python File Handling and ConfigParser functions before attempting this example.

Config files is something commonly used by games and even less common by mods, unless it has certain functions that can be toggled. Originally, my "config file" was the Module Tuning, but with users needing S4S just to change an option, I had to figure out how to write my own config handler for my big mod.

Note: As the basic parts have been covered by Frankk, I will only cover the deeper components.

Auto Writing a Config File[]

I am not really fond of self creating a config file and shipping that out with the mod, I want it to be auto created so that it makes it easier for me and the users.

config_key_list = [
    'disable_notifs',
    'override_debug_menu',
    'force_debug_cheat_menu',
]

preset_modifiers = [
    "False",
    "True",
    "True"
]


def config_prep_file():
    """
    Configures and creates a brand new Config File for use.
    :return:
    """
    with open(get_config_dir(), "w") as config_file:
        config_file.write("[TD1 Developer Access Panel Config Settings]")
        # Incrementor Int by default is presumed as 0 and increments from there.
        # List Assigns start off as 0 as well.
        for incrementor in range(len(config_key_list)):
            config_file.write("\n" + config_key_list[incrementor] + " = " + preset_modifiers[incrementor])

        config_file.close()

It is possible to create a file from a script, you will need to know Python's File Handling functions in order to implement it. In the case of a config file, you need the section or header and the options with their values.

As a time saver, it is marginally easier to use lists with a for loop to write each option with their value. Having the write function in each line can be wasteful, confusing and not efficient, especially in the long run.

Appending and Auto Checking[]

I also had to consider each update, especially if I were to add new options to toggle. Not only that, with updates, people may not have that option available, and I'd rather not ship a config file update.

def config_init_check():
    """
    Runs an initial check on the config file for any missing options.
    Prudent on newer versions of the mod with config implementations.
    :return:
    """
    with open(get_config_dir(), "r") as config_file:
        config.read_file(config_file)
        for incrementor in range(len(config_key_list)):
            if config.has_option(main_header, config_key_list[incrementor]) is False:
                append_option(incrementor)

        if apprentice_logger is not None and apprentice_logger is not False:
            apprentice_logger.info("Config List Check Success.", owner="TwelfthDoctor1")

        if master_logger is not None and master_logger is not False:
            master_logger.info("Config List Check Success.", owner="TwelfthDoctor1")


def append_option(identifier):
    """
    Appends missing options into the config file.
    :param identifier:
    :return:
    """
    option = config_key_list[identifier]
    modifier = preset_modifiers[identifier]
    with open(get_config_dir(), "a") as config_file:
        config_file.write("\n" + option + " = " + modifier)
        config_file.close()

        if apprentice_logger is not None and apprentice_logger is not False:
            apprentice_logger.info("Option Key: {0} has been appended into Config File.".format(option), owner="TwelfthDoctor1")

        if master_logger is not None and master_logger is not False:
            master_logger.info("Option Key: {0} has been appended into Config File.".format(option), owner="TwelfthDoctor1")

    with open(get_config_dir(), "r") as config_file:
        return config_file

So, config_init_check will run and with sieve through all the options to see if it exists on the list. If it doesn't, it will call append_option which will append the missing option into the config file.

Now my method here does not deal with duplicates and those out of order, because my presumption is that no one would modify the config file in terms of changing the position of each option. But you are always welcome to include that option in your scripts.

Changing an Option In-Game[]

The reason why you want users to change the config options in game is that it makes it easier for them to understand, you would rather not ask them to edit the config file, they'd be confused if there isn't anything to help them understand what they are changing.

@Command('td1devaccess.set_option', command_type=CommandType.Live)
def set_option(option, _connection=None):
    output = Output(_connection)
    with open(get_config_dir(), "r") as config_file:
        config.read_file(config_file)
        if config.has_option(main_header, option):
            orig_value = config.get(main_header, option)
            config_file.close()

            for key in config_key_list:
                if key == option:
                    option_toggle(config_key_list.index(key))

        else:
            output("Wrong Option Specified. Please refer to config list and try again.")
            config_file.close()


def option_toggle(identifier):
    client = services.client_manager().get_first_client()
    option = config_key_list[identifier]

    if config.has_option(main_header, option) is False:
        append_option(identifier)
    else:
        with open(get_config_dir(), "r") as config_file:
            config.read_file(config_file)
            setting_value = config.getboolean(main_header, option)
            print("{0} {1}".format(option, setting_value))

            if setting_value is True:
                value = "False"
                config.set(main_header, option, value)
                config.write(open(get_config_dir(), "w"))

            elif setting_value is False:
                value = "True"
                config.set(main_header, option, value)
                config.write(open(get_config_dir(), "w"))

        if apprentice_logger is not None and apprentice_logger is not False:
            apprentice_logger.info("Option: {0} has been set to {1}. (Formerly {2})".format(option, value, setting_value), owner="TwelfthDoctor1")

        if master_logger is not None and master_logger is not False:
            master_logger.info("Option: {0} has been set to {1}. (Formerly {2})".format(option, value, setting_value), owner="TwelfthDoctor1")

By far, this is the biggest hurdle to get through, as instead writing directly to the config file, you NEED to use ConfigParser itself to write (denoted as config.write) to the file as the regular write has no clue on what to change and write. Also, the set function only sets the option, it does not set it directly, you still need to use the write function to change the option value or no changes will take effect.

Now don't ask me why why it had to be done this way, its how its done in the Python Docs.

At the end of all this, you could have a code like this:

config_key_list = [
    'disable_notifs',
    'override_debug_menu',
    'force_debug_cheat_menu',
]

preset_modifiers = [
    "False",
    "True",
    "True"
]

main_dir = Path(__file__).resolve().parent.parent

config = configparser.ConfigParser()

main_header = 'TD1 Developer Access Panel Config Settings'


def get_config_dir():
    """
    Gets the Configuration File Directory.

    No Params Required.
    """

    log_name = "TD1_AccessPanel_Settings.cfg"

    config_dir = os.path.join(main_dir, log_name)

    return config_dir


def config_prep_file():
    """
    Configures and creates a brand new Config File for use.
    :return:
    """
    with open(get_config_dir(), "w") as config_file:
        config_file.write("[TD1 Developer Access Panel Config Settings]")
        # Incrementor Int by default is presumed as 0 and increments from there.
        # List Assigns start off as 0 as well.
        for incrementor in range(len(config_key_list)):
            config_file.write("\n" + config_key_list[incrementor] + " = " + preset_modifiers[incrementor])

        config_file.close()


if os.path.exists(get_config_dir()) is True:
    with open(get_config_dir(), "r") as config_file:
        config.read_file(config_file)

        if apprentice_logger is not None and apprentice_logger is not False:
            apprentice_logger.info("TD1 DevAccessPanel Config File Found.", owner="TwelfthDoctor1")

        if master_logger is not None and master_logger is not False:
            master_logger.info("TD1 DevAccessPanel Config File Found.", owner="TwelfthDoctor1")

else:
    config_prep_file()
    with open(get_config_dir(), "r") as config_file:
        config.read_file(config_file)

        if apprentice_logger is not None and apprentice_logger is not False:
            apprentice_logger.info("TD1 DevAccessPanel Config File Missing. Creating New Config File...", owner="TwelfthDoctor1")

        if master_logger is not None and master_logger is not False:
            master_logger.info("TD1 DevAccessPanel Config File Missing. Creating New Config File...", owner="TwelfthDoctor1")


@Command('td1devaccess.list_config', command_type=CommandType.Live)
def print_config_values(_connection=None):
    output = Output(_connection)
    with open(get_config_dir(), "r") as config_file:
        config.read_file(config_file)
        for incrementor in range(len(config_key_list)):
            value = config.get(main_header, config_key_list[incrementor])
            output("{}: {}".format(config_key_list[incrementor], value))


@Command('td1devaccess.set_option', command_type=CommandType.Live)
def set_option(option, _connection=None):
    output = Output(_connection)
    with open(get_config_dir(), "r") as config_file:
        config.read_file(config_file)
        if config.has_option(main_header, option):
            orig_value = config.get(main_header, option)
            config_file.close()

            for key in config_key_list:
                if key == option:
                    option_toggle(config_key_list.index(key))

        else:
            output("Wrong Option Specified. Please refer to config list and try again.")
            config_file.close()


def config_init_check():
    """
    Runs an initial check on the config file for any missing options.
    Prudent on newer versions of the mod with config implementations.
    :return:
    """
    with open(get_config_dir(), "r") as config_file:
        config.read_file(config_file)
        for incrementor in range(len(config_key_list)):
            if config.has_option(main_header, config_key_list[incrementor]) is False:
                append_option(incrementor)

        if apprentice_logger is not None and apprentice_logger is not False:
            apprentice_logger.info("Config List Check Success.", owner="TwelfthDoctor1")

        if master_logger is not None and master_logger is not False:
            master_logger.info("Config List Check Success.", owner="TwelfthDoctor1")


config_init_check()


def option_toggle(identifier):
    client = services.client_manager().get_first_client()
    option = config_key_list[identifier]

    if config.has_option(main_header, option) is False:
        append_option(identifier)
    else:
        with open(get_config_dir(), "r") as config_file:
            config.read_file(config_file)
            setting_value = config.getboolean(main_header, option)
            print("{0} {1}".format(option, setting_value))

            if setting_value is True:
                value = "False"
                config.set(main_header, option, value)
                config.write(open(get_config_dir(), "w"))

            elif setting_value is False:
                value = "True"
                config.set(main_header, option, value)
                config.write(open(get_config_dir(), "w"))

        if apprentice_logger is not None and apprentice_logger is not False:
            apprentice_logger.info("Option: {0} has been set to {1}. (Formerly {2})".format(option, value, setting_value), owner="TwelfthDoctor1")

        if master_logger is not None and master_logger is not False:
            master_logger.info("Option: {0} has been set to {1}. (Formerly {2})".format(option, value, setting_value), owner="TwelfthDoctor1")

        message_title_config_change = lambda **_: LocalizationHelperTuning.get_raw_text("Access Panel Option Change")
        message_text_config_change = lambda **_: LocalizationHelperTuning.get_raw_text("The option: {} has been changed from {} to {}.\n\nA restart may be required for changes to take effect.".format(
            option, setting_value, value
        ))

        notification_config_change = UiDialogNotification.TunableFactory().default(
            client.active_sim,
            text=message_text_config_change,
            title=message_title_config_change,
            expand_behavior=UiDialogNotification.UiDialogNotificationExpandBehavior.FORCE_EXPAND,
            urgency=UiDialogNotification.UiDialogNotificationUrgency.URGENT
        )
        notification_config_change.show_dialog()


def get_option_value(option):
    """
    Gets the value from the option (key) in the Config List.
    :param option:
    :return:
    """
    with open(get_config_dir(), "r") as config_file:
        config.read_file(config_file)
        value = config.getboolean(main_header, option)
        return value


def append_option(identifier):
    """
    Appends missing options into the config file.
    :param identifier:
    :return:
    """
    option = config_key_list[identifier]
    modifier = preset_modifiers[identifier]
    with open(get_config_dir(), "a") as config_file:
        config_file.write("\n" + option + " = " + modifier)
        config_file.close()

        if apprentice_logger is not None and apprentice_logger is not False:
            apprentice_logger.info("Option Key: {0} has been appended into Config File.".format(option), owner="TwelfthDoctor1")

        if master_logger is not None and master_logger is not False:
            master_logger.info("Option Key: {0} has been appended into Config File.".format(option), owner="TwelfthDoctor1")

    with open(get_config_dir(), "r") as config_file:
        return config_file

Well...I cannot fully explain this as ConfigParser is not within my territory of full understanding. But hopefully whoever reads this may understand a little bit more on config files and ConfigParser.

Also Yes I know there is the RawConfigParser, it still depends on what you want to use, but you also note that RawConfigParser uses older and unsafe methods of changing values, it is recommended you use the ConfigParser instead.

Advertisement