Inject interactions by super affordance
Oct 12, 2020 19:25:51 GMT -5
SimmaFierce, august_sun, and 5 more like this
Post by frankk on Oct 12, 2020 19:25:51 GMT -5
STOP! Before following this tutorial, please be aware that it is old. I wrote it when I had just been modding for a few months, and was not aware of better ways to do things. This code should technically still work, but just be aware that it is not the recommended way to do things anymore, and overuse of this injection method can lead to recursion exceptions. I do plan on updating this tutorial, I just don't have the time right now.
If you're familiar with scripting, you can still follow this tutorial, but replace the use of the @inject_to decorator with the add_on_load_complete() method to register a callback on instance manager load.
This tutorial will show you how to inject an interaction to all objects that have a particular super affordance (e.g. adding an interaction to all objects that have the computer locking super affordance, which is all computers). It assumes that you already know the basics of both tuning and script modding.
[1] Why bother?
Imagine that you want to add an interaction to all computers in the game. An issue you're going to realize pretty quickly is that each computer has its own tuning file, which means that you're going to need all 20+ of their tuning IDs, and inject your interaction to each of those manually. This method is tedious, incredibly prone to error, and will not work with new computers that are added to the game or any CC computers.
There are a couple of solutions to this problem. One is to inject your interaction to all objects with a particular tag, which works well, but only for objects that have tags. Another is to inject your interactions to all objects that have a particular super affordance, which offers a bit more flexibility than injecting by tag, and is what I'm going to be showing you.
[2] How to do it
At a high level, the steps to this are pretty simple to understand:
Now, here's how to actually achieve that:
[2.1] Get an injector
An injector can be used to insert your code into the game's code. If you don't have one, you can use the following:
[2.2] Find the object instance manager
The object instance manager holds references to every object in the game. You can use your injector to find this manager, so that you can use it to iterate through all of the objects:
The `@inject_to(InstanceManager, 'load_data_into_class_instances')` line is telling the game to run your function when the `load_data_into_class_instances` method of the `InstanceManager` class is called. This method is called when an instance manager is loaded, so your function will be called every time an instance manager is loaded, too.
Since your function is called on every instance manager loaded event, but the only one we care about is the object instance manager, we're checking that `self.TYPE == Types.OBJECT` before executing our second function (`CREATORNAME_MODNAME_on_object_instance_manager_loaded`). This ensures that the correct instance manager is passed to that function. This function doesn't exist yet, but we'll define it next.
Be sure to replace `CREATORNAME` with your name and `MODNAME` with the name of your mod. Do this for every function you define. It helps reduce collisions between mods.
[2.3] Define your injections
This part kind of acts like a proxy between the object manager loading and the actual injection of interactions to objects. This is how I do it:
Let's break this down:
Ok, so why am I doing it like this? That's an easy question - code reuse. You want to be able to reuse your code for as many interaction/super affordance injections as you need, rather than copy-and-pasting the same function for every new pair of tuning IDs. Let me show you how this works, and why this approach is so powerful.
As its written, the current function will inject the interactions with tuning ID 123456789 to every object that has the super affordance with the tuning ID 12345. If that's all you need it for, then cool. But what if you have more than one interaction to inject? Well, that's why I'm using tuples. You can simply change it to this:
What if you want to inject these two interactions to every object that has either the super affordance 12345 or the super affordance 54321? Again, simple change:
What if you have an entirely different interaction (11223344) to inject to all objects with yet another super affordance (6789)? Just add a new dictionary to the list:
And so on.
One thing to note: Be sure that you always include a comma after EVERY tuning ID, including if there is only one. A missing comma may lead Python to misinterpret your tuple as a single value, which will cause an exception.
Obviously, these are just placeholder tuning IDs. Replace them with your actual tuning IDs, and don't forget to replace `CREATORNAME` with your name and `MODNAME` with the name of your mod.
Now, with that important step out of the way, let's move on to the actual injection: let's define that `CREATORNAME_MODNAME_inject_interactions_by_sas` function that we referenced.
[2.4] Iterate through objects, perform injections where necessary
The key to searching through all of the objects is in this loop right here:
Lets break this down:
You can use this loop (or some variation of it) in the function that we called, but never defined, in section [2.3] above. Let's define it now:
Ok, this is a lot. Let's break this down, too:
And there you go!
[3] Performance
If you're concerned about the performance of this method, here are a couple of things to consider:
--
Hopefully this tutorial helped you! Please leave any questions you have on this thread, and I will respond whenever I get a chance.
If you're familiar with scripting, you can still follow this tutorial, but replace the use of the @inject_to decorator with the add_on_load_complete() method to register a callback on instance manager load.
This tutorial will show you how to inject an interaction to all objects that have a particular super affordance (e.g. adding an interaction to all objects that have the computer locking super affordance, which is all computers). It assumes that you already know the basics of both tuning and script modding.
[1] Why bother?
Imagine that you want to add an interaction to all computers in the game. An issue you're going to realize pretty quickly is that each computer has its own tuning file, which means that you're going to need all 20+ of their tuning IDs, and inject your interaction to each of those manually. This method is tedious, incredibly prone to error, and will not work with new computers that are added to the game or any CC computers.
There are a couple of solutions to this problem. One is to inject your interaction to all objects with a particular tag, which works well, but only for objects that have tags. Another is to inject your interactions to all objects that have a particular super affordance, which offers a bit more flexibility than injecting by tag, and is what I'm going to be showing you.
[2] How to do it
At a high level, the steps to this are pretty simple to understand:
- Iterate through each object in the game
- For each object, check if it has the super affordance you're looking for
- If it does, then inject your interaction to its super affordances list
Now, here's how to actually achieve that:
[2.1] Get an injector
An injector can be used to insert your code into the game's code. If you don't have one, you can use the following:
from functools import wraps
def inject(target_function, new_function):
@wraps(target_function)
def _inject(*args, **kwargs):
return new_function(target_function, *args, **kwargs)
return _inject
def inject_to(target_object, target_function_name):
def _inject_to(new_function):
target_function = getattr(target_object, target_function_name)
setattr(target_object, target_function_name, inject(target_function, new_function))
return new_function
return _inject_to
[2.2] Find the object instance manager
The object instance manager holds references to every object in the game. You can use your injector to find this manager, so that you can use it to iterate through all of the objects:
@inject_to(InstanceManager, 'load_data_into_class_instances')
def CREATORNAME_MODNAME_on_instance_manager_loaded(original, self, *args, **kwargs):
result = original(self, *args, **kwargs)
try:
if self.TYPE == Types.OBJECT:
CREATORNAME_MODNAME_on_object_instance_manager_loaded(self)
except Exception as e:
# Use an error logger here if you have one, if not, just re-raise the exception
raise Exception(f"Error with MODNAME by CREATORNAME: {str(e)}")
return result
The `@inject_to(InstanceManager, 'load_data_into_class_instances')` line is telling the game to run your function when the `load_data_into_class_instances` method of the `InstanceManager` class is called. This method is called when an instance manager is loaded, so your function will be called every time an instance manager is loaded, too.
Since your function is called on every instance manager loaded event, but the only one we care about is the object instance manager, we're checking that `self.TYPE == Types.OBJECT` before executing our second function (`CREATORNAME_MODNAME_on_object_instance_manager_loaded`). This ensures that the correct instance manager is passed to that function. This function doesn't exist yet, but we'll define it next.
Be sure to replace `CREATORNAME` with your name and `MODNAME` with the name of your mod. Do this for every function you define. It helps reduce collisions between mods.
[2.3] Define your injections
This part kind of acts like a proxy between the object manager loading and the actual injection of interactions to objects. This is how I do it:
def CREATORNAME_MODNAME_on_object_instance_manager_loaded(self):
injections_by_sa = [
{
"interactions": (123456789,),
"sas": (12345,)
},
]
for injection in injections_by_sa:
CREATORNAME_MODNAME_inject_interactions_by_sas(self, **injection)
Let's break this down:
- This is the definition of the function that we called from the first function in section [2.2]. The argument `self` is the same `self` that we passed in - this is the object instance manager.
- The variable `injections_by_sa` is just a list of injections you want to perform (`[]` defines a list). This list can contain as many dictionaries (defined with `{}`) as you'd like. If you're unfamiliar with the syntax of lists and dictionaries in python, read about them in the documentation. Each dictionary should contain a key called "interactions" that maps to a tuple of your interactions' tuning IDs, and a key called "sas" that maps to a tuple of the super affordances' tuning IDs. If you're unfamiliar with tuples, they are also explained in the documentation I linked. I'll expand on this more in a minute, because I know it can be a bit tricky if you're unfamiliar with all of these data structures.
- The loop `for injection in injections_by_sa` iterates through the list of dictionaries, temporarily storing each dictionary in the `injection` variable on its iteration. Inside this loop, it will call the `CREATORNAME_MODNAME_inject_interactions_by_sas` function (which we will define in the next section), passing it the interactions and super affordances of each injection.
Ok, so why am I doing it like this? That's an easy question - code reuse. You want to be able to reuse your code for as many interaction/super affordance injections as you need, rather than copy-and-pasting the same function for every new pair of tuning IDs. Let me show you how this works, and why this approach is so powerful.
As its written, the current function will inject the interactions with tuning ID 123456789 to every object that has the super affordance with the tuning ID 12345. If that's all you need it for, then cool. But what if you have more than one interaction to inject? Well, that's why I'm using tuples. You can simply change it to this:
{See the code}
injections_by_sa = [
{
"interactions": (123456789,
987654321,),
"sas": (12345,)
},
]
What if you want to inject these two interactions to every object that has either the super affordance 12345 or the super affordance 54321? Again, simple change:
{See the code}
injections_by_sa = [
{
"interactions": (123456789,
987654321,),
"sas": (12345,
54321,)
},
]
What if you have an entirely different interaction (11223344) to inject to all objects with yet another super affordance (6789)? Just add a new dictionary to the list:
{See the code}
injections_by_sa = [
{
"interactions": (123456789,
987654321,),
"sas": (12345,
54321,)
},
{
"interactions": (11223344,),
"sas": (6789,)
},
]
And so on.
One thing to note: Be sure that you always include a comma after EVERY tuning ID, including if there is only one. A missing comma may lead Python to misinterpret your tuple as a single value, which will cause an exception.
Obviously, these are just placeholder tuning IDs. Replace them with your actual tuning IDs, and don't forget to replace `CREATORNAME` with your name and `MODNAME` with the name of your mod.
Now, with that important step out of the way, let's move on to the actual injection: let's define that `CREATORNAME_MODNAME_inject_interactions_by_sas` function that we referenced.
[2.4] Iterate through objects, perform injections where necessary
The key to searching through all of the objects is in this loop right here:
for _, obj_tuning in self._tuned_classes.items():
if hasattr(obj_tuning, '_super_affordances'):
# Do what you need to do here
Lets break this down:
- `self` is the object instance manager
- `_tuned_classes` is a property of the object instance manager, which maps tuning IDs to actual object tuning instances
- `items()` is a method that is called on the `_tuned_classes` dictionary, and it returns a list of key/value pairs (the keys are the tuning IDs, the values are the tuning instances)
- `for _, obj_tuning in ...` is a loop that is iterating through all of the key/value pairs that was returned by `items()`. The key (tuning ID) is assigned to the `_` variable (we call it `_` because we don't care about it, we're not going to use it), and the value (tuning instance) is assigned to the `obj_tuning` variable. The code within the `for` loop is going to be execute once for every key/value pair, with the variables being updated each time
- `hasattr(obj_tuning, '_super_affordances')` is just making sure that the object has a property called `_super_affordances`, so that we don't get an exception if we try to access it on an object that doesn't exist
You can use this loop (or some variation of it) in the function that we called, but never defined, in section [2.3] above. Let's define it now:
def CREATORNAME_MODNAME_inject_interactions_by_sas(self, interactions=None, sas=None):
affordance_manager = services.get_instance_manager(Types.INTERACTION)
interactions_tuning = tuple(filter(lambda a: a, [affordance_manager.get(a) for a in interactions]))
sas_tuning = tuple(filter(lambda sa: sa, [affordance_manager.get(sa) for sa in sas]))
if not interactions_tuning or not sas_tuning:
return
def has_some_sa(sa_list):
for sa in sas_tuning:
if sa in sa_list:
return True
return False
for _, obj_tuning in self._tuned_classes.items():
if hasattr(obj_tuning, '_super_affordances') and has_some_sa(obj_tuning._super_affordances):
obj_tuning._super_affordances = obj_tuning._super_affordances + interactions_tuning
Ok, this is a lot. Let's break this down, too:
- The argument `interactions` is going to be a tuple of tuning IDs of interactions to inject to objects. The argument `sas` is going to be a tuple of the tuning IDs of the super affordances to check objects for. These values are passed in from the function in [2.3].
- The `affordance_manager` is a class in the game that manages all of the affordances/interactions. We can use it to get the tuning instances from their tuning IDs, which are being passed in through the `interactions` and `sas` arguments.
- `interactions_tuning` is a tuple containing all of the valid tuning instances of our interactions.
- `sas_tuning` is a tuple containing all of the valid tuning instances of the super affordances.
- `if not interactions_tuning or not sas_tuning` checks if either tuple is empty (this may happen if your injection requires a pack, but the user does not have the packs), and will just return from the function if they are, so that no exception occurs.
- `has_some_sa` is a local function which we will use to check if an object has one of the super affordances in the `sas_tuning` tuple
- Finally, that loop. I added `has_some_sa(obj_tuning._super_affordances)`, which will check that the object has at least one of the super affordances we're looking for. If this passes, that final line `obj_tuning._super_affordances = obj_tuning._super_affordances + interactions_tuning` will be executed, which will add our interactions to that object.
And there you go!
[3] Performance
If you're concerned about the performance of this method, here are a couple of things to consider:
- The performance is the same, in terms of big-O time complexity, as injecting interactions by tag. In both cases, the time complexity is O(m*n), where m is the number of objects in the game, and n is the average number of affordances per object or tags per object. In practice, injecting by affordance does turn out marginally less efficient than by tag, but it is insignificant in terms of time complexity, and will not be noticeable.
- I have multiple mods that utilize this method of injecting interactions, and I have never noticed any issues with performance. If you are over-using this method, or are running many mods that use it, combined with having all of the packs and a ton of CC, you may notice some longer loading screens. But when used appropriately, there is no noticeable impact.
- This code only executes during loading screens, so even if it is overused, it will never impact gameplay performance at all - just loading screen time.
--
Hopefully this tutorial helped you! Please leave any questions you have on this thread, and I will respond whenever I get a chance.