Core Library is a collection of useful snippets and tunables for The Sims 4 mods! It's recommended to use it as a dependency to help prevent many injections to common game methods.
Download the latest packaged ts4script from https://lot51.cc/core and place in your Mods folder.
If you are creating a mod that depends on this library, please do not include the packaged library with your files. Direct your users to the url above to ensure they receive the latest patch updates.
Core Library follows Semantic Versioning. Given a version number MAJOR.MINOR.PATCH:
- MAJOR version changes represent a change that breaks backwards compatibility
- MINOR version changes represent new/updated functionality that is backwards compatible
- PATCH version changes represent backwards compatible bug fixes
The TuningInjector allows you to set a minimum core version to prevent any game breaking injections, and will notify the Player they have an incompatible version installed.
It's recommended to set the minimum as a MAJOR.MINOR version to allow your injections to run if a player has a patch version that does not exactly match.
These snippets can be generated and included in your packages to use features provided by the library without any scripting. Additional snippets are available in the snippets
directory but have not yet been documented.
These commands are available in the cheat box, or as a do_command
basic extra in an Interaction.
Opens a URL in the Player's default browser. Params (optional) can be a stringified JSON object with additional url query params.
Forces a refresh of the available stock in a PurchasePickerSnippet that has stock_management
enabled.
from lot51_core.utils.log import Logger
from lot51_core.utils.paths import get_mod_root
from lot51_core.utils.config import Config
# Get the mod root relative to this file.
# By default will traverse up 2 parent directories to handle compiled ts4scripts
mod_root = get_mod_root(__file__, depth=2)
# Create a logger that outputs in mod root
logger = Logger("NameOfYourMod", mod_root, "my_mod.log", version="1.0")
# Create a json config file in mod root
config = Config(mod_root, "my_mod.json", logger, default_data={"custom_setting": False})
# Get a value, and provide a default return value if the setting does not exist
custom_setting = config.get("custom_setting", default=False)
# Set a value in memory and then save. Useful for multiple operations
config.set("custom_setting", True)
config.set("custom_setting_2", True)
config.save()
# Set a value and save it to the json file immediately
config.set_hard("custom_setting", True)
It is recommended to convert 64-bit IDs to a string
before saving to a config file. Then convert the string back to an int
when retrieving it.
The config data is stored in JSON format, and you may experience data loss as 64-bit integer literals are unsupported in certain contexts.
# set
household_id = services.active_household_id()
config.set("saved_household_id", str(household_id))
# get
household_id = int(config.get("saved_household_id", 0))
Many mods rely on code running when the loading screen lifts, or when a Sim travels. As players grow their script mod collection they may find that their game is unable to load due to core game functions having too many injections. Python has a Maximum Recursion Depth that prevents functions from being wrapped more than 1000 times.
Core event handlers can replace most injection points and support a near unlimited amount of listeners. Additionally:
- All exceptions will automatically be caught and written to
lot51_core.log
. - A
context
object will be passed to most events that provides common game state values.
from lot51_core.services.events import event_handler, CoreEvent
@event_handler(CoreEvent.ZONE_LOAD)
def _on_zone_load(service, context=None, **kwargs):
# Returns a DateAndTime of the currently simulated time internally.
# This value will always be <= to game_now
sim_now = context.sim_now
# Returns a DateAndTime of the time displayed in the UI
# representing the time the game should be simulating to.
# This value will always be >= to sim_now
game_now = context.game_now
# Returns true/false
in_world_edit_mode = context.in_world_edit_mode
is_traveling = context.is_traveling
# Returns the current save slot id, this does not change
# when the game switches to a scratch save, so you can
# use it for persisting data per save
save_slot_id = context.save_slot_id
# Returns the name of the current save as a string
save_slot_name = context.save_slot_name
logger.debug("zone has loaded")
Most events mirror methods called by game services, with additional events provided by injections.
Events are generally listed in the order they are called. Use the CoreEvent enum as a shortcut for event names.
* An asterisk denotes an event that receives the context object as a kwarg
Called after all instance managers have loaded. You can perform tuning injections here.
The Core Library global service has been set up and started.
All custom Service classes have been registered.
Called when the game has initialized all game services and is setting them up before starting them.
Raw save game data is being loaded into services to prepare for zone load.
The zone instance is now available. No sims or objects are available yet if this is during the initial launch of the game rather than traveling.
The household and sim info managers have been loaded. This is only called once on the initial load of the save.
At this point all Sims and objects have been spawned into the zone behind the loading screen and can be modified before the Player can see them.
The zone is fully loaded and the loading screen has disappeared. This is the best time to display startup notifications.
Services are preparing to save the game.
Services are now saving data into the protobufs. The zone is still available.
The zone is being destroyed for travel or exit. Most services will be unavailable after this point.
The Core Library global service has been stopped.
A GameObject has been spawned into the world. The obj instance is provided as the 2nd argument.
A GameObject has been removed from the world. The obj instance is provided as the 2nd argument.
Called when the Player has entered build mode
Called when the Player has exited build mode. Useful to detect lot trait changes, or new objects.
This event is useful for creating your own loops based on the server clock as an alternative to alarms. You can even check the difference between game_now and sim_now to detect a large time jump and prevent your mod from clogging the simulation while it catches up. (Generally a jump greater than 30 minutes indicates simulation lag or a player cheating the time forward and your update method should bail out.)
Warning: The simulation and client are single threaded and this event is broadcast on every server tick. Blocking the game loop, or executing long-running code on every tick can cause the game client to completely crash without the ability to generate any LastException files.
This example is a custom service class that will update every 15 sim minutes, unless the simulation threshold has been met.
from date_and_time import TimeSpan, create_time_span
from lot51_core.services.events import event_handler, CoreEvent
from lot51_core import logger
class SomeCustomService:
UPDATE_INTERVAL = 15
SIMULATION_THRESHOLD = 30
def __init__(self):
self._next_update = TimeSpan.ZERO
def can_update(self, context):
if not self.time_jump_detected(context):
if self._next_update == TimeSpan.ZERO or context.sim_now >= self._next_update:
return True
def time_jump_detected(self, context):
diff = context.game_now - context.sim_now
if diff.in_minutes() >= self.SIMULATION_THRESHOLD:
return True
def update(self, context):
if not self.can_update(context):
return
self._next_update = context.sim_now + create_time_span(minutes=self.UPDATE_INTERVAL)
# DO SOMETHING HERE EVERY FIFTEEN MINUTES
logger.debug("4 8 15 16 23 42")
some_custom_service = SomeCustomService()
@event_handler(CoreEvent.GAME_TICK)
def _my_custom_service_update_handler(*args, context=None):
# check if the zone is available
if context.zone is not None:
some_custom_service.update(context)
Exceptions in the handler will automatically be caught and logged to lot51_core.log
as a safeguard to a simulation crash. This usually presents itself as Sims replaying the same animation constantly, walk to their destination and then standing forever, and pie menus not opening. It can sometimes recover by waiting, or opening/closing the ESC menu. Otherwise players will need to force quit the game.
Socket based loops like an HTTP/TCP server should use this event to read AND write in the same thread. Executing code from a separate thread can cause the game client to immediately crash when it attempts to run functions that have been imported from EA's C code.
You may also use the event service to broadcast your own events. Please do not use the reserved event names above. Prefix your events with your creator or mod name to ensure you don't conflict with other creators.
The EventService instance will be provided as the first arg to easily broadcast additional events or responses. Additionally, all parameters passed to process_event
will pass through to the handler.
from lot51_core.services.events import event_service, event_handler
event_service.process_event("my_mod.do_something", additional_data=(1, 2, 3))
@event_handler("my_mod.do_something")
def do_something_handler(service, additional_data=()):
pass
Core Library has an alternative manager to help register "official" services in the game's ServiceManager.
Note: Services are still initialized when entering a zone from world edit mode. It's recommended to check the in_world_edit_mode
boolean on the context object.
from sims4.service_manager import Service
from lot51_core.services.service_manager import service_manager
class MyCustomService(Service):
def save(self, *args, **kwargs):
pass
def pre_save(self, *args, **kwargs):
pass
def setup(self, *args, **kwargs):
pass
def load(self, *args, **kwargs):
pass
def start(self, *args, **kwargs):
pass
def stop(self, *args, **kwargs):
pass
def on_zone_load(self, *args, **kwargs):
pass
def on_zone_unload(self, *args, **kwargs):
pass
def on_cleanup_zone_objects(self, *args, **kwargs):
pass
def on_all_households_and_sim_infos_loaded(self, *args, **kwargs):
pass
# Register your custom service
service_manager.register_service(MyCustomService)
# A helper function to get your service instance
def get_my_service():
return service_manager.get_service(MyCustomService)
MIT License
Copyright (c) 2022 Lot 51
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.