|
Post by marquillotuca89 on Jun 6, 2021 22:49:31 GMT -5
Hi guys! I've been following a series of scripting modding tutorials, and I found this one that shows how to override code from the game. I've overriden some functions with no arguments succesfully, however, I wanted to do a little experiment with some of the functions from the newest interior designer career. I want to clarify that even though, I have experience with tuning mods, my coding skills are a joke haha. This is what I've got. import careers.decorator_career_gig
import sims4.commands
import services
import sims4
import build_buy,sims4.log
from traits.trait_type import TraitType
from protocolbuffers import DistributorOps_pb2, Sims_pb2
protocol_constants = DistributorOps_pb2.Operation
logger = sims4.log.Logger('Gig', default_owner='mbilello')
logger = sims4.log.Logger('CareerCommand', default_owner='rrodgers')
def custom_get_gig_score_for_preferences(self, preferences_list, likes_value=1, dislikes_value=1, has_no_objects_value=-5000):
tags_modified = build_buy.get_gig_tag_changes(self._customer_lot_id)
likes_changes = 0
dislikes_changes = 0
traits_changes = 0
for preference_trait in preferences_list:
if preference_trait is None:
continue
tags = preference_trait.preference_item.get_any_tags()
if tags is not None:
for tag in tags:
tag_index = [i for i, v in enumerate(tags_modified) if v[0] == tag]
if len(tag_index) > 0:
if preference_trait.trait_type == TraitType.LIKE:
likes_changes += tags_modified[tag_index[0]][1]
elif preference_trait.trait_type == TraitType.DISLIKE:
dislikes_changes += tags_modified[tag_index[0]][1]
else:
traits_changes += tags_modified[tag_index[0]][1]
likes_multiplier = 1
dislikes_multiplier = 1
traits_multiplier = 1
for score in self._decorator_gig_tuning.preference_scoring_weight:
if score.trait_type == TraitType.LIKE:
likes_multiplier = likes_changes * score.weight
elif score.trait_type == TraitType.DISLIKE:
dislikes_multiplier = dislikes_changes * score.weight
else:
traits_multiplier = traits_changes * score.weight
final_value = likes_multiplier * likes_value
final_value += dislikes_multiplier * dislikes_value
if traits_multiplier > 1:
final_value += traits_multiplier
final_value = -600000
return final_value
careers.decorator_career_gig.DecoratorGig.get_gig_score_for_preferences = custom_get_gig_score_for_preferences So basically, I'm making it so the "final_value" the function return will always be -60000, something that should always ruin the gig score, since this function gets called in fucntion "calculate_gig_result" as a multiplier. However, it doesn't seem to take effect on my game. As far as I can understand, to override a piece of code from an existing function of the game, I need to: 1) Import the file into my current py 2) Write something like: directory.file.class.function=customfunction And this where I don't even know if my override is working or not. Is everything right with what I'm doing? Do I need to import the whole class because other functions use this function in the original file or is it enough with what I've done. @sims4.commands.Command('score_custom', command_type=sims4.commands.CommandType.Live)
def get_gig(_connection=None):
score = careers.decorator_career_gig.DecoratorGig.gig_score
sims4.commands.output('Your current gig score is'.format(score), _connection) I can't even make a cheat to get the gig_score working hahaha Any help will be appreciated. Thank so so so much!
|
|
|
Post by o19 on Jun 7, 2021 0:06:46 GMT -5
Hi
I don't like the idea to replace EA methods at all. Python supports "inject" to hook into any method and to run it. See modthesims.info/showthread.php?p=4751246#post4751246 for details or use a library to simply use @inject... without the need to write the inject code yourself.
Without logging it may be hard to know whether EA is calling this method at all. Getting started without any logging may be a pain. Luckily there are libraries available, also native logging may be used.
Hopefully you'll inject into the method instead of replacing it as other mods may already inject into it. And these mods do not list the functions they inject into as injecting works for multiple mods. Replacing one method may break many mods. Even though the blog looks nice I wonder how new June is to TS4 modding. There may be aspects she never heard of (like many CC tutorials ignoring cut numbers, edges etc.)
Your 1st method's code may be reduced to "return -600000" so it is very unlikely that it fails. If you want to use the cheat console make sure to import the required libs:
import sims4 from sims4.commands import CommandType
@sims4.commands.Command('foo', command_type=sims4.commands.CommandType.Live) def cmd_o19_foo(_connection=None): out = sims4.commands.CheatOutput(_connection) try: import careers score = careers.decorator_career_gig.DecoratorGig.gig_score out(f"Your current gig score is '{score}'.") except Exception as e: out(f"Oops! Got exception '{e}'") # in case decorator_career_gig is undefined
To print something it is highly recommended to use the current Python style which is faster and more readable.
I didn't test the code above and PyCharm complains about decorator_career_gig not being defined. Hopefully the code works for you and print at least an exception if it fails.
|
|
|
Post by marquillotuca89 on Jun 7, 2021 11:15:21 GMT -5
Thank so much o19! Your suggestion of logging it's honestly great, I'm so used to tuning files that I forgot I'm actually writing on Python, where I can tell my mod to generate a log file. :P Now, injecting in tuning mods means that you will add a new piece of code whenever a method is called (injecting an affordance means that whenever the "affordances" method is called, the newer piece of code you added will also be executed) but I'm not sure if I want to do that, specially after reading what Deaderpool wrote. To be clear what I want to do is this 1) In EA's files, there is a "calculate_gig_result" method that references the "get_gig_score_for_preferences" method. 2) Since I want the "calculate_gig_result" to have a diferent result, I'm working on the "get_gig_score_for_preferences" method. 3) What I want to do is that whenever the "get_gig_score_for_preferences" gets called, my newer "_custom_get_gig_score_for_preferences" is called instead. Which means that I need to overwrite the function completely. I don't want to add another method, I want to overwrite the original one (the mod I want to create is for personal use) Now, from what I understood, Deaderpool only gives examples on how to "hook" your piece of code to an already existing method.Meaning that if a function called "EA's_Code" runs, what you do inside the injector will be "added_up" as a part of the already existing method. What I want to do is that whenever the "EA's_Code" function gets called, my newer form of calculation gets called instead. However, if I do something like this: import careers.decorator_career_gig @inject_to(careers.decorator_career_gig.DecoratorGig(ActiveGig), 'get_gig_score_for_preferences') def custom_get_gig_score_for_preferences(original, self, preferences_list, likes_value, dislikes_value): original(self, preferences_list, likes_value=1, dislikes_value=1) final_value = -50000. Since the "final_value" already exist on the original function, and my part of the code runs after that value is defined, when the original function returns the value, -50000 will returned instead of that the function calculated originally.. Am I right? Do I need to add the return too? Or injection in script modding means that we will overwrite the whole method too but giving the possibility for other mods to use it? Edit: The cheat doesn't seem to be working. :( Edit 2: I made the cheat work. However, it gives me this weird result: I need to format it, right?
|
|
|
Post by o19 on Jun 7, 2021 15:12:47 GMT -5
3) What I want to do is that whenever the "get_gig_score_for_preferences" gets called, my newer "_custom_get_gig_score_for_preferences" is called instead. Which means that I need to overwrite the function completely. I don't want to add another method, I want to overwrite the original one (the mod I want to create is for personal use)
Even in this case if a mod is injecting into this function you will cause issues for the mod. Actually you usually get 'original' as a reference to the original EA function and may call it. There is no need to call it, your new method can simply return a value.
From the page linked above:
@inject_to(zone.Zone, 'on_loading_screen_animation_finished') def inject_zone_loading(original, self): original(self) # Comment this line as you are not interested to run the EA function at all. While testing you may even run it and log the return value to a file for educational purposes. You may even run other code before and/or set values to manipulate the outcome of the original version. # Do any custom coding you want here, which will happen after the EA-scripts for zone loading finish. # Add return -600000 and you're done.
The error says that score is neither a string nor a int/float value but a function. With {} it's nicely formatted. With
import inspect x = inspect.getmembers(score) print(f"{x}")
you may get some ideas what you can do with 'score'. Better log this to a file as the console will likely display only 1000 characters.
|
|
|
Post by o19 on Jun 7, 2021 15:20:07 GMT -5
With s4cl you would simply add: @commoninjectionutils.inject_safely_into(ModInfo.get_identity(), DecoratorGig, DecoratorGig.get_gig_score_for_preferences.__name__) def do_get_gig_score_for_preference(original, self, *args, **kwargs): # return original(self, *args, **kwargs) return -600000
This would be your mod and as long as EA does not change their method names it'll work. Depending on your injection code it may be different but for me it's no joy to look into different inject and log options every time.
|
|
|
Post by marquillotuca89 on Jun 7, 2021 18:00:11 GMT -5
Oh, this changes everything. I didn't know you could skip the "original" to avoid running that function. THANK YOU! The issue I had before was that the whole script wasn't running for some reason. Now it's working.
You are an angel, I was trying to use Pythong logging function but it wasn't working. I tried to log to the console but it was a mess haha.
import inspect x = inspect.getmembers(score) print(f"{x}")
Uuuh, well, according to the same decorator_gig file, the return of that function should be an int. So it's really weird that it's returning a function.
I will also try that too. Thank you so much for real.
|
|
|
Post by marquillotuca89 on Jun 8, 2021 0:00:24 GMT -5
I'm sorry for the double posting, but I really want to thank you o19!
import sims4.commands
import careers
from functools import wraps
import build_buy,sims4.log
import traceback
import os.path
import inspect
from traits.trait_type import TraitType
@inject_to(careers.decorator_career_gig.DecoratorGig, 'get_gig_score_for_preferences')
def custom_get_gig_score_for_preferences(original, self, preferences_list, likes_value=1, dislikes_value=1, argument=1):
#original(self, preferences_list, likes_value, dislikes_value)
if argument == 1:
writelog("Argument 1")
return 0
else:
tags_modified = build_buy.get_gig_tag_changes(self._customer_lot_id)
likes_changes = 0
dislikes_changes = 0
traits_changes = 0
for preference_trait in preferences_list:
if preference_trait is None:
continue
tags = preference_trait.preference_item.get_any_tags()
if tags is not None:
for tag in tags:
tag_index = [i for i, v in enumerate(tags_modified) if v[0] == tag]
if len(tag_index) > 0:
if preference_trait.trait_type == TraitType.LIKE:
likes_changes += tags_modified[tag_index[0]][1]
elif preference_trait.trait_type == TraitType.DISLIKE:
dislikes_changes += tags_modified[tag_index[0]][1]
else:
traits_changes += tags_modified[tag_index[0]][1]
likes_multiplier = 1
dislikes_multiplier = 1
traits_multiplier = 1
for score in self._decorator_gig_tuning.preference_scoring_weight:
if score.trait_type == TraitType.LIKE:
likes_multiplier = likes_changes * score.weight
elif score.trait_type == TraitType.DISLIKE:
dislikes_multiplier = dislikes_changes * score.weight
else:
traits_multiplier = traits_changes * score.weight
final_value = likes_multiplier * likes_value
final_value += dislikes_multiplier * dislikes_value
if traits_multiplier > 1:
final_value += traits_multiplier
writelog("Argument 2")
writelog(f" '{final_value}'.")
return final_value
This is part of the code I ended up using. As you can see, I even added a new parameter (argument), which I'm using to siwtch between cases and forms of calculation.
Thanks to the logging files I could confirm this function was the one in charge to help the others and I also learnt that doing "-5000" was provoking other functions to receive an unexpected value. I needed to set the return to 0 to produce a failure outcome.
Thank you so much for your patience!
All I need to do now is to create a command that allow me to change that argument parameter from an interaction (the same interaction used by EA to start the whole evaluation process), using a "do_command" method as a part of a tested outcome.
Is it possible to create a command too? (and don't tell how to do it, I just need to know if that's possible, I have plenty of examples on how to do that and I don't want to bother you anymore)
|
|
|
Post by o19 on Jun 8, 2021 6:18:01 GMT -5
You're welcome. I hope it helps also others to create new mods for TS4.
From my point of view you want to execute the original function to get the final_value and change it afterwards with a custom multiplier (0 .. 3). Cloning (or "stealing") EA code may be a bad idea. With a very short function your code is easier to manage.
@ Is it possible to create a command too? One may either use the cheat console (better do this first) or append a (debug) menu entry to the existing pie menu or other UI elements. The game will never append a new argument to the function calls. Better use a global or class variable which you can access within the code above and from the command.
|
|
|
Post by marquillotuca89 on Jun 8, 2021 8:33:39 GMT -5
I think at this point is fair to explain what I want to do:
Context
- The gig score calculation process is executed when the reveal ends (after the Sim goes "tadaaa"). In that moment, a tuning interaction executes a command as a part of its basic extras method, and starts the whole evalutation process right there.
What I want to do
1) Since I have way more experience creating tuning mods, I want to override that interaction (hopping that nobody else do it with a mod in the future haha ) to also include two tested outcomes: a)The "normal" outcome where the normal process is executed. b) The failure outcome, that will happen because in a specific gig you didn't add specific objects (let's say you forgot to add a shower in a bathroom renovation, well, you're screwed). This whole testing process is more comfortable in tuning files than in script.
2) The failure outcome then, will execute a "do_command" method, receiving an argument as a number.
3) In the script of the mod, I want to create a command that receives that number and then, switch the case to the failure one. In my overriden function, I want to set up a condition where, if the number is 1 (for example) then EA's Code run, if the number is other, then the failure outcome runs.
Now that you told me that what I wanted to do could fail, this is what I think my code will end up being:
import sims4.commands
import careers
from functools import wraps
import sims4.log
import traceback
import os.path
import inspect
switcher = 2
@sims4.commands.Command('careers.change_gig_outcome', command_type=sims4.commands.CommandType.Live)
def change_gig_outcome(input,_connection=None):
out = sims4.commands.CheatOutput(_connection)
global switcher
if input == 1:
switcher = 1
else:
switcher
@inject_to(careers.decorator_career_gig.DecoratorGig, 'get_gig_score_for_preferences')
def custom_get_gig_score_for_preferences(original, self, preferences_list, likes_value=1, dislikes_value=1):
global switcher
if switcher == 1:
# original(self, preferences_list, likes_value, dislikes_value)
writelog("Argument 1")
return 0
else:
original(self, preferences_list, likes_value, dislikes_value)
writelog("Argument 2")
Meaning that the global variable would have a default value, and the command should, for example, change the global variable to 1 to force a failure outcome. At the end, I will use the original function if the command doesn't change a thing.
EDIT: How can I get final_value from the injector?
|
|
|
Post by o19 on Jun 8, 2021 15:36:43 GMT -5
Depending on what you want to do it may be easier to do it in a script as described here: sims4studio.com/thread/22471/create-custom-tuning-testsAnyhow with a tuning files background better test there. final_value = original(... should assign the return value of original() to final_value. To call your script from the tuning you could follow Scumbumbo's guide: modthesims.info/showthread.php?p=4710110I don't like the tuning files, compared to Python code it's just a mess So I make a big way around them whenever possible.
|
|
|
Post by marquillotuca89 on Jun 8, 2021 18:37:25 GMT -5
OMG. I can't thank you enough o19! With you, I've lost my "fear" of scriptmodding. This is what my final main looks like now:
import sims4.commands import careers import injector import sims4.log import os.path
switcher = 1
#Utilities def writelog(str, logname=None): if logname is None: logname = "Interior_Decorator_Overhaul.log" filename = os.path.join(os.path.dirname(os.path.realpath(__file__)), logname) with open(filename, "a") as fp: fp.write('{}\n'.format(str))
#Actual code #Command to change the outcome of the gig to a failure one @sims4.commands.Command('careers.change_gig_outcome', command_type=sims4.commands.CommandType.Live) def change_gig_outcome(input: int = 1, _connection=None): global switcher if input == 0: switcher = 0 writelog("Failure outcome choosen") else: writelog("Normal outcome choosen") switcher = 1
#Injector to override the function with my code @injector.inject_to(careers.decorator_career_gig.DecoratorGig, 'get_gig_score_for_preferences') def custom_get_gig_score_for_preferences(original, self, preferences_list, likes_value=1, dislikes_value=1): global switcher final_value = original(self, preferences_list, likes_value, dislikes_value) final_value = final_value * switcher writelog(f"Switcher current value is '{switcher}'.") writelog(f"Final_Value current value is '{final_value}'.") return final_value
It is so small, yet so powerful. I didn't even need to "override" the original code. Now I can properly say I did what I wanted to do. Now I just need S4S to get updated (I don't like using S4PE or XML Extractor Files) to put everything together! You're an angel.
And about the tests... I think in this case I'm gonna stick to tunning hahaha.
|
|
|
Post by o19 on Jun 9, 2021 6:07:21 GMT -5
You could change your inject code to something like this:
@injector.inject_to(careers.decorator_career_gig.DecoratorGig, 'get_gig_score_for_preferences') def custom_get_gig_score_for_preferences(original, self, *args, **kwargs): final_value = 0 try: if switcher != 0: final_value = switcher * original(self, *args, **kwargs) writelog(f"custom_get_gig_score_for_preferences() switcher='{switcher}' final_value='{final_value}'.") return final_value except Exception as e: writelog(f"custom_get_gig_score_for_preferences() Exception '{e}'.") return original(self, *args, **kwargs)
'switcher' is global by default, unless you want to modify it here it's afaik no need to define it as global. By using '*args, **kwargs' your code will always work, no matter which function parameters EA changes. Only if the method name or type is modified injection will fail and your mod will stop working. This is very unlikely.
Also in writelog() a try/expect block would do no harm, in case the directory doesn't exist, is not writable or the file is readonly the log message should be silently discarded.
|
|
|
Post by marquillotuca89 on Jun 9, 2021 13:50:11 GMT -5
Uuuuh, thanks for the example!
I learned how to properly place the *args and *kwargs parametes. Since Deader explained that it was only placed for optional parameters, I was using it wrong. Thank you!
And I already had a log problem. Whenver I used June's devmode, the log function worked, however, when I compiled my mod, the story was different. Using what you're saying helped me to solve it!
|
|