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!
Quick Reply
Search this Thread
Lab Assistant
Original Poster
#1 Old 28th Mar 2015 at 5:24 PM
Default Creating a scripting mod that modifies Maxis scripts
Hi all.

As you may or may not know, I am the author of the "No More Culling" mod, which has until the latest patch been successful at disabling the game's culling feature with a modified StoryProgressionService XML. The problem is that, since the new patch, this method of stopping culling no longer works, as EA has added new functionality that calls the culling action upon loading a save.

After hacking through the game's Python files, I've managed to find a way to disable this new function, by directly editing the Max Population action in the Simulation/Story Progression/actions.pyo file. I can get my edited version into my game by replacing the original script and making a new Simulation.zip, which does work in my game, but obviously this is not how I want to deliver the mod to the general public.

I'm not really knowledgeable at all when it comes to Python, and I was wondering how I could inject the modified StoryProgressionActionMaxPopulation class (specifically def process_action) into the game via the means of a standard mod, without directly replacing the game's core files. Ideally, I want the game to access my modified code instead of its own, but all the user would have to do is place a file into their Mods folder.

Can anyone help?
Attached files:
File Type: zip  actions.pyo.zip (4.5 KB, 17 downloads) - View custom content
Advertisement
Mad Poster
#2 Old 28th Mar 2015 at 9:58 PM
Hey, I'll take a stab at this. I am guessing you've added your logic to the StoryProgressionActionMaxPopulation class. It looks like you changed, "process_action". You can use an "injector.py" definition that you can find in use in many mods. I don't know if he originally wrote it or it came from somewhere else, but scumbumbo is who I consider an expert on the usage of this stuff (more so than myself for sure!)

It normally looks like this:
Code:
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


Then, in your own mod, you'd have a python file that would look something like this:
Code:
from injector import inject_to
import story_progression.actions

@inject_to(story_progression.actions.StoryProgressionActionMaxPopulation , 'process_action')
def inject_process_action(original, self, story_progression_flags)
   original(self, story_progression_flags)


The problem is, you're actually wanting to override that method and take some functionality out of it. You could do that by just adding all of the function exactly like it is in the "process_action" into your own function in your mod script and then leave-out the part you don't want. But, if EA changes that function in the future, you're going to have to incorporate any changes they make as well. You may have better luck if you inject to the "_get_culling_score" function and change it to always return 0. I believe if you do that, then the code you're leaving out of your version of actions.py could be left in.

Maybe something like this:
Code:
from injector import inject_to
import story_progression.actions

@inject_to(story_progression.actions.StoryProgressionActionMaxPopulation , '_get_culling_score')
def inject_get_culling_score(original, self, sim_info)
   original(self, sim_info) # This would normally be used to get the culling score for process_action to use
   return 0 # Instead, we're going to always return 0


You could add some additional logic in there of your own so you're not ALWAYS returning zero. But I think that's the basic idea. Also, there may be concerns about the fact we're injecting around a "private" definition. I'm not smart enough on Python to be able to answer that one for you.
Mad Poster
#3 Old 28th Mar 2015 at 10:10 PM
Looking at it a bit closer, that may not be exactly what you want as we'll still be appending zeroes to the self._household_scores so when they do the process_action it will still cull I think. Maybe what I've described there will help you, though, just as far as how to inject into that action. It actually looks like if you just make it so all SimInfo have can_be_culled to false, then they won't be culled ever. So, maybe that's alternate place to look rather than the StoryProgressionActionMaxPopulation action. This is going a lot deeper than you may want to go and it may just be easier to do like you were looking at originally and just override the "process_action". This code really does cause anything to happen now in "process_action", though, so I almost question why you'd make that override do anything other than just "pass" or "return".

Code:
from injector import inject_to
import story_progression.actions
import services

@inject_to(story_progression.actions.StoryProgressionActionMaxPopulation , 'process_action')
def inject_process_action(original, self, story_progression_flags)
        self._household_scores.clear()
        for sim_info in services.sim_info_manager().values():
            while sim_info.can_be_culled():
                culling_score = self._get_culling_score(sim_info)
                self._household_scores[sim_info.household.id].append(culling_score)


There's several other ways this could go. You could override can_be_culled so it is always false. There is a trait that can be set on sims so they are immune to culling. You could change household type so they are not "is_persistent_npc".

Anyway. Hopefully I haven't harmed more than helped! Good luck!
Lab Assistant
Original Poster
#4 Old 29th Mar 2015 at 6:08 AM Last edited by Dark Gaia : 29th Mar 2015 at 6:55 AM.
Hi. This is exactly what I'm looking for. I think I may be able to cobble something together based on what you've shown here - what I was really looking for was a way to inject the code. There's always a chance that EA will modify the function in the future, but as long as I know how to inject my own code, I don't mind having to update it as it's only deleting one argument from the definition.

The other methods you've posted would probably work as a more permanent fix, which I'll likely look into as well, but at the moment a simple workaround would probably make a lot of people very happy.

Thanks so much for your help

EDIT: I was getting an invalid syntax error with the code above, but it's fixed now
Deceased
#5 Old 29th Mar 2015 at 6:53 AM
Another option, since you're looking to completely eliminate the process, would be to just override one of the object methods completely. Something like this should do the trick, I'd target the process_action and just skip it entirely so it doesn't even waste time trying to build a cull list....
Code:
#import story_progression.actions

def my_storyprogression_process_action(self, story_progression_flags):
    return

story_progression.actions.StoryProgressionActionMaxPopulation.process_action = my_storyprogression_process_action
As soon as the above is loaded (untested, assuming I didn't make any typos which is unlikely as I was up until 5am last night working on a crashed hard drive) it should just completely replace the method - no need to mess around with injection.

The injection method (I nabbed it from scripthoge btw) is primarily for if you want to make sure the original code runs and then do something else afterwards.
Lab Assistant
Original Poster
#6 Old 29th Mar 2015 at 6:54 AM Last edited by Dark Gaia : 29th Mar 2015 at 7:05 AM.
Thanks Scumbumbo! This looks like the best option. I'll test out all of these different methods and see which one I prefer.

EDIT: Your method seems to be the fastest, performance wise. Since the game is no longer calculating culling scores, it actually runs a little faster.
Deceased
#7 Old 29th Mar 2015 at 7:16 AM
Glad it seems to be working well for you!
Mad Poster
#8 Old 29th Mar 2015 at 7:10 PM
That's a nifty thing to know! I can probably use that as well! Thanks!
Back to top