Darktide: Creating a Weapon Customization Plugin

Introduction

This is a walkthrough on how to make attachments for the Extended Weapon Customization (EWC) mod for Warhammer 40,000: Darktide, which is the main point of creating a plugin. I'm assuming you have a cursory knowledge of filesystems and are comfortable with installing mods for Darktide. I'm assuming you will experiment by adding to the template plugin, so I won't cover the basics of creating Darktide mods here (see the DMF docs on initializing mods).

Requirements

  1. Any text editor, preferably one with syntax highlighting. Notepad will work in a pinch, but programs such as Notepad++ or Visual Studio Code are much more powerful.
  2. The weapon_customization_plugin template.
    This describes the core steps of how everything works. It goes over each step necessary for creating an attachment from scratch. The way Syn's Edits and my plugin (OwO) have added parts is based on how Random Account did it in the MT Plugin, which takes these steps and makes it easier to do en-masse. I'll cover the differences further below.
    Grasmann also left a lot of notes in this plugin explaining what flags exist for making parts.
    To get it, join the Darktide Modders Discord, go to the #weapon-customization-mod channel, and scroll way back to the pinned message containing the template archive.
  3. [Optional] Go to Mod Options --> Darktide Mod Framework --> Enable Developer Mode. This will let you reload mods (which will add your attachments) without having to close the game. By default, you reload mods using Ctrl + Shift + R.
  4. [Optional] Modding Tools utility. This adds a menu that lets you move each attachment and see the changes live.
    Note that the Nexus version has a bug that will make you crash when you reload mods, which is fixed in the versions uploaded to the Modding Tools thread in the Darktide Modders Discord.
    I have a quick demo video for using it.

Going Through the Template

I'll briefly cover each part of this, but I won't discuss the comments. These explain each parameter for each step, and you can read those yourself.

Technical Setup

The first line of the template is something every Darktide mod does. The first three sections, Performance, Data, and Functions, are just things you can copy and paste (when making your own plugin). You can skip ahead.
Performance makes a local copy of global functions. This is supposed to have better performance, since accessing things from the global table involves table lookup. See this StackOverflow discussion for elaboration. I have not verified this myself, but Gras has said similar things in the Discord. Upd: This is confirmed by a Lua optimization guide, created and presented by Fatshark at a game dev conference (I forgot which): [Fatshark Lua Optimization Guide](https://dmf-docs.darkti.de/#/Fatshark-%E2%80%90-Lua-Optimizing-Guide?id=use-a-local-to-avoid-repeated-globaltable-lookups)
Data is making shorthand for where game data is located, so you can save some typing. For example, the actual location of the model for the Shredder Autopistol iron sights is "content/items/weapons/player/ranged/sights/autogun_pistol_sight_01". Instead of typing out that mess at the start every time, it gets stored into _item_ranged. The .. means string concatenation, which means "connect these two strings to make one string."
Functions defines a prepend function so you can insert things into existing tables. Prepending means you have a list, for example, and you put a new thing into the list at the start. Since it only applies to one fix at a time, the index `i` is always 1. Too lazy to find the real reason for prepending vs appending, so I just go with it.

More Technical Setup

Before I get into it, there's also this part around the actual injections:
      function mod.on_all_mods_loaded()
          local wc = get_mod("weapon_customization")
          if wc then
              ...
          end
      end
    

This is just background stuff to make sure our code runs properly. The function on the outside means "run this one startup, once all mods have loaded." Then, it checks to make sure EWC is installed. If it is, your code will execute. Otherwise, nothing will happen. I personally do not use this exact structure, but I'll go with for the sake of this guide.

Injection

Now for the actual process that injects parts.

ID Assignment

      table.insert(
        wc.attachment.autogun_p1_m1.sight,
        {id = "autogun_pistol_sight_01", name = "Autopistol Sight"}
      )
    
This section injects the ID autogun_pistol_sight_01 into the table wc.attachment.autogun_p1_m1.sight.
wc.attachment. means this is an attachments table from the original EWC mod
autogun_p1_m1. means it's the attachments for all of the autoguns (Infantry, Braced, and Vigilant). Autoguns are a special case because all 3 are in the same name. Normally, the number in p1 would change corresponding to the Pattern. For example, Lasguns are split across p1 (Infantry), p2 (Helbore), and p3 (Recon). m1 means Mark, but it includes all of them with just m1.
sights is the name of the slot these attachments are associated with. These names will be very similar to the name that shows up in the Customization menus.
The ID is can be any unique string. It's used to identify the attachment. You can call it albanian_cock_slam_04 or whatever. The name is what's displayed in the Customization menu, so this is what users will call it.

Model Assignment

      table.merge_recursive(
        wc.attachment_models.autogun_p1_m1,
        {
          autogun_pistol_sight_01 = {
            model = _item_ranged.."/sights/autogun_pistol_sight_01", type = "sight", parent = "rail", 
            angle = 0, move = vector3_box(0, 0, 0), remove = vector3_box(0, -.2, 0), 
            automatic_equip = {rail = "rail_01"}
          }
        }
      )
    
Note: I changed some of the spacing because I find it easier to read this way. The functionally is still the same.
The main takeaway here is autogun_pistol_sight_01 = { model = _item_ranged.."/sights/autogun_pistol_sight_01" ... }. That first part, as you might recognize, is the ID given above. Then, the model is the "address" (I don't know exactly what to call this) of where Darktide keeps the physical model. In this case, it's the model for the Shredder Autopistol iron sights. To use parts from player weapons, they'll all start with _item_ranged or _item_melee. For other things, it takes more work that I can't explain very well.
As far as I know, there's not really an easy way to match the model and address. What I do is scroll through the Customization menu until I find an attachment from EWC that looks like the part I want. I take note of the name (with "Rename Attachments" disabled in the EWC Mod Options), then try and find it in the EWC files. I go over search methods in my EWC custom edits guide. Following that method, you'll find the ID assignment. Scroll a bit further down, and you'll find the model assignment.

Fixes

      table.prepend(
        wc.anchors.autogun_p1_m1.fixes,
        {
          -- Rail
          { dependencies = {"autogun_pistol_sight_01", "!receiver_01"},
            rail = {scale = vector3_box(0, 0, 0)}
          },
          -- Sight
          { dependencies = {"autogun_pistol_sight_01"},
            sight = {offset = true, position = vector3_box(0, .01, 0)}
          },
        }
      )
    
Note: I changed some of the spacing because I find it easier to read this way. The functionally is still the same.
Fixes apply transformations (position, rotation, and scale/size) to part, if the dependencies (combination of parts used) match; I cover how dependencies work in edits guide and I'm too lazy to succintly copy that over here. EWC checks for fixes from top to bottom, following the load order.
Slots, once directly affected by a fix, won't be affected again. For example, say I create a blade blade_of_destiny and a hilt virginity_shield, and I have:
      table.prepend(
        wc.anchors.combatsword_p1_m1.fixes,
        {
          { dependencies = {"virginity_shield",},
            hilt = {scale = vector3_box(1, 2, 1)}
          },
          { dependencies = {"blade_of_destiny"},
            hilt = {position = vector3_box(1, 10, 1)}
            blade = {position = vector3_box(0, 0.5, 0)}
          },
        }
      )
    
If I equip both of these attachments on the same sword, the fixes will first apply the fixes from matching with "virginity_shield," which stretches the hilt vertically. Then, it applies the fixes from matching with "blade_of_destiny," but since the hilt has already been affected, it doesn't get stretched again. However, it will still apply the fixes to the blade, which moves it up.

Final Injection

      table.insert(
        wc.sights,
        "autogun_pistol_sight_01"
      )
    
This injects your attachment by its ID into the slot's table.