[Tutorial] How to Make a RimWorld Mod, Step by Step

Started by jecrell, June 03, 2017, 05:00:43 AM

Previous topic - Next topic

jecrell

How to Make a RimWorld Mod
STEP BY STEP
by Jecrell

#XML #C# #Beginner #Intermediate #Advanced
#Hediff #Weapon #Ranged_Weapon #Projectile #ThingDef

"Modding RW for dummies." -KapTaiN_KaVerN

Table of Contents
I. Introduction
II. Required items
III. XML Stage
IV. Connecting XML and C#
V. C# Assembly Setup
VI. C# Coding
VII. Localization

Questions? Comments?
Let's mod RimWorld together.
Visit the Unofficial RimWorld Discord.
https://discord.gg/rimworld
...Psst. Still there? If you'd like to support
me and my works, do check out my Patreon.
Someday, I could work for RimWorld full time!

https://www.patreon.com/jecrell

jecrell

#1
Introduction



In this tutorial we're going to create a weapon that covers many different aspects of RimWorld's systems. This provides us with a clearer understanding of how RimWorld works for modding.

By following this tutorial, you will be able to do your own complex RimWorld modding in XML and C# in the same way that I can. I will give you all my tools and know-how.

This weapon will be known as The Plague Gun. When fired, if it hits its target, it will have a chance to make the target catch the plague.

To make this mod happen, we will need to create the ThingDefs in XML for the bullet and gun, and create a C# assembly that uses the Projectile class to define a new Impact event for our plague projectile.

This tutorial cannot fully teach XML and C#, and it can help you familiarize yourself a bit with them, but there are lots of free resources online to learn them.
XML -> https://www.w3schools.com/xml/
C# -> http://www.learncs.org/
...Psst. Still there? If you'd like to support
me and my works, do check out my Patreon.
Someday, I could work for RimWorld full time!

https://www.patreon.com/jecrell

jecrell

#2
Required Items







Notepad++ or
Atom or
Sublimetext
|Use any text editor that allows you to edit XML files and use "Find in Files" for referencing.
Visual Studio Community|Use this or any other C# compiler to turn scripts into .dll files that RimWorld can use.
dnSpy|This is for referencing the game's decompiled C# scripts.

Optional Goodies



Rimworld Mod Development Cookiecutter
This is a tool for developers to begin mods more quickly, eliminating the overhead of setting up a new project.
https://ludeon.com/forums/index.php?topic=39038.0
...Psst. Still there? If you'd like to support
me and my works, do check out my Patreon.
Someday, I could work for RimWorld full time!

https://www.patreon.com/jecrell

jecrell

#3
XML Stage



1) Create a mod folder.
RimWorld>Mods>PlagueGun
- Go to your RimWorld's base folder. For myself, that is D:\SteamLibrary\steamapps\common\RimWorld\. Others may find it in C:\Program Files (x86)\Steam\steamapps\common\RimWorld or a custom directory.
- Go into the Mods folder.
- Make a new folder with our mod's title: PlagueGun

2) Inside PlagueGun, make an About folder.
RimWorld>Mods>PlagueGun>About

3) Inside the About folder, make a new text file and rename it About.xml.
RimWorld>Mods>PlagueGun>About>About.xml
- You will need a good text editor to make a proper XML file -- Notepad++ or Atom. I always make a .TXT file first and change it to .XML. If you can't rename your file's type, make sure you have filetypes visible in your view settings.
- About.xml is the file that shows your mod in the mod list inside the RimWorld game. It is also used when creating a Workshop upload for Steam.
- At the top of an XML file, always include this for RimWorld.


<?xml version="1.0" encoding="utf-8"?>


- Then add the MetaData tags for the Workshop and in-game Mod list.


<ModMetaData>
<name>Test Mod - Plague Gun</name>
<author>YourName</author>
<targetVersion>0.17.0</targetVersion>
<description>V1.0
    This mod adds a plague gun, a weapon that has a chance to give your enemies the plague.
</description>
</ModMetaData>


- Save the file.

4) Add a Preview.png or Preview.jpeg to your About folder.
RimWorld>Mods>PlagueGun>About>Preview.png
- This lets users see what your mod looks like in the RimWorld mod list or on the Steam Workshop.
- I prefer the dimensions of 480x300 pixels, but the Steam Workshop will be able to work with almost any resolution you specify.
- Example:

5) Make a Defs folder in your Mod's directory.
RimWorld>Mods>PlagueGun>Defs
- RimWorld will read your XML files in any directory. You can name your directories however you like under the /Defs/ subfolder. Defs/StrangeNewAlienGuns/MyGuns.xml will work. For the purposes of this tutorial, however, we will use the RimWorld standard structure.


What are Defs?
RimWorld uses something called Defs like blueprints for in-game objects. Instead of using hidden C# code, RimWorld will look up an XML Def and copy it to the game world. This makes things easier for us, the modders. Everything from characters, animals, floors, damages, buildings, and even diseases in RimWorld use Defs. We're going to make Defs for our Plague Gun and Plague Bullet.




6) Make a new ThingDefs folder in your Defs folder.
RimWorld>Mods>PlagueGun>Defs>ThingDefs

7) Make a new text file in your ThingDefs folder, and change it to RangedWeapon_PlagueGun.xml.
RimWorld>Mods>PlagueGun>Defs>ThingDefs>RangedWeapon_PlagueGun.xml
- This file will contain the blueprints (ThingDefs or Thing Definitions) for our new gun and bullets.
- Next we will fill out our XML file by copying an existing revolver's ThingDef and a revolver bullet ThingDef.
- In RimWorld, it is often best to use the XML attribute ParentName="BaseBullet" when making a bullet, because it will copy XML code from a pre-existing BaseBullet ThingDef, which can save us time and key taps.


DO NOT USE THIS -- This is an incomplete preview.

<?xml version="1.0" encoding="utf-8"?>
<Defs>

<ThingDef ParentName="BaseBullet">
  <defName>TST_PlagueGun_Bullet</defName>
  <label>plague bullet</label>
  ...
</ThingDef>

<ThingDef ParentName="BaseHumanMakeableGun">
  <defName>TST_PlagueGun_Weapon</defName>
  <label>plague gun</label>
  ...
</ThingDef>

</Defs>


9a) First add our favorite line to the top.

<?xml version="1.0" encoding="utf-8"?>

Again, this line of code shows RimWorld that this is an XML file to extract data from.

9b) Add <Defs> opening and closing tags to the XML to hold our new code.


<?xml version="1.0" encoding="utf-8"?>
<Defs>


</Defs>


9c) Use your text editor. Use its "Find in Files" function to reference and copy Bullet_Revolver to your XML file.
For me, Find in Files is a function I use repeatedly. In Notepad++, if you press CTRL+SHIFT+F, you can go to the Find in Files screen. From there, you can enter a phrase to search for, and then you can enter the file path to search through. This makes it wildly easier to search for examples in RimWorld's Core. RimWorld holds copies of all of its weapons, items, buildings, etc. inside the Mods/Core directory.
- So, start by using Find in Files... and find this: defName>Bullet_Revolver
- When you find Bullet_Revolver, copy from its beginning <ThingDef> all the way until its closing </ThingDef> tag into your XML File.

10) Use your text editor. Use its "Find in Files" function to reference and copy Gun_Revolver to your XML file.
- Typically, Gun_Revolver is right below Bullet_Revolver in XML, so hopefully this will be easy to find and copy.
- Repeat copying ThingDef code to your new XML file.

11) Change the defName, labels, and other stats of Bullet_Revolver and Revolver in your XML file to make them unique.
- TIP Use prefixes to avoid conflicting with other mods. RimWorld doesn't distinguish between two mods that both use <defName>Tobacco</defName>, for instance. If two modders use different prefixes, however, e.g. <defName>VegetableGarden_Tobacco</defName> and <defName>ROM_Tobacco</defName>, no conflict will occur, and both mods can co-exist. This tutorial uses TST_ for its example prefix.
- Change the <defaultProjectile> tag inside the PlagueGun's Verbs to use the defName of your bullet (TST_Bullet_PlagueGun). Failing to do this will result in a no errors, but your gun will shoot regular bullets and not give the plague effect.

Once again, under the verbs. Please make sure <defaultProjectile> references the bullet.
e.g.
<defaultProjectile>TST_Bullet_PlagueGun</defaultProjectile>

[/b]
Example completed XML file:

<?xml version="1.0" encoding="utf-8"?>

<Defs>

  <ThingDef ParentName="BaseBullet">
    <defName>TST_Bullet_PlagueGun</defName>
    <label>plague bullet</label>
    <graphicData>
      <texPath>Things/Projectile/Bullet_Small</texPath>
      <graphicClass>Graphic_Single</graphicClass>
    </graphicData>
    <projectile>
      <flyOverhead>false</flyOverhead>
      <damageDef>Bullet</damageDef>
      <damageAmountBase>9</damageAmountBase>
      <speed>55</speed>
    </projectile>
  </ThingDef>

  <ThingDef ParentName="BaseHumanMakeableGun">
    <defName>TST_Gun_PlagueGun</defName>
    <label>plague gun</label>
    <description>A curious weapon notable for its horrible health effects.</description>
    <graphicData>
      <texPath>Things/Item/Equipment/WeaponRanged/Revolver</texPath>
      <graphicClass>Graphic_Single</graphicClass>
    </graphicData>
    <soundInteract>InteractRevolver</soundInteract>
    <statBases>
      <WorkToMake>15000</WorkToMake>
      <Mass>1.4</Mass>
      <AccuracyTouch>0.91</AccuracyTouch>
      <AccuracyShort>0.71</AccuracyShort>
      <AccuracyMedium>0.50</AccuracyMedium>
      <AccuracyLong>0.32</AccuracyLong>
      <RangedWeapon_Cooldown>1.26</RangedWeapon_Cooldown>
      <Weapon_Bulk>0.5</Weapon_Bulk>
    </statBases>
    <weaponTags>
      <li>SimpleGun</li>
    </weaponTags>
    <costList>
      <Steel>30</Steel>
      <Component>2</Component>
    </costList>
    <verbs>
      <li>
        <verbClass>Verb_Shoot</verbClass>
        <hasStandardCommand>true</hasStandardCommand>
        <defaultProjectile>TST_Bullet_PlagueGun</defaultProjectile>
        <warmupTime>0.3</warmupTime>
        <range>26</range>
        <soundCast>ShotRevolver</soundCast>
        <soundCastTail>GunTail_Light</soundCastTail>
        <muzzleFlashScale>9</muzzleFlashScale>
      </li>
    </verbs>
    <tools>
      <li>
        <label>grip</label>
        <capacities>
          <li>Blunt</li>
        </capacities>
        <power>8</power>
        <cooldownTime>1.6</cooldownTime>
      </li>
      <li>
        <label>barrel</label>
        <capacities>
          <li>Blunt</li>
          <li>Poke</li>
        </capacities>
        <power>8</power>
        <cooldownTime>1.6</cooldownTime>
      </li>
    </tools>
  </ThingDef>

</Defs>
...Psst. Still there? If you'd like to support
me and my works, do check out my Patreon.
Someday, I could work for RimWorld full time!

https://www.patreon.com/jecrell

jecrell

#4
Connecting XML and C#


For the next part of the tutorial, we are going to use C# code to create a custom ThingDef blueprint type, and we're going to create a custom class for our Thing to use in-game when it spawns. Before we can get to C# however, we need to "bridge" XML and C# by pointing the XML to use our C# code. First, decide your mod's namespace. This prevents RimWorld from being confused by other mods. This is the name RimWorld knows to use for your C# code. For the purposes of this tutorial, we're going to use Plague.

The following XML will not work in-game until we've written the C# code and created an assembly (.dll file) for our mod.

12) Change the line for the ThingDef to reference a custom ThingDef class.
<ThingDef Class="Plague.ThingDef_PlagueBullet" ParentName="BaseBullet">
  <defName>TST_Bullet_PlagueGun</defName>

13) Add three more lines to the thing def before the closing tag (</ThingDef>).
<AddHediffChance>0.05</AddHediffChance>
<HediffToAdd>Plague</HediffToAdd>
<thingClass>Plague.Projectile_PlagueBullet</thingClass>

- AddHediffChance and HediffToAdd will give us a percentage of success to add a Hediff of our choice in XML (in this case we're using Plague). We will code how these parameters are used in the C# Coding section.

------See Attached File for Complete XML (A17)--------

[attachment deleted by admin due to age]
...Psst. Still there? If you'd like to support
me and my works, do check out my Patreon.
Someday, I could work for RimWorld full time!

https://www.patreon.com/jecrell

jecrell

#5
Setting up the C# Assembly



15) Open your compiler of choice for C#.
-I use Visual Studio Community edition, a free Windows-based compiler for C# code.
-This part of the tutorial will assume you will use the same.

16) Make a new Visual C# Class Library .NET Framework project. Name it PlagueGun. Make its directory RimWorld>Mods>PlagueGun>Source

17) Go into the project properties.

18) In the Application tab, Make sure the assembly names and namespace match your XML assembly name.
-Earlier we used Plague.ThingDef_PlagueBullet. Plague should be our assembly namespace and assembly name.

19) In that same window, change the Target Framework version to .NET Framework 3.5
-Forgetting to do this will cause lots of errors.
-Select Yes when it asks you if you're sure to change the framework.

20) Go to the Build tab in the project properties.

21) Change the output path to be RimWorld\Mods\PlagueGun\Assemblies
- All .dll files will go into this directory when we "build" our code library.

22) In that same window, click the Advanced... button.

23) Change Debugging information to none.
- This prevents a simple error that occurs when RimWorld doesn't know what to do with the debug .pdb files.

24) In Solution Explorer. Go into Properties and edit AssemblyInfo.cs. Change the name of your assembly and version number as you like.
- This doesn't have to all be the same as your namespace, but it doesn't hurt to be consistent.

25) In the main option bar at the top of the visual studio (File, Edit, View...), click Project, and click Add Reference.

26) Click Browse and go to RimWorld\RimWorldWin_Data\Managed

27) Add references to Assembly-CSharp.dll and UnityEngine.dll

28) In the Solution Explorer (typically on the right side of the application), look at the references drop down list.

29) Select Assembly-CSharp. Check the Properties section (usually under Solution Explorer). Make sure the properties section has Copy Local set to FALSE.

30) Do this (Copy Local to FALSE) for UnityEngine as well.
- By doing this, we prevent the project from causing one million hash conflicts by copying the entire game's code twice!

31) Go into the References; Delete the yellow triangle references, and click the Build button and Build Solution.
- If there are errors, be sure to delete unused References and delete lines for #using that aren't in use like #using System.Threading.Tasks

Now the workspace setup is complete and we can add C# code to RimWorld.
...Psst. Still there? If you'd like to support
me and my works, do check out my Patreon.
Someday, I could work for RimWorld full time!

https://www.patreon.com/jecrell

jecrell

#6
C# Coding



Let's make our XML blueprint (ThingDef) for our new projectile type.

31) Open your Class1.cs.
32) Right click and rename the .cs file to your liking.
33) Add these lines to the top this file (and every .cs file you make from now on for modding RimWorld).

using RimWorld;
using Verse;

- These are the shared libraries that hold RimWorld code references. Without these, our code will not be able to connect with RimWorld.
34) Rename the namespace to the namespace you used earlier in the options menu and in your XML (Plague).
35) Change public class Class1 to your new ThingDef class you wrote earlier and make it inherit the ThingDef from RimWorld. Then add in our custom ThingDef variable from earlier as a float.
- Floats are very large numbers in C#. RimWorld will use them to keep track of of percentages. 1.0f is 100% and 0.05% is 5%).
e.g.

namespace Plague
{
  public class ThingDef_PlagueBullet : ThingDef
  {
       public float AddHediffChance = 0.05f; //The default chance of adding a hediff.
       public HediffDef HediffToAdd = HediffDefOf.Plague;
  }
}

**TIP** Use ThingDefs to store useful variables for your classes. DamageDefs and other variables can be kept here and changed easily in XML.

Let's make the actual projectile. For this tutorial, we're going to make a new projectile that checks for impact and adds a Hediff (health differential - poison, toxins, implants, anything).
36) Add a new class  (Project->Add Class)
37) Rename the namespace (Plague) and give the class a title that matches your XML (Projectile_PlagueBullet).
38) Make your new class inherit the Bullet class (a child of the Projectile class), so the game will treat your new projectile like a bullet and not throw (fun) errors.
39) Write a 'property' method that finds and returns your ThingDef blueprint to easily grab its XML variables.
(e.g.)

namespace Plague
{
    public class Projectile_PlagueBullet: Bullet
    {
        #region Properties
        //
        public ThingDef_PlagueBullet Def
        {
            get
            {
                return this.def as ThingDef_PlagueBullet;
            }
        }
        #endregion Properties
    }
}


40) Open Zhentar's ILSpy Mono.
- Next we want to reference the original Projectile code, so we can write a new impact event.
- Often times, RimWorld code will be private or hidden, and we can't override the code with our own. However, in this case, we can override the Projectile's "Impact" method. Even so, let's take a look at the source code to understand what we're doing better.

41) Use File->Open (or click and drag) RimWorld's Assembly-CSharp into Zhentar's ILSpy Mono.
- Assembly-CSharp is where all the code for RimWorld is located. By loading it into Zhentar's ILSpy, we can take a closer look at code used in the game. This is INCREDIBLY HELPFUL to understand the inner workings of the game.
42) Search (CTRL+F) for the Projectile class.
43) Instead of Projectile, take a look at Projectile_DoomsdayRocket.
- Notice that it is overriding the Impact method safely by using protected override void Impact.
- Let's try using this code in our own project.
44) In our Projectile_PlagueBullet class, write out protected override void Impact and autocomplete using intellisense.
-Intellisense is the best thing about Visual Studio Community. The more you program with RimWorld mods the more familiar with it you'll be. It autocompletes and fixes up simple errors.
45) Add this code under Properties.


        #region Overrides
        protected override void Impact(Thing hitThing)
        {
            base.Impact(hitThing);

            /*
             * Null checking is very important in RimWorld.
             * 99% of errors reported are from NullReferenceExceptions (NREs).
             * Make sure your code checks if things actually exist, before they
             * try to use the code that belongs to said things.
             */
            if (Def != null && hitThing != null && hitThing is Pawn hitPawn) //Fancy way to declare a variable inside an if statement. - Thanks Erdelf.
            {
                var rand = Rand.Value; // This is a random percentage between 0% and 100%
                if (rand <= Def.AddHediffChance) // If the percentage falls under the chance, success!
                {
                        /*
                         * Messages.Message flashes a message on the top of the screen.
                         * You may be familiar with this one when a colonist dies, because
                         * it makes a negative sound and mentioneds "So and so has died of _____".
                         *
                         * Here, we're using the "Translate" function. More on that later in
                         * the localization section.
                         */
                        Messages.Message("TST_PlagueBullet_SuccessMessage".Translate(new object[] {
                        this.launcher.Label, hitPawn.Label
                    }), MessageSound.Standard);

                    //This checks to see if the character has a heal differential, or hediff on them already.
                    var plagueOnPawn = hitPawn?.health?.hediffSet?.GetFirstHediffOfDef(Def.HediffToAdd);
                    var randomSeverity = Rand.Range(0.15f, 0.30f);
                    if (plagueOnPawn != null)
                    {
                        //If they already have plague, add a random range to its severity.
                        //If severity reaches 1.0f, or 100%, plague kills the target.
                        plagueOnPawn.Severity += randomSeverity;
                    }
                    else
                    {
                        //These three lines create a new health differential or Hediff,
                        //put them on the character, and increase its severity by a random amount.
                        Hediff hediff = HediffMaker.MakeHediff(Def.HediffToAdd, hitPawn, null);
                        hediff.Severity = randomSeverity;
                        hitPawn.health.AddHediff(hediff, null, null);
                    }
                }
                else //failure!
                {
                    /*
                     * Motes handle all the smaller visual effects in RimWorld.
                     * Dust plumes, symbol bubbles, and text messages floating next to characters.
                     * This mote makes a small text message next to the character.
                     */
                    MoteMaker.ThrowText(hitThing.PositionHeld.ToVector3(), hitThing.MapHeld, "TST_PlagueBullet_FailureMote".Translate(Def.AddHediffChance), 12f);
                }
            }
        }
        #endregion Overrides

46) Build your project again to save the new changes.
47) Go in-game to check to see if RimWorld found any errors and to check your own code's results.
48) In the options menu, make sure Development mode is enabled.
- This will give us an easy way to add our Weapon to the in-game map.
49) Start a map. Click the tool icons above. Explore them a bit. Pretty cool eh? When ready, find the Spawn Weapon command.
- IF for some reason you do not see your Plague Gun or new item available in the lists, press the ~ key on your keyboard. Check your error logs for anything mentioning the Plague Gun.
- These kinds of errors are very common.
50) In Spawn Weapon, click the Plague Gun and drop it in the map. Have a character equip it and start shooting.
51) There is at least one thing left to do (as you may have seen) translation!

--- Attached Below -- Complete .CS Files For Reference ---

[attachment deleted by admin due to age]
...Psst. Still there? If you'd like to support
me and my works, do check out my Patreon.
Someday, I could work for RimWorld full time!

https://www.patreon.com/jecrell

jecrell

#7
Translations



Now it's time to turn our project's C# strings into text that everyone can translate if they switch to other languages.

Remember these lines in our C# project?

                        Messages.Message("TST_PlagueBullet_SuccessMessage".Translate(new object[] {
                        this.launcher.Label, hitPawn.Label
                    }), MessageSound.Standard);


and...


MoteMaker.ThrowText(hitThing.PositionHeld.ToVector3(), hitThing.MapHeld, "TST_PlagueBullet_FailureMote".Translate(Def.AddHediffChance), 12f);


Both of these sections use the .Translate string sub-function.
Each .Translate can take a single argument (.Translate(stringGoesHere)) or multiple arguments (.Translate(new object[] {stringOne, stringTwo})).
In XML, these arguments are then referenced as {0} if singular, or {0} and {1} etc for multiple arguments. I'll show you what I mean below.

.Make a Languages folder.
RimWorld>Mods>PlagueGun>Languages

.Make an English folder.
RimWorld>Mods>PlagueGun>Languages>English

.Make a Keyed folder
RimWorld>Mods>PlagueGun>Languages>English>Keyed
-Keyed means that our C# code references a "Key" in our mod's language dictionary.

.Make a text file and change it to an XML file named PlagueGun_Keys.xml

.Fill out the PlagueGun_Keys.xml with our string keys.
-First, add in this familiar line.
<?xml version="1.0" encoding="utf-8" ?>
-Add in the tag for LanguageData

<LanguageData>
</LanguageData>

-In between LanguageData, add in the keys we used earlier, and define them.
-Earlier, we used the string keys TST_PlagueBullet_FailureMote and TST_PlagueBullet_SuccessMessage.
-Filling in the file should look something like this.

<?xml version="1.0" encoding="utf-8" ?>
<LanguageData>
  <TST_PlagueBullet_FailureMote></TST_PlagueBullet_FailureMote>
  <TST_PlagueBullet_SuccessMessage></TST_PlagueBullet_SuccessMessage>
</LanguageData>

-Let's use our argument notes from earlier. We passed one argument to the failure mote. So we should use {0}, to represent the hit chance. We passed two arguments to the success message, so we should use {0}, to represent the launcher, and {1}, to represent the hit character.
-Fill out the text for the messages.

<?xml version="1.0" encoding="utf-8" ?>
<LanguageData>
  <TST_PlagueBullet_FailureMote>Failure: {0} chance</TST_PlagueBullet_FailureMote>
  <TST_PlagueBullet_SuccessMessage>{0} infected {1} with the plague!</TST_PlagueBullet_SuccessMessage>
</LanguageData>


.Save the file.

.Translation complete.
...Psst. Still there? If you'd like to support
me and my works, do check out my Patreon.
Someday, I could work for RimWorld full time!

https://www.patreon.com/jecrell

jecrell

...Psst. Still there? If you'd like to support
me and my works, do check out my Patreon.
Someday, I could work for RimWorld full time!

https://www.patreon.com/jecrell

jecrell

...Psst. Still there? If you'd like to support
me and my works, do check out my Patreon.
Someday, I could work for RimWorld full time!

https://www.patreon.com/jecrell

jecrell

...Psst. Still there? If you'd like to support
me and my works, do check out my Patreon.
Someday, I could work for RimWorld full time!

https://www.patreon.com/jecrell

jecrell

...Psst. Still there? If you'd like to support
me and my works, do check out my Patreon.
Someday, I could work for RimWorld full time!

https://www.patreon.com/jecrell

jecrell

...Psst. Still there? If you'd like to support
me and my works, do check out my Patreon.
Someday, I could work for RimWorld full time!

https://www.patreon.com/jecrell

kaptain_kavern


milon