[Solved] Howto: mod settings for XML mod?

Started by Napoleonite, February 14, 2020, 10:07:47 AM

Previous topic - Next topic

Napoleonite

I have a mod that is 100% XML. I just want to add some mod settings so users can change some XML values like the cost of an item for example or the research amount required. I checked https://rimworldwiki.com/wiki/Modding_Tutorials/ModSettings#DoSettingsWindowContent and it shows how to load&save variable from&to disk, but how do I actually make it change the XML values for the game so that it actually does something?
So how do I use the variables from the mod settings to change the 'XML values'?


LWM

Unfortunately, there's no way to do it with only XML.

If you're okay with using C# and setting up the basic mod settings (either HugsLib or just basic RimWorld), you can directly access the Defs and directly change the values to the setting.

So for example, if I want to let the user change the label for a bed, I could do this to change it:

DefsDatabase<ThingDef>.GetNamed("Bed").label=newLabel; // get newLabel from settings

It's not super easy, but it's also somewhat straightforward if you are okay with C# and you are familiar with how everything works.  It's a bit slower if you are not familiar.


Napoleonite

#3
Thanks. I created a C# project (that was easy). But where do I put that line of code? I can't find the proper event/function to put it in. I tried this:

public class EGB_Settings : Mod
{
        // <other code here>

        public override void WriteSettings()
        {
            base.WriteSettings();
            DefDatabase<ThingDef>.GetNamed("Bed").label = EGB_Settings.Settings.BedLabel;
        }
}

//  [StaticConstructorOnStartup] // This one is executed too early (before the defs are loaded) so also not possible.


But this works (obviously) ONLY when changing the mod's settings and then creating a new colony immediately after. How do I ensure that this line is always called (loading game, new game, or when changing the setting inside an existing game)? What to override?

Also, apparently my settings aren't being saved for my TextEntryLabeled
        public override void DoSettingsWindowContents(Rect inRect)
        {
            Listing_Standard listingStandard = new Listing_Standard();
            listingStandard.Begin(inRect);

            listingStandard.CheckboxLabeled("aa".Translate(), ref Settings.ExampleBool, "exampleBoolToolTip"); // saved

            listingStandard.Label("exampleFloatExplanation");
            Settings.ExampleFloat = listingStandard.Slider(Settings.ExampleFloat, 100f, 300f); // saved

            Settings.BedLabel = listingStandard.TextEntryLabeled("Bed label", Settings.BedLabel); // NOT SAVED? WHY?

            listingStandard.End();
            base.DoSettingsWindowContents(inRect);
        }


And how do I check if that XML-path even exists? If another mod changed that path somehow, it will currently crash horribly. Or is a simple "DefDatabase<ThingDef>.GetNamed("Bed") != null" enough?

Napoleonite

#4
Here's my full code:
   public static EGB_SettingsData Settings;

        /// <summary>
        /// A mandatory constructor.
        /// </summary>
        public EGB_Settings(ModContentPack content) : base(content)
        {
            //Log.Warning("Endgame Buildings" + ": Settings loaded.");
            Settings = GetSettings<EGB_SettingsData>();
            //EGB_Main.ApplyXMLChanges();
        }

        public override void DoSettingsWindowContents(Rect inRect)
        {
            Listing_Standard listingStandard = new Listing_Standard();
            listingStandard.Begin(inRect);

            listingStandard.CheckboxLabeled("aa".Translate(), ref Settings.ExampleBool, "exampleBoolToolTip");

            listingStandard.Label("exampleFloatExplanation");
            Settings.ExampleFloat = listingStandard.Slider(Settings.ExampleFloat, 100f, 300f);

            Settings.BedLabel = listingStandard.TextEntryLabeled("Bed label", Settings.BedLabel);

            //IntRange test = new IntRange(0, 100);
            //Settings.BedCost = listingStandard.IntRange(ref Settings.BedCost, ref test, 0f, 100f);

            // WTF does string buffer mean?
            //listingStandard.TextFieldNumericLabeled<int>("Bed Cost", ref Settings.BedCost, minValue: 1f, maxValue: 2000f);

            listingStandard.End();
            base.DoSettingsWindowContents(inRect);
        }

        /// <summary>
        /// This is the name as how it'll appear ain the game's settings menu.
        /// </summary>
        public override string SettingsCategory()
        {
            return "EGB_SettingsCategory".Translate();
        }

        public override void WriteSettings()
        {
            base.WriteSettings();
            Log.Warning("Endgame Buildings" + ": Settings saved.");
            EGB_Main.ApplyXMLChanges();
        }
    }

    public class EGB_SettingsData : ModSettings
    {
        public bool ExampleBool;
        public float ExampleFloat = 200f;
        public string BedLabel = "Testing Beds";
        //public int BedCost = 10;

        /// <summary>
        /// The part that writes our settings to file. Note that saving is by ref.
        /// </summary>
        public override void ExposeData()
        {
            Scribe_Values.Look(ref ExampleBool, "exampleBool");
            Scribe_Values.Look(ref ExampleFloat, "exampleFloat", 200f);
            if (DefDatabase<ThingDef>.GetNamed("Bed") != null)
            {
                Scribe_Values.Look(ref DefDatabase<ThingDef>.GetNamed("Bed").label, "bed label", "Testing beds");
            }
            //Scribe_Values.Look(ref DefDatabase<ThingDef>.GetNamed("Bed").costList[0].count);
            base.ExposeData();
        }
    }

LWM

Oh.  Hmm.  Defs haven't been loaded by the time settings are loaded on game startup.

I would do this:

  [StaticConstructorOnStartup] // this makes the static constructor get called AFTER defs are loaded
  public class OnDefsLoaded {
    static OnDefsLoaded() {
      // apply settings to defs now that defs are loaded:
      ApplySettingsToDefs();
    }
    public static ApplySettingsToDefs() {  // a public static method that can be called from anywhere
      DefDatabase<ThingDef>.GetNamed("Bed").label = EGB_Settings.Settings.BedLabel;
    }
  }


And you probably want to add this to your DoWindowContents, too:

    Settings.BedLabel = listingStandard.TextEntryLabeled("Bed label", Settings.BedLabel);
    OnDefsLoaded.ApplySettingsToDefs();
    // WTF does string buffer mean?
        // It means that the text fields need a string-buffer to hold what the user is typing, but they don't
        // have one of their own, so you have to supply them one?
    //listingStandard.TextFieldNumericLabeled<int>("Bed Cost", ref Settings.BedCost, minValue: 1f, maxValue: 2000f);


Honestly, most of the time I want to look for how to do something, I either look at
* https://github.com/lilwhitemouse/RimWorld-LWM.MinorChanges/blob/master/Source/MinorChanges.cs
   Uses settings to determine whether to activate HarmonyTranspilers
or
* https://github.com/lilwhitemouse/RimWorld-LWM.DeepStorage/blob/master/DeepStorage/ModSettings.cs
   Does everything under the sun and some things that should never see the light of any star.
If nothing else, I have the syntax correct somewhere in there  ::)  ;D

It's been a long day, so no promises, but I think that'll work.

Napoleonite

[StaticConstructorOnStartup]
Yes! That was what I was looking for. Everything works now :D. Thank you very much, I've been stuck on this for ages.

Ah, I guess you can use the StringBuffer to do various checks, but if you don't need it, just assign it some random unique local variable I guess.

Thanks for those links. I've also been looking at other mods to get it working but still couldn't figure it out, many also use 3rd party libraries which I prefer not to use.

LWM

I also find Mehni's mods fairly understandable and CommonSense has been useful to me in the past.

And there is often good advice here!

Good luck :)