Hi there! You are currently browsing as a guest. Why not create an account? Then you get less ads, can thank creators, post feedback, keep a list of your favourites, and more!
Deceased
Original Poster
#1 Old 2nd Apr 2016 at 10:12 PM
Default The Inspector -- ADVANCED Scripting
Seriously -- this is advanced stuff, don't even read this if you're just starting out. You need to understand TS4 XML tuning, and have at least a decent knowledge of Python modding of TS4. I can help you to understand, so feel free to ask questions, but any beginner questions like, "I can't get the inspector script to run... why?" will be brutally ignored.

OK, so here's the mini-guide (ok, it's actually kind of long!) that I've been wanting to get done for inspecting variables in game for the purposes of determining how to update a tuning on the fly from scripting.

I've included an inspector script that makes things a lot easier, minimizing the number of times you have to inspect a variable, to find out what other variable you want to inspect, modifying the code, reloading, and going back to step 1. This will let you specify a tuning ID number to inspect and will dump the results to the cheat output or a log file. You can then just retype the command and add the variable of that tuning you want to inspect and see the output and repeat until you hit a wall. Then you have to start doing the edit/reload/run cycle to finish the investigation. This should become clearer as you read the mini-guide, using
Inge's Affordance Filter snippet request as an example.

First, you need to install the included inspector.py script and include the reload script in the same folder. Since this is a script we want to be able to reload on the fly in the game without having to exit and reload TS4, it needs to be a "loose" script not compiled in a zip file. So create a folder in mods called Script Testing or something, and a subfolder in there called Scripts. The Scripts subfolder name MUST be named Scripts. So in my setup, I have a subfolder called Script Testing\Scripts in my mods folder and the inspector and reload scripts should go into that Scripts folder. The game can now load the scripts from there, and the reload script will allow you to edit the inspector and just type reload inspector in the cheat command window to reload if you make changes.

Now we're ready to load the game up and try out our example process. We are trying to find out what variable to change in order to add an item to the include_affordances of the af_Retail_Item_Sold (id 110412) snippet. So let's start out by using the inspect command to look at it. Open the cheat window and type in the command inspect 110412. You should get the output below. Note that you can instead use the command inspect.log 110412 and the output will go to the inspector.log text file in the Scripts folder.

Code:
********************************************************************************
inspect 110412 
af_Retail_Item_Sold
********************************************************************************
  INSTANCE_TUNABLES={'value': <snippets._TunableAffordanceFilter object at 0x000007FFE5A81E90>}
  guid64=110412
  reloadable=True
  resource_key='00000000!00000000'0001af4c.7df2169c'
  snippet_type=affordance_filter
  tuning_manager=InstanceManager_snippet
  value=_TunableAffordanceFilterWrapper._filter


These are the primary variables names and their values that exist in the tuning for this snippet. With experience you will learn where you want to go from here, but for this example we'll just "know" already that we want to look at the value. The value of value is _TunableAffordanceFilterWrapper._filter which is just the string representation of an instanced Python object. At this point I used to have to edit my script and reload and type the command again to look into that object, rinsing and repeating until I got to what I was really interested in, which again is the include_affordances.

With this version of the inspector script though, you can just include a dot separated list of variable names and if that variable is found and can be inspected the script will do so. So let's look into that variable named value with the command inspect 110412 value. This outputs the variables contained within that instance of the _TunableAffordanceFilterWrapper Python object, and we get this:

Code:
********************************************************************************
inspect 110412 value
af_Retail_Item_Sold
********************************************************************************
  _factory_name=_filter
  _name=_TunableAffordanceFilter
  _tuned_values=ImmutableSlots({'default_inclusion': ImmutableSlots({'include_affordances': (<class 'sims4.tuning.instances.retail_RestockItem'>, <class 'sims4.tuning.instances.si_Retail_ClearSign'>, <class 'sims4.tuning.instances.retail_RestockItem_Instant'>), 'include_lists': (), 'exclude_affordances': (), 'exclude_lists': (), 'include_all_by_default': False})})
  factory=<function _TunableAffordanceFilter._filter at 0x000007FFE583C5F0>


We can see that there is a factory function for creating a new tunable affordance filter, but using factories can be a real PITA. We want to just edit this sucker on the fly, so let's look at the _tuned_values with inspect 110412 value._tuned_values:

Code:
********************************************************************************
inspect 110412 value._tuned_values
af_Retail_Item_Sold
********************************************************************************
  _cls_base_hash=1461938790333055641
  _cls_keys=('default_inclusion',)
  _hash=-1367916536951088537
  clone_with_overrides=<bound method ImmutableSlots.clone_with_overrides of ImmutableSlots({'default_inclusion': ImmutableSlots({'include_affordances': (<class 'sims4.tuning.instances.retail_RestockItem'>, <class 'sims4.tuning.instances.si_Retail_ClearSign'>, <class 'sims4.tuning.instances.retail_RestockItem_Instant'>), 'include_lists': (), 'exclude_affordances': (), 'exclude_lists': (), 'include_all_by_default': False})})>
  default_inclusion=ImmutableSlots({'include_affordances': (<class 'sims4.tuning.instances.retail_RestockItem'>, <class 'sims4.tuning.instances.si_Retail_ClearSign'>, <class 'sims4.tuning.instances.retail_RestockItem_Instant'>), 'include_lists': (), 'exclude_affordances': (), 'exclude_lists': (), 'include_all_by_default': False})
  get=<bound method ImmutableSlots.get of ImmutableSlots({'default_inclusion': ImmutableSlots({'include_affordances': (<class 'sims4.tuning.instances.retail_RestockItem'>, <class 'sims4.tuning.instances.si_Retail_ClearSign'>, <class 'sims4.tuning.instances.retail_RestockItem_Instant'>), 'include_lists': (), 'exclude_affordances': (), 'exclude_lists': (), 'include_all_by_default': False})})>
  items=<bound method ImmutableSlots.items of ImmutableSlots({'default_inclusion': ImmutableSlots({'include_affordances': (<class 'sims4.tuning.instances.retail_RestockItem'>, <class 'sims4.tuning.instances.si_Retail_ClearSign'>, <class 'sims4.tuning.instances.retail_RestockItem_Instant'>), 'include_lists': (), 'exclude_affordances': (), 'exclude_lists': (), 'include_all_by_default': False})})>
  pop=<bound method ImmutableSlots.pop of ImmutableSlots({'default_inclusion': ImmutableSlots({'include_affordances': (<class 'sims4.tuning.instances.retail_RestockItem'>, <class 'sims4.tuning.instances.si_Retail_ClearSign'>, <class 'sims4.tuning.instances.retail_RestockItem_Instant'>), 'include_lists': (), 'exclude_affordances': (), 'exclude_lists': (), 'include_all_by_default': False})})>
  update=<bound method ImmutableSlots.update of ImmutableSlots({'default_inclusion': ImmutableSlots({'include_affordances': (<class 'sims4.tuning.instances.retail_RestockItem'>, <class 'sims4.tuning.instances.si_Retail_ClearSign'>, <class 'sims4.tuning.instances.retail_RestockItem_Instant'>), 'include_lists': (), 'exclude_affordances': (), 'exclude_lists': (), 'include_all_by_default': False})})>
  values=<bound method ImmutableSlots.values of ImmutableSlots({'default_inclusion': ImmutableSlots({'include_affordances': (<class 'sims4.tuning.instances.retail_RestockItem'>, <class 'sims4.tuning.instances.si_Retail_ClearSign'>, <class 'sims4.tuning.instances.retail_RestockItem_Instant'>), 'include_lists': (), 'exclude_affordances': (), 'exclude_lists': (), 'include_all_by_default': False})})>


And here we see the value of outputing this to a file. In the cheat output window it's a mess to look at and only good for doing a quicky look. Seeing this output, you'll want to use the inspect.log 110412 value._tuned_values to get all that into a nice easily read text file. Turn off word wrap to look at the text file and two things jump out at me right away. First, this is an "immutable" object, which means we can't change it - it's protected from doing so. The second is that, as an ImmutableSlots object it has a clone_with_overrides method that allows us to copy everything except what we want to change (the include_affordances) into a new object. So we can't change the original, but we can copy it and override just the include_affordances. Nice!

It's starting to feel like we're getting close (in one sense we are, but there's a long way to go also!) We can see that include_affordances in there. It's inside the default_inclusion tuning. Great, so let's look at that with inspect.log 110412 value._tuned_values.default_inclusion. You may notice (hopefully) at this point that the names of the variables are following the pattern of the actual tuning. We're now looking at the <V n="default_include" t="exclude_all"> part of the af_Retail_Item_Sold tuning:

Code:
********************************************************************************
inspect.log 110412 value._tuned_values.default_inclusion
af_Retail_Item_Sold
********************************************************************************
  _cls_base_hash=-2411155093337494248
  _cls_keys=('exclude_affordances', 'exclude_lists', 'include_affordances', 'include_all_by_default', 'include_lists')
  _hash=-4024199276879265345
  clone_with_overrides=<bound method ImmutableSlots.clone_with_overrides of ImmutableSlots({'include_affordances': (<class 'sims4.tuning.instances.retail_RestockItem'>, <class 'sims4.tuning.instances.si_Retail_ClearSign'>, <class 'sims4.tuning.instances.retail_RestockItem_Instant'>), 'include_lists': (), 'exclude_affordances': (), 'exclude_lists': (), 'include_all_by_default': False})>
  exclude_affordances=()
  exclude_lists=()
  get=<bound method ImmutableSlots.get of ImmutableSlots({'include_affordances': (<class 'sims4.tuning.instances.retail_RestockItem'>, <class 'sims4.tuning.instances.si_Retail_ClearSign'>, <class 'sims4.tuning.instances.retail_RestockItem_Instant'>), 'include_lists': (), 'exclude_affordances': (), 'exclude_lists': (), 'include_all_by_default': False})>
  include_affordances=(<class 'sims4.tuning.instances.retail_RestockItem'>, <class 'sims4.tuning.instances.si_Retail_ClearSign'>, <class 'sims4.tuning.instances.retail_RestockItem_Instant'>)
  include_all_by_default=False
  include_lists=()
  items=<bound method ImmutableSlots.items of ImmutableSlots({'include_affordances': (<class 'sims4.tuning.instances.retail_RestockItem'>, <class 'sims4.tuning.instances.si_Retail_ClearSign'>, <class 'sims4.tuning.instances.retail_RestockItem_Instant'>), 'include_lists': (), 'exclude_affordances': (), 'exclude_lists': (), 'include_all_by_default': False})>
  pop=<bound method ImmutableSlots.pop of ImmutableSlots({'include_affordances': (<class 'sims4.tuning.instances.retail_RestockItem'>, <class 'sims4.tuning.instances.si_Retail_ClearSign'>, <class 'sims4.tuning.instances.retail_RestockItem_Instant'>), 'include_lists': (), 'exclude_affordances': (), 'exclude_lists': (), 'include_all_by_default': False})>
  update=<bound method ImmutableSlots.update of ImmutableSlots({'include_affordances': (<class 'sims4.tuning.instances.retail_RestockItem'>, <class 'sims4.tuning.instances.si_Retail_ClearSign'>, <class 'sims4.tuning.instances.retail_RestockItem_Instant'>), 'include_lists': (), 'exclude_affordances': (), 'exclude_lists': (), 'include_all_by_default': False})>
  values=<bound method ImmutableSlots.values of ImmutableSlots({'include_affordances': (<class 'sims4.tuning.instances.retail_RestockItem'>, <class 'sims4.tuning.instances.si_Retail_ClearSign'>, <class 'sims4.tuning.instances.retail_RestockItem_Instant'>), 'include_lists': (), 'exclude_affordances': (), 'exclude_lists': (), 'include_all_by_default': False})>


Oooh, getting closer! We can see that exclude_affordances is an empty tuple, as is exclude_lists, then there's a get method and after that is include_affordances which is a tuple that has actual values in it. The instances of affordance tunings! That's what we want to add to!

So, can we just expand the include_affordances the way we do with other tuples in the game? Sure, but since default_inclusion is another one of those immutable objects, we can't actually CHANGE the value of the include_affordances to our new tuple. Our normal method of doing something like...

Code:
    our_new_affordance_list = []
    for tuning in some_list:
        our_new_affordance_list.append(tuning)
    default_inclusion = default_inclusion + tuple(our_new_affordance_list)


But that just won't work. For one thing it's just psuedo-code, but again we can't actually CHANGE default_inclusion because it's immutable. But since this is also an ImmutableSlots class, it has a handy-dandy clone_with_overrides method. So let's get those tunings we need and add those to a new copy of default_inclusion and use that clone_with_overrides to create a new default_inclusion tuning.

We'll do this in the inspector's "play area". This is where we get down to trying something, finding out what happened, editing the code, reloading and trying again until we get it right. Let's edit the inspector's play area as follows.

Code:
        af_Retail_Item_Sold = get_tuning(110412)
        _tuned_values = af_Retail_Item_Sold.value._tuned_values
        default_inclusion = _tuned_values.default_inclusion
        include_affordances = default_inclusion.include_affordances
        output('{}'.format(include_affordances))


With these changes, we can reload the script with the reload inspector command, and then run the inspect.custom command to see that we got our include_affordances tuple just fine. This is starting to look doable now! Let's try changing it by adding a few affordances. Edit the play area again...

Code:
        af_Retail_Item_Sold = get_tuning(110412)
        _tuned_values = af_Retail_Item_Sold.value._tuned_values
        default_inclusion = _tuned_values.default_inclusion
        include_affordances = default_inclusion.include_affordances
        affordance_to_add = get_tuning(40871)
        include_affordances = include_affordances + (affordance_to_add, )
        output('{}'.format(include_affordances))


*** Remember to issue a reload inspector command everytime we change the script before issuing the inspect.custom command again. Otherwise you'll just be running the old script!

The above changes just add a single affordance (since Inge didn't give us the actual affordances desired, I'm just tossing a route fail into the list for testing) to the include_affordances, and sure enough it works.

For the real final mod we'd of course want to get a list of affordances in a more efficient manner (see the original
Adding Interactions to Sim/Object Super Affordance List via Python thread) when the game loads and add them in the ordinary fashion. We're just playing in our play area here.

So, can we update the actual tuning? Let's try. Our first stab at it (replace the play area as follows).

Code:
        af_Retail_Item_Sold = get_tuning(110412)
        _tuned_values = af_Retail_Item_Sold.value._tuned_values
        default_inclusion = _tuned_values.default_inclusion
        include_affordances = default_inclusion.include_affordances
        affordance_to_add = get_tuning(40871)
        include_affordances = include_affordances + (affordance_to_add, )
        default_inclusion.include_affordances = include_affordances
        output('{}'.format(default_inclusion.include_affordances))


We try adding the include affordances to the default_inclusion object directly, but OUCH it throws an exception that the ImmutableSlots object does not support item assignment. Told you so! Let's try using that clone_with_overrides method instead. To use that, we just use that method of the original we're wanting to clone and add and argument list containing the new assignments we want in that object, like so:

Code:
        af_Retail_Item_Sold = get_tuning(110412)
        _tuned_values = af_Retail_Item_Sold.value._tuned_values
        default_inclusion = _tuned_values.default_inclusion
        include_affordances = default_inclusion.include_affordances
        affordance_to_add = get_tuning(40871)
        new_include_affordances = include_affordances + (affordance_to_add, )
        new_default_inclusion = default_inclusion.clone_with_overrides(include_affordances=new_include_affordances)
        output('{}'.format(new_default_inclusion.include_affordances))


OK, that worked! We now have a new_default_inclusion object which contains our updated affordances in the include_affordances. It's still not in the original snippet tuning yet though, and we have another immutable on the path to that. Extending to the next step upward in the chain in our play area....

Code:
        af_Retail_Item_Sold = get_tuning(110412)
        _tuned_values = af_Retail_Item_Sold.value._tuned_values
        default_inclusion = _tuned_values.default_inclusion
        include_affordances = default_inclusion.include_affordances
        affordance_to_add = get_tuning(40871)
        new_include_affordances = include_affordances + (affordance_to_add, )
        new_default_inclusion = default_inclusion.clone_with_overrides(include_affordances=new_include_affordances)
        new_tuned_values = _tuned_values.clone_with_overrides(default_inclusion=new_default_inclusion)
        output('ORIGINAL: {}'.format(_tuned_values))
        output('NEW: {}'.format(new_tuned_values))


And the results look great, we have a new_tuned_values which is identical to the original but includes our added route fail affordance. So now let's try replacing the _tuned_values in the actual tuning with our new tuned values.

Code:
        af_Retail_Item_Sold = get_tuning(110412)
_tuned_values = af_Retail_Item_Sold.value._tuned_values
default_inclusion = _tuned_values.default_inclusion
include_affordances = default_inclusion.include_affordances
affordance_to_add = get_tuning(40871)
new_include_affordances = include_affordances + (affordance_to_add, )
new_default_inclusion = default_inclusion.clone_with_overrides(include_affordances=new_include_affordances)
new_tuned_values = _tuned_values.clone_with_overrides(default_inclusion=new_default_inclusion)
output('ORIGINAL: {}'.format(_tuned_values))
output('NEW: {}'.format(new_tuned_values))
af_Retail_Item_Sold.value._tuned_values = new_tuned_values


Hmm, that seemed to run, but did it work? Let's try going back to our inspect.log 110412 value._tuned_values command to look at the actual in-game tuning for the af_Retail_Item_Sold and see. By going back to that command, we're looking at a live copy of the tuning and not possible a lingering local variable in our play area. It's the "real thing".

And the results show that the route fail affordance has been added to the snippit successfully. Awesome! We now have the knowledge on how to add an affordance to the include_affordances of a snippet, and if you actually did follow along and "get it" along the way, you should have the basic knowledge needed to investigate how to update just about any tuning in the game.

That's the end of this example, the only thing that remains to put this in a real mod is to get the proper affordance(s) we want to add and put the whole shebang into a script that injects into the load_data_into_class_instances function of the instance manager. You should already know how to do that if you've read the Adding Interactions to Sim/Object Super Affordance List via Python thread.

I hope this helps folks out, and again, if you've followed to the end and pretty much understand please feel free to shout out your questions. But if you completely don't get it, just walk away quietly and no one will think any less of you.
Attached files:
File Type: zip  inspector.zip (1.3 KB, 148 downloads) - View custom content
Description: inspector.py
Advertisement
One horse disagreer of the Apocalypse
#2 Old 2nd Apr 2016 at 10:24 PM
Thank you!!

"You can do refraction by raymarching through the depth buffer" (c. Reddeyfish 2017)
Deceased
Original Poster
#3 Old 2nd Apr 2016 at 10:27 PM
You're quite welcome, Inge, and sorry it took me so long to get around to this. I wanted to spend some time and try not to leave anything out (but probably did anyway), so it kept getting delayed.
Test Subject
#4 Old 3rd Oct 2017 at 7:29 AM
you ded
One horse disagreer of the Apocalypse
#5 Old 3rd Oct 2017 at 8:37 AM
From the point of view of The Sims game, I think he must be.

"You can do refraction by raymarching through the depth buffer" (c. Reddeyfish 2017)
Deceased
Original Poster
#6 Old 13th Nov 2017 at 1:08 AM
Quote: Originally posted by Inge Jones
From the point of view of The Sims game, I think he must be.

Nah, was just in a coma (not in real life, from the point of view... yeah)
One horse disagreer of the Apocalypse
#7 Old 13th Nov 2017 at 8:39 AM
Good grief! *I* nearly died from a heart attack with your sudden resurrection :P

"You can do refraction by raymarching through the depth buffer" (c. Reddeyfish 2017)
Deceased
Original Poster
#8 Old 13th Nov 2017 at 10:52 AM
I reinstalled the game about a week ago, and since I always had more fun and frustration modding the game than actually playing it figured I'd drop back by here, maybe update a few mods, finished the one I started two years ago.. lol
One horse disagreer of the Apocalypse
#9 Old 13th Nov 2017 at 12:01 PM
I haven't played or modded in must be getting on for a year now. Writing my own unity game/simulation now. I don't expect it to be any good to play but I am learning all sorts of things that are fascinating.

"You can do refraction by raymarching through the depth buffer" (c. Reddeyfish 2017)
Deceased
Original Poster
#10 Old 13th Nov 2017 at 5:59 PM
Quote: Originally posted by Inge Jones
I haven't played or modded in must be getting on for a year now. Writing my own unity game/simulation now. I don't expect it to be any good to play but I am learning all sorts of things that are fascinating.

Interesting and cool - hope that you have fun with it, but who knows maybe I'll see it on Steam early access one day and I can send some money your way!
Test Subject
#11 Old 3rd Mar 2018 at 4:01 AM
Thanks for the great tool and information about the ImmutableSlots along with its workaround. It troubled me for days.
Back to top