New A17 Mod Features

Started by ZorbaTHut, May 23, 2017, 10:46:46 AM

Previous topic - Next topic

ZorbaTHut

With A17 looming over the horizon, it's definitely time to write about some of the new mod features. This is a living document and will be edited as people give suggestions; if you think something should be reworded, lemme know and I'll merge it in.

With no further ado, and in no particular order:

Mod Class

Mods can now inherit from the class Verse.Mod. All non-abstract classes inheriting from Verse.Mod are constructed immediately after all mods and defs are loaded, and before virtually anything else happens; it's a good place to put startup functionality. These are intended as singletons, and are constructed as such.

You can get mod classes via LoadedModManager:

LoadedModManager.GetMod<AmazingMod>();

We may use this for future global callbacks.

Mod Settings

There is now a "mod settings" button at the top-left of the options dialog, which includes a dropdown that lists all known mod setting categories. Each Verse.Mod class can show up here, by overriding the SettingsCategory and DoSettingsWindowContents functions in Mod itself. SettingsCategory should return a string, usually your mod's name; DoSettingsWindowContents should draw all the relevant mod settings using the standard Rimworld UI system.

If your mod is extremely complicated and you want multiple settings categories in the dropdown, you can have multiple classes inherit from Verse.Mod.

As these are located in the Options dialog, they're intended for global cross-game settings, not per-game configuration.

Mod Settings Serialization

Along with this, we've added tools for saving mod settings side-by-side with Rimworld's settings.

Inherit from Verse.ModSettings; add as many members as you want, then override ExposeData() in the same manner as a normal ExposeData function. Mod settings are stored in the Mod class; call GetSettings<ModSettingsName>() to get them (and load from disk, if needed). So, for example:

LoadedModManager.GetMod<AmazingMod>().GetSettings<AmazingModSettings>();

Updating the settings in-place won't immediately write them; you either need to call .Write() on the ModSettings class, or .WriteSettings() on the Mod.

LoadedModManager.GetMod<AmazingMod>().GetSettings<AmazingModSettings>().Write();
LoadedModManager.GetMod<AmazingMod>().WriteSettings();

Note that each Mod class can support at most exactly one Settings class. If you need multiple settings files, you'll need multiple Mod files.

Finally, note that the Mod Settings feature and Mod Settings Serialization feature, while certainly meant to be used together, aren't connected in any official way. If you want to make multiple Mod Settings windowed that refer to a single serialized file, go for it; if you want to completely disregard one half of this feature and use the other half, go for it.

If you don't want global settings, but want per-map or per-game settings, then maybe you want . . .

GameComponent/WorldComponent/MapComponent

The MapComponent system has been somewhat revamped. The game will now automatically add instances of all non-abstract classes derived from Verse.GameComponent/Verse.WorldComponent/Verse.MapComponent to the game, world, and all maps, both on creation and on loading. In addition, if a game is loaded that includes unknown components, it will discard the component (with an error message, but without causing future problems).

Mods should be able to use this to store per-game/per-world/per-map settings, without fear of causing major game breakages if a mod is added or removed midway through a game.

In order to make use of this, just inherit from one of the above types. No further registration is needed.

Enum->Def Conversions

We've converted a lot of internal "enum" types into Def objects. This is intended to make it easier for modders to add new categories of thing. In most cases, we expect these will take heavy modding, including use of Harmony, to make use of, but at least now it's possible.

A list of new defs, possibly missing some:

FleshTypeDef, PrisonerInteractionModeDef, BillRepeatModeDef, BillStoreModeDef, ReservationLayerDef, ImpactSoundTypeDef, DamageArmorTypeDef, TrainableIntelligenceDef, StuffAppearanceDef.

If there's more internal enums that you want turned into defs, please post in the mod support request thread.

DefModExtensions

The core Def class now includes a list, modExtensions, which can be filled with classes inherited from Verse.DefModExtensions. The contents of this list are completely ignored by the game; it's provided specifically for mods to attach extra data to arbitrary defs. Also see the new functions Def.GetModExtension<T>() and Def.HasModExtension<T>()

Def patching

Finally, and probably most importantly, Rimworld now includes a way for mods to patch defs without having to replace the entire definition. This means - as an example - that a mod which decreases weapon damage and a mod that increases weapon crafting cost can easily co-exist! We strongly recommend making use of this in any situation where you might have previously overwritten an entire def.
 
This is a larger feature than the others, and thankfully for me, Zhentar has already written an excellent guide. Go read that :)

----

Questions? Comments? This is a first-draft of documentation. You are encouraged to do a better job of writing stuff than I did, and then add it to the wiki!

kaptain_kavern


cuproPanda

I feel like the PatchOperations could use a couple more features.

Success SilentFail: doesn't spit out an error if the operation failed. This would be handy for checking if a mod has added a def, and if so replacing values - otherwise no big deal, just fail silently.

PatchOperation_Sequence: some way for the PatchOperation_Test to break/continue the list of operations. Like in the above example, test if there is a ThingDef with DefName "JumboDoor", and if one isn't found, don't do the following operations in the list.

What I am envisioning is something like this:

<Operation Class="PatchOperationSequence">
<success>SilentFail</success>
<Operations>
<li Class="PatchOperationTest">
<xpath>//ThingDef[defName="JumboDoor"]</xpath>
                                <success>Continue</success>
</li>
<li Class="PatchOperationReplace">
<xpath>//ThingDef[defName="JumboDoor"]/costList</xpath>
<value>
<costList>
<ReinforcedSteel>50</ReinforcedSteel>
</costList>
</value>
</li>
</Operations>
</Operation>


In this hypothetical, the mod which has the patch file adds ReinforcedSteel. If there is another mod (by the same person maybe) that adds a JumboDoor, this mod makes the cost use ReinforcedSteel instead of the original cost. If not, no error is thrown and the player doesn't have to wonder why their error log is flooded with errors upon activating a mod.

That said, the new patching system is absolutely wonderful and it's a great feature! :)
cuproPanda's Mods: Survivalist's Additions, Additional Joy Objects, Cupro's Drinks, Quarry, Cupro's Stones, Zen Garden, Cupro's Alloys, Preset Filtered Zones, & more!

Spdskatr

Yes! Spread this to the whole world.

Also, LetterDefs are probably the best add to the def collection.
My mods

If 666 is evil, does that make 25.8069758011 the root of all evil?

AngleWyrm

#4
Quote from: cuproPanda on May 23, 2017, 04:33:42 PM
...some way for the PatchOperation_Test to break/continue the list of operations. Like in the above example, test if there is a ThingDef with DefName "JumboDoor", and if one isn't found, don't do the following operations in the list.

xpaths are like a query that can return sets to work on, so that the implementation becomes a little different from an if/then statement:

Select the set of things 'JumboDoor' and make an alteration to that set. All the things called 'JumboDoor' get your modifications, and everything that's not JumboDoor doesn't, which includes an empty set.
My 5-point rating system: Yay, Kay, Meh, Erm, Bleh

Fluffy (l2032)

@cuproPanda, unless I'm very much mistaken you can already get this behaviour. The crux is in the fact that it's not the individual ops in the sequence that cause the error message, it's the whole sequence that fails. You can set success=always on the sequence and it won't squeal about the error, but the 'error' in the test will still stop the second op from processing.

Fluffy (l2032)

that said, as AngleWyrm points out your example really doesn't need a sequence at all - you could just set the success=always flag on the op adding/altering the costList and it would report success whether is has done something or not.

On a side note, I'm not sure if you're using C#, but personally, I vastly prefer manipulating defs from C# code over patches whenever possible. Sure you'd need a compiler to tweak/update the code, but it gives so much more flexibility - and it's a lot easier to debug/maintain.

cuproPanda

Ah, see I was using success never, then inverting it in the sequence. Going off of the tutorial posted I was under the impression PatchOperation_Sequence was the way to go for this sort of thing; I figured it'd throw an NRE if I just assumed nodes were present. I see now :)
cuproPanda's Mods: Survivalist's Additions, Additional Joy Objects, Cupro's Drinks, Quarry, Cupro's Stones, Zen Garden, Cupro's Alloys, Preset Filtered Zones, & more!

Nandonalt

Hey ZorbaTHut! Thanks for all the useful information.
Do you have any practical examples of custom Mod Settings so I can take a closer look?

Thanks in advance.

ZorbaTHut

#9
Quote from: Fluffy (l2032) on May 24, 2017, 03:44:02 AM
@cuproPanda, unless I'm very much mistaken you can already get this behaviour. The crux is in the fact that it's not the individual ops in the sequence that cause the error message, it's the whole sequence that fails. You can set success=always on the sequence and it won't squeal about the error, but the 'error' in the test will still stop the second op from processing.

Yeah, this is pretty much the intention. Here's an example snippet that I was using in some early explanations:


<Operation Class="PatchOperationSequence">
  <success>Always</success>
  <operations>
    <li Class="PatchOperationTest">
      <xpath>//ThingDef[defName = "DiningChair"]/costList</xpath>
      <success>Invert</success>
    </li>
    <li Class="PatchOperationAdd">
      <xpath>//ThingDef[defName = "DiningChair"]</xpath>
      <value>
        <costList />
      </value>
    </li>
  </operations>
</Operation>
<Operation Class="PatchOperationAdd">
  <xpath>//ThingDef[defName = "DiningChair"]/costList</xpath>
  <value>
    <Cloth>5</Cloth>
  </value>
</Operation>


In this case, it adds a costList to DiningChair if one doesn't already exist, then adds cloth to it. Now all dining chairs cost 5 cloth.

I think this can be done with xpath as well, though.

Quote from: Nandonalt on May 24, 2017, 04:23:31 PM
Do you have any practical examples of custom Mod Settings so I can take a closer look?

I totally do not, I'm sorry :) I've asked some modders if anyone's making significant use of this yet. Worst-case, I can rig up an ultra-simple example if needed.

Edit: Fluffy has pointed me at FluffyBreakdowns for a simple example. Also, apparently, MedicalTab, "and one more" that they can't seem to remember. Hopefully that's enough to get you started!

Edit edit: link#1 link#2 thanks again, Fluffy!

jamaicancastle

I haven't heard anything about this, but I'm guessing the research tabs are a new feature?

The short version from what I can tell: there's a new def type, the ResearchTabDef. It has two attributes: defName and label. You set up your research tab and then add <tab>[your tab's defName]</tab> to one or more ResearchProjects. In-game, the research menu will have multiple tabs: all the normal research projects under Main, and the ones you designated under whatever label you choose.

This should help mods that add a bunch of new research projects, both in terms of being able to lay them out nicely without worrying about cluttering up the research screen or interfering with other mods, and in terms of focusing attention on them so they aren't overlooked, as they might be in the main screen.

(Sorry if this is old and/or blindingly obvious to everyone, but I happened to stumble across this today and realized I hadn't heard about it at all.)

BlackSmokeDMax

Quote from: jamaicancastle on May 25, 2017, 07:46:46 AM
I haven't heard anything about this, but I'm guessing the research tabs are a new feature?

The short version from what I can tell: there's a new def type, the ResearchTabDef. It has two attributes: defName and label. You set up your research tab and then add <tab>[your tab's defName]</tab> to one or more ResearchProjects. In-game, the research menu will have multiple tabs: all the normal research projects under Main, and the ones you designated under whatever label you choose.

This should help mods that add a bunch of new research projects, both in terms of being able to lay them out nicely without worrying about cluttering up the research screen or interfering with other mods, and in terms of focusing attention on them so they aren't overlooked, as they might be in the main screen.

(Sorry if this is old and/or blindingly obvious to everyone, but I happened to stumble across this today and realized I hadn't heard about it at all.)

I believe Ykara has implemented Research Tabs into EPOE, so if you want to take a look that is probably a good spot. (might be a test release so look for posts near end of thread if that is the case)

Zhentar

I have another example of ModSettings here: https://github.com/Zhentar/ZhentarTweaks/blob/3e107f7518f55305406f3bccf96f53a846a1a4d8/Source/LetterStackDetour.cs

Mine is totally better than Fluffy's because it includes both the Mod with the settings load and the ModSettings class in the same file :)

Spdskatr

#13
I haven't seen this anywhere on the forums, but is the ResearchMod class also new to this alpha? It can do some amazing stuff, and I don't see this class get implemented or documented in any of the core defs...

EDIT: No it isn't. It has changed though.
My mods

If 666 is evil, does that make 25.8069758011 the root of all evil?

Rikiki

Hi ZorbaTHut!
I already successfuly used a patch to change vanilla data. Nice feature! :)

I am now trying to change the texture of some meat the same way but... it appears that the patches are performed before the implied meat defs generation.
In term of code: in Verse.PlayDataLoader.DoPlayLoad, LoadedModManager.LoadAllActiveMods is called before DefGenerator.GenerateImpliedDefs_PreResolve.

Do you have a solution to this? ::)
Thanks!