Backstories, traits and mods - HELP!

Started by LegendaryMinuteman, April 16, 2019, 05:03:40 PM

Previous topic - Next topic

LegendaryMinuteman

Hello RimModders,

First off, bona fides, I'd say I'm a 70-80% proficient modder for RimWorld.  As well as making new animals and weapons on the simpler xml end, I've wrestled with C# and have compiled my first assembly for my mod Planetside Politics available on Steam Workshop.  I've also worked on modifying Castor's existing mod Religions of RimWorld.

So now I have always present political and religious priorities for my pawns greatly complicating pawn social interaction as desired.  The mods are great.

Now, "The Problem."

I would like to limit planet indigenous tribes to certain religions and limited political ideologies like theocracy and monarchy and away from "post-industrial" ideology and likewise for religion.  I don't want space colonists starting with rim planet tribes' religious beliefs and I want pirates to have despotic politics and sinister religious practices, etc.

Knowing that backstories are somewhat hidden for kickstarter reasons, etc etc, is there a convenient way that I can tie traits to backstories?

The solution as I see it is to create a new C# class as a list to add to PawnKindDefs with something like <ForcedTraits> which would require them on character creation but I'm not entirely sure how to inject that into the character creation process so that the data actually gets used.

At the moment I'm attempting to reverse engineer from Rainbeau Flambe's Editable Backstories and Names 1.0 mod but his mod has a much more significant scope so extracting what is of use to me from his wall of code is a bit difficult and as I'm not attempting to reinvent the wheel, just make sure a certain hubcap goes with each one, it seems like I'm making it more difficult for myself than I really am.

So long story short, I have new traits.  I'd like tribals and pirates and outlanders and all the default classes to start with specific traits and never start with others.  Any suggestions?

Thanks for any help in advance!
Cheers,
LM

Mehni

spawnCategories and their associated counterpart backstoryCategories.

Alternatively, faction tech level.

LegendaryMinuteman

#2
Thanks for the help but I need a little guidance in what to do with the information.  I know I want to tie the traits to specific backstoryCategories and techLevels but I'm not sure how to do this.

Say I add a list to the TraitDefs such as

public List<TraitDef> conflictingtechLevel = new List<TraitDef>();

Now where do I go from here?  I'm trying to write something that detects the pawn's faction's tech level and then refuses to add the trait if it comes back positive.  This is where I'm at.

Or alternatively, would it be possible to introduce a variable so that a forced trait is selected from a pool of possible forced traits.  For example instead of
<forcedTraits>
<li>Trait01</li>
<li>Trait02</li>
<li>Trait03</li>
</forcedTraits>

something that produces
x = 1 to 3
<forcedTraits>
<li>Trait(x)</li>
<forcedTraits>

I'm not familiar enough with C# to know the syntax or if such a thing is even possible.

Mehni

You can't add fields or variables to existing classes; C# simply forbids this. Since the backstories aren't exposed to XML either, you can forget about any XML-based approaches too.

That still leaves multiple approaches to your issue. You mention "forcedTraits" and that is certainly possible. Every BackStory in the BackStoryDatabase has a public List<TraitEntry> forcedTraits; entry. You can add yours to that. Do keep in mind those are *forced* entries. The game doesn't have a choice in adding traits from that list; it will add all of them.

Another option is a Harmony Patch on the method(s) that add a trait to the Pawn. There are two or three of those:
- RimWorld.TraitSet.GainTrait
- Verse.PawnGenerator.GenerateTraits
- RimWorld.ScenPart_ForcedTrait.ModifyPawnPostGenerate

at a quick glance, a patch on the first should suffice.

The final option that I see is inspired by the Forced_Trait Scenario Part. Add your own Scenario Part that inherits from RimWorld.ScenPart_PawnModifier.ModifyPawnPostGenerate, and add the appropriate traits there.

Note; while you can't add a list to the TraitDefs, there's nothing stopping you from having your list/dictionary/custom data structure keeping track of what Traits are appropriate and what aren't. If you think a field like that would solve all your problems, I would recommend a design pattern that RimWorld supports very well: Adding a ModExtension to the relevant Defs. See the RimWorld wiki for more info on that.

LegendaryMinuteman

#4
Now we're getting somewhere.  Thanks again for your expertise it is really helping.

My thinking is this:
Create a new list on the ReligiousTraits [techLevel] just as exists for FactionDefs.
Then looking at the original code for TraitSet GainTrait we see this concept:

public void GainTrait(Trait trait)
   {
      if (HasTrait(trait.def))
      {
         Log.Warning(pawn + " already has trait " + trait.def);
         return;
      }
      allTraits.Add(trait);
   }
So if I can get this to something like:
            [HarmonyPatch(typeof(TraitSet), "GainTrait")]
            private static class Patch_GainTrait
            {
                private static bool TraitTech(TraitDef __instance, FactionDef other, ref bool __result)
                {
                    if (__instance is TraitDef_ReligionTrait)
                          {
                                   ******************
From there I compare __instance.techLevel and other.techLevel and check for overlap, else return.

Is that essentially the idea?

Mehni

Quote from: LegendaryMinuteman on April 17, 2019, 03:58:53 PM

public void GainTrait(Trait trait)
{
if (HasTrait(trait.def))
{
Log.Warning(pawn + " already has trait " + trait.def);
return;
}
allTraits.Add(trait);
//other code of the original method omitted
}
//So if I can get this to something like:
            [HarmonyPatch(typeof(TraitSet), "GainTrait")]
            private static class Patch_GainTrait
            {
                private static bool TraitTech(TraitDef __instance, FactionDef other, ref bool __result)
                {
                    if (__instance is TraitDef_ReligionTrait)
                          {

From there I compare __instance.techLevel and other.techLevel and check for overlap, else return.

Is that essentially the idea?

In the right direction, yeah. The approach/design could work, but the C# you posted won't.

private static bool TraitTech(TraitDef __instance, FactionDef other, ref bool __result)
There are four errors in this line:
- Since you're using annotation patching, you need to specify whether or not this is a postfix or a prefix. You can do that with the methodname or with more annotations.
- The __instance will never be of type TraitDef. The instance is TraitSet, as that is the class you are patching.
- Since the original method doesn't receive a FactionDef as argument, neither can your patch.
- The original method returns void. A void returns nothing. That means it does not return a __result. There is no __result to return.

You'll also need a way to get the Pawn from the TraitSet, but that's a protected variable. Harmony 1.2+ offers an easy way of getting non-public variables: prefix the variable name with three underscores and accept them as argument. So ProperlyNamedMethod(TraitSet __instance, Pawn ___pawn, Trait trait) { ... }

__instance is TraitDef_ReligionTrait -- are you going to subclass TraitDef just for the sake of adding a techLevel field? In that case, I strongly suggest using a DefModExtension instead. They are a hundred times easier; more compatible, less code, less maintenance.

On a more global level: Is your current intent a destructive prefix (i.e. don't run the original method) if the trait is of a too high tech level? While that certainly works, it has two downsides/side-effects you might not be aware of:
- It prevents other prefixes and the original method from running. This is generally considered a bad thing, for the sake of compatibility. Sometimes that's acceptable, other times not.
- It will sneakily reduce the commonality of your trait. If a tribal gets assigned a high-tech trait and you say "no, don't add this", it's very likely they will end up with a different trait that's not one of yours. That's okay.

LegendaryMinuteman

#6
You're a lifesaver.  Thank you again.  I have some homework at this point.  But to answer a few of your questions and I suppose to ask a few of my own.
Quote__instance is TraitDef_ReligionTrait
Wait, didn't you say __instance will never be of type TraitDef?

Quoteare you going to subclass TraitDef just for the sake of adding a techLevel field? In that case, I strongly suggest using a DefModExtension instead. They are a hundred times easier; more compatible, less code, less maintenance.
Okay, I finally have a handle on what you're saying here.

To be clear, there's two separate things happening here with the same goal.  I'm trying to implement the techLevel for my politics traits and I'm also creating a patch to do the same for Castor's Religions of RimWorld mod.  That being said, there are a few more things happening within the subclass but they're still just added lists.  They're categories for scales of opinion buff/debuffs for ThoughtWorker interaction between pawns with the same or opposing traits.

Since I'm walking in Castor's footprints I made a subclass simply because he made subclasses but his mod is significantly more complicated than what I'm trying to do with my mod.  So I see what you mean about using a modextension over creating a subclass.

So final case should be Castor's mod, the religion patch applying the techLevels, the politics mod which will contain the prefix checking for techLevels, and then it just becomes a load order situation.

QuoteOn a more global level: Is your current intent a destructive prefix (i.e. don't run the original method) if the trait is of a too high tech level?
That's the plan but mostly because that's the best solution of which I can conceive.  I assumed this wouldn't be an issue because as I interpreted it, this method runs on a case by case basis, so shutting down the method for that particular trait, assuming I don't want it in the first place, wouldn't be an issue.  But I could be way off base.

Quote- It prevents other prefixes and the original method from running. This is generally considered a bad thing, for the sake of compatibility. Sometimes that's acceptable, other times not.
I guess I had assumed that since it was a check for a property that was only extant within my mods it wouldn't be a problem as it would otherwise never trigger.  I take it I was mistaken in this regard.

Quote- It will sneakily reduce the commonality of your trait. If a tribal gets assigned a high-tech trait and you say "no, don't add this", it's very likely they will end up with a different trait that's not one of yours. That's okay.
I had considered that this would be the case and had a few assumptions I hoped would ease the issue.  For one, they are heavily weighted to ensure that at least one is selected and then once one is selected they're mutually exclusive.  Secondly, I use five trait slots using the mods together and would when all is said and done recommend others do the same.  With five rolls and the heavy weighting I would hope the likelihood of a successful roll would be rather high.  At the end of the day, if it is beyond me to implement a solution, it is a small problem I'd just have to accept.

One last question that's been bothering me.  My original solution to this was to make the mod require Rainbeau's Editable Backstory mod which bypassed the internal backstory system with his own xml based backstory solution.  I figured the easiest thing to do would take advantage of the original backstory method which has a public list disallowedTraits.  I'd do a patch operation to add the relevant traits in a <disallowedTraits> list to the backstory xmls and that would be that but when I tried to add the traits this way and tested it with the backstories it didn't seem to do anything at all and they still showed up.  Even when I modified them directly to ensure the disallowedTraits list was present.  I also noticed that no backstory actually seems to use this list.  Do you have any idea why this solution didn't work?  As far as I can tell this should have been the easiest solution to the entire matter.

Anyway, thanks again for the help and I'll get cracking and see what I can do.

LWM

Quote from: LegendaryMinuteman on April 18, 2019, 04:20:22 PM
You're a lifesaver.  Thank you again.  I have some homework at this point.  But to answer a few of your questions and I suppose to ask a few of my own.
Quote__instance is TraitDef_ReligionTrait
Wait, didn't you say __instance will never be of type TraitDef?

__instance represents the object you are patching via Harmony.

So if I'm patching Thing's DeSpawn, then __instance is a Thing (and I might have Prefix(Thing __instance..., and I could have it do something like Log.Error("Oh, no, thing "+__instance.ToString()+" is despawning!");, and __instance would be *that particular thing*)

Quote
QuoteOn a more global level: Is your current intent a destructive prefix (i.e. don't run the original method) if the trait is of a too high tech level?
That's the plan but mostly because that's the best solution of which I can conceive.  I assumed this wouldn't be an issue because as I interpreted it, this method runs on a case by case basis, so shutting down the method for that particular trait, assuming I don't want it in the first place, wouldn't be an issue.  But I could be way off base.

If you're going to go that route, you could always use the harmony "priority annotation":


[HarmonyPatch etc]
class etc
  [HarmonyPriority(Priority.Last)]
  static bool Prefix(etc) {
    if (!__instance is MyTraitEtc) return true; // execute normal if it's not my trait in question
    // do stuff, maybe set __result
    return false; // don't execute original  (make sure this is okay)
  }


That way you're less likely to break anything, because other prefixes should run before yours and hopefully you're not breaking anything in the vanilla (but never any guarantees?)

--LWM

LegendaryMinuteman

Looks like it's resolved!

[HarmonyPatch(typeof(TraitSet), "GainTrait")]
        private static class Patch_GainTrait
        {
            private static bool Prefix(TraitSet __instance, Pawn ___pawn, Trait trait)
            {

                if (trait.def is TraitDef_PoliticsTrait)
                {
                    if (trait.def.defName == "PoliticsSocialist" || trait.def.defName == "PoliticsCommunist" || trait.def.defName == "PoliticsFascist")
                    {
                        if (___pawn.Faction.def.techLevel == RimWorld.TechLevel.Neolithic)
                        {

                            return false;

                        }
                        else
                        {
                            return true;
                        }
                    }
                    else
                    {
                        return true;
                    }
                }
                else
                {
                    return true;
                }


            }


        }

    }

}

But going by defName is obviously hamfisted but this method is more than adequate for my purposes.  I'll think about trying some better solutions but for now I can consider it at least operational.

Thanks for all the help.

LWM

Actually, for that particular application, I might use a priority Priority.First, because you wouldn't want anyone else to touch the GainTrait code if you're stopping it like that (which looks pretty reasonable?  The code should try and find another trait since these never get added)

--LWM

PS - "hamfisted?"  A unique identifier is *exactly* what defName is for!