Modding Tutorials/Plague Gun (old)/Csharp Coding

From RimWorld Wiki
Jump to navigation Jump to search

This tutorial was originally written by Jecrell. Thread.

Editors note: There are a couple of parts in this tutorial that differ from the original to reflect changes made to the original game.

C# Coding[edit]

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

  1. Open your Class1.cs.
  2. Right click and rename the .cs file to your liking.
  3. 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.
  4. Rename the namespace to the namespace you used earlier in the options menu and in your XML (Plague).
  5. Change public class Class1 to your new ThingDef class you wrote earlier and make it inherit the ThingDef from RimWorld.
    • Editors note: Renaming things in an IDE like Visual Studio is best done by pressing F2 or right-clicking the item/name. Doing it like that will change all occurrences of that thing, so everything that references your namespace or Class1 will now use the new name.
  6. 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.05f is 5%).
      namespace Plague
        public class ThingDef_PlagueBullet : ThingDef
             public float AddHediffChance = 0.05f; //The default chance of adding a hediff. The XML overwrites this.
             public HediffDef HediffToAdd;
    • TIP Use ThingDefs to store useful variables for your classes. DamageDefs and other variables can be kept here and changed easily in XML.
      • Excursion with the editor: Jecrell's original tutorial set a default value for HediffToAdd. It had something like public HediffDef HediffToAdd = HediffDefOf.Plague; but as of 1.0 that causes the following warning:
        Tried to use an uninitialized DefOf of type HediffDefOf. DefOfs are initialized right after all defs all loaded. Uninitialized DefOfs will return only nulls. (hint: don't use DefOfs as default field values in Defs, try to resolve them in ResolveReferences() instead) Debug info: DirectXmlToObject is currently instantiating an object of type Plague.ThingDef_PlagueBullet

At the time RimWorld reads and loads our ThingDef_PlagueBullet class, the DefDatabase isn't yet populated with Defs, so HediffDefOf.Plague will return null. There are two ways to solve this:

        1. Leave it blank, and just use the XML values. Downside: no default. If you forget to set a Hediff in the XML, it will be null.
        2. Read the error again, and try to resolve them in ResolveReferences() instead. ThingDef does indeed provide a ResolveReferences method we can override:
                  public override void ResolveReferences()
                      HediffToAdd = HediffDefOf.Plague;
          and that solves the issue!

  1. 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).
  2. Add a new class (Project->Add Class)
  3. Rename the namespace (Plague) and give the class a title that matches your XML (Projectile_PlagueBullet).
  4. 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.
  5. Write a 'property' method that finds and returns your ThingDef blueprint to easily grab its XML variables.
    namespace Plague
        public class Projectile_PlagueBullet: Bullet
            #region Properties
            public ThingDef_PlagueBullet Def
                    //Case sensitive! If you use this.Def instead this.def it will return Def, which is this getter. This will cause a never ending cycle and a stack overflow.   
                    return this.def as ThingDef_PlagueBullet;
            #endregion Properties

  1. Open your favourite decompiler. See Required Items and Main article Decompiling source code.
    • We'll be using 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.
  2. 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.
  3. Search (CTRL+F) for the Projectile class.
  4. 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.
  5. 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 auto-completes and fixes up simple errors.
  6. Add this code under #endregion Properties:
    #region Properties
    public ThingDef_PlagueBullet Def
            //Case sensitive! If you use Def it will return Def, which is this getter. This will cause a never ending cycle and a stack overflow.
            return this.def as ThingDef_PlagueBullet;
    #region Overrides
    protected override void Impact(Thing hitThing)
        /* This is a call to the Impact method of the class we're inheriting from.
            * You can use your favourite decompiler to see what it does, but suffice to say
            * there are useful things in there, like damaging/killing the 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.
                    this.launcher.Label, hitPawn.Label
                ), MessageTypeDefOf.NeutralEvent);
                //This checks to see if the character has a heal differential, or hediff on them already.
                var plagueOnPawn =;
                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;
                    //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);
                    hediff.Severity = randomSeverity;
            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
  7. Build your project again to save the new changes.
  8. Go in-game to check to see if RimWorld found any errors and to check your own code's results.
  9. In the options menu, make sure Development mode is enabled. Main article Testing mods
    • This will give us an easy way to add our Weapon to the in-game map.
  10. 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.
  11. In Spawn Weapon, click the Plague Gun and drop it in the map. Have a character equip it and start shooting.
  12. There is at least one thing left to do (as you may have seen) translation!

Editor's notes[edit]

  • If you've read the other articles on this wiki, you could argue that DefModExtensions are a viable alternative to the <ThingDef Class="ThingDef_PlagueBullet"> - you'd be right. Neither approach is wrong.
  • If you're wondering why we've added three tags to the XML but only two tags to the ThingDef_PlagueBullet, congratulations on being observant. The thingClass tag is one that exists in the ThingDef class we're inheriting from. Remember how we took a look at the Projectile_DoomsdayRocket class? The <defName>Bullet_DoomsdayRocket</defName> has Projectile_DoomsdayRocket set as its thingClass.

Changes and Updates[edit]

  • Messages.Message uses MessageSound.Standard in the example, but this was changed to a Def-based system in a later release of RimWorld.
  • The Translate() option underwent a refactor and no longer needs an array of arguments.
  • Some of the default null values in the MakeHediff() and AddHediff() were redundant; they were optional arguments with null as the default value.
  • var plagueOnPawn =; had an additional null-check on hitPawn. This is redundant: if (hitThing != null && hitThing is Pawn hitPawn) is already a null-check. It was also too late: if hitPawn was null then hitPawn.Label would have already already caused a NullReferenceException.

Completed files[edit]

See GitHub for the complete mod.

See also[edit]

  1. Required Items
  2. XML Stage
  3. Connecting XML and C#
  4. C# Assembly Setup
  5. C# Coding <- You are here.
  6. Localisation

More in-depth[edit]