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). I've tried to include some explanation for general programming terminology, but if I missed anything you should look it up elsewhere.
Requirements
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.
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.
Or download Grasmann's GitHub repositor and take out the plugin from there.
[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.
[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 in this context.
=============================
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.
This template demonstrates how to add basic attachments to an existing slot. The example is adding the Shredder Autopistol iron sights to the Autoguns, but this can be applied to any combination of part and weapon family.
Technical Setup
The first line of the template is something every Darktide mod does, getting a reference for itself so it can use features from Darktide Mod Framework.
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 and you're putting 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. Prepending is done so fixes are compatible between the plugins and the base mod. Since plugins need to load after the base mod for the base mod to initialize itself, fixes are prepended to give them priority in the base mod. In other cases, I think prepending vs appending doesn't matter and this was just used out of convenience. As a side effect, this is why attachments are shown in the order (base mod --> bottom plugin --> top plugin).
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.
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.
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:
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.
This injects your attachment by its ID into the slot's table. In general, I'm not sure how necessary this is, but I do know it's required for having functional scopes with lenses.
=============================
Comparing the Template to Functions from the MT Plugin
Now, those basic operations were (in the grand scheme of things) pretty simple. However, it's easy to forget the exact syntax on which one to use, and any errors will produce generic error messages (something like attempting to insert to table nil).
The MT Plugin, the first major plugin for EWC, created some helper functions to ease the workflow for us plugin authors. It wraps the base method functions with some extra functions to make it simpler to inject multiple attachments in one declaration and adds some more detailed error messages. Later major plugins have adopted these helpers.
For simplicity, I'll refer to the ways shown in the template plugin as "the base mod method" and using these helper functions as "the MT method"
Let's take a look at the Attachment Injection part through both methods. The other two parts (Model Injection and Fixes Injection) follow the same structure.
MT: Inject Attachments
By the base method, there is
table.insert(
wc.attachment.autogun_p1_m1.sight,
{id = "autogun_pistol_sight_01", name = "Autopistol Sight"}
)
The equivalent using the MT method would be
function mod.inject_attachments(variant_id, slot, attachments_table)
if not wc.attachment[variant_id] then
mod:error(string.format("attachment variant_id [%s] invalid", variant_id))
return
end
if not wc.attachment[variant_id][slot] then
mod:error(string.format("attachment slot [%s.%s] invalid", variant_id, slot))
return
end
attachment_ids[variant_id] = attachment_ids[variant_id] or {}
attachment_ids[variant_id][slot] = attachment_ids[variant_id][slot] or {}
for _, attachment in ipairs(attachments_table) do
if not string.sub(attachment.name, 1, 3) == "MT " then
attachment.name = "MT "..attachment.name
end
table.insert(wc.attachment[variant_id][slot], attachment)
table.insert(attachment_ids[variant_id][slot], attachment.id)
end
end
Wow. That looks like a big difference. However, the core functionality is the same. Since you already know what the base method means, let's break the MT method down.
The parameters variant_id, slot, attachments_table are given when you call this function to specify which weapon the attachment is for, which slot it goes into, and the list of ids and names. I'll elaborate later.
The two if not ... blocks are to make sure EWC supports the weapon before we try injecting into it, and it'll exit if the weapon is not supported. This is mainly to give us plugin authors more obvious error messages. Without this, when we injected it would've said something like Argument to table.insert is nil (expected table) without specifying where the table came from (so you won't know which weapon caused the issue).
Next, it's getting currently present attachments and slots from the plugin it comes from. attachment_ids is a table created by each plugin that uses this method, and it's created before any of these functions are run. The or {} part is to automatically assign it an empty table on the first try. It means "use what exists or create an empty table" because Lua automatically uses the first value in an "or" boolean operation if that first value exists.
The for loop goes through every attachment id/name pair and injects them. I kind of blew past the parameters, so that may be a bit confusing, so let's go through an example. I'll use a simplified part of my OwO plugin, the slim blades:
mod.inject_attachments("powersword_p1_m1", "blade", {
{id = "owo_slim_dclaw_01", name = "OwO Slim DClaw 1"},
{id = "owo_slim_dclaw_02", name = "OwO Slim DClaw 2"},
{id = "owo_slim_dclaw_03", name = "OwO Slim DClaw 3"},
{id = "owo_slim_dclaw_04", name = "OwO Slim DClaw 4"},
{id = "owo_slim_dclaw_05", name = "OwO Slim DClaw 5"},
{id = "owo_slim_dclaw_06", name = "OwO Slim DClaw 6"},
{id = "owo_slim_dclaw_07", name = "OwO Slim DClaw 7"},
})
With the base method, table.insert can only take one attachment at a time. That means I would've had to type table.insert(wc.attachment.powersword_p1_m1.blade, {id = "owo_slim_dclaw_0n", name = "OwO Slim DClaw n"}) 7 times (or manually created a loop every time). With the MT method, I gave it the table and it'll run that automatically for each id/name pair in each subtable.
It also does a bit more. The if not string.sub part is to check if the first 3 characters of the name is "MT ", and if not, it adds it to the front. Unfortunately, it doesn't really work lol. If it did, you'd replace that with your preferred prefix. table.insert(wc.attachment[variant_id][slot], attachment)
is the equivalent part to the base method, just using index notation instead of dot notation because that's how Lua syntax works. table.insert(attachment_ids[variant_id][slot], attachment.id)
adds the id to the plugin's list of attachment ids to validate later.
In the end, that means instead of writing the table.insert line over and over again, I call this function and give it a table, which is much more natural and easier to remember.
=============================
Organizing Your Code to Add Attachments to Multiple Weapons
Using either method, you could simply put that all into the main file and call it a day. That's fine for very simple plugins, but it can very quickly get disorganized. That's why the base mod and every major plugin splits things into separate files. The base mod has to create all the initial tables, while the MT method only concerns itself with injecting attachements/models/fixes, so I'll mainly break down the MT method, using the modified slim blades example above.
First of all, you have your main mod file (weapon_customization_your_plugin.lua) which defines your helper functions and executes the other files. With the MT method, plugins create "common" files to define functions including injections for attachments and models, then they have "weapon" files (one for each weapon family) to call those functions for the respective weapon family. The "common" and "weapon" files are listed in files_to_load.lua file, which runs them. files_to_load gets run in the main mod file. For my example, let's say I want to add the slim blades to the Power Swords and the Catachan "Devil's Claw" Combat Swords. The folder structure looks like this:
[WIP]!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
You create a Lua file for each weapon family and name it after the first mark (you could technically name it whatever you want). Let's say I want to add parts to the Power Sword and Catachan "Devil's Claw" Combat Swords.
Bundling Injections for Reuse
=============================
Analyzing Fixes and Dependencies for Efficiency
As you'll recall, fixes are the instructions to change position/rotation/scale for attachments, and the appropriate ones are applied (on a per weapon basis) by searching through all the fixes in the weapon family's anchors (fixes table) for matching dependencies.
The code used to find fixes is in weapon_customization/scripts/mods/weapon_customization/classes/gear_settings.lua on (as of writing) line 1190: GearSettings.apply_fixes = function(self, gear_id_or_item, unit_or_name). This is ~150 lines, so I'll only break down what I believe are the highlights.
First, it checks if this weapon has customizations available. If so, it searches to see if this weapon family has fixes available. Then, it checks every attachment slot to list out all of this weapon's currently equipped attachments. Once it has this list, it goes through some quick checks to see if dependencies need to be checked at all.
EWC uses the native function table.contains to see if the dependencies contain at least one of the equipped attachments. It'll go through (in the worst case) every dependency check in the fixes table for this weapon family. Since this is a comprehensive search (because it can't know if a fix is uncessary without checking it), that means that every fix you add affects performance (very minorly) for all the weapons in the family, not just the ones with your parts equipped.
Calculating the Worst-Case Scenario
The key here is that we can calculate the worst-case total amount of checks made, checking every single dependency.
We have every slot checked for each dependencies, so we can call it (number_of_slots * possible_dependencies) = total_checks_done
If you add another slot, that gives (number_of_slots + 1) * possible_dependencies = (number_of_slots * possible_dependencies) + possible_dependencies
If you add another dependency check, that gives number_of_slots * (possible_dependencies + 1) = (number_of_slots * possible_dependencies) + number_of_slots
Ok, so what does this mean? It means that when you add a slot, it increases the total checks done by the amount of dependencies. Since dependencies will basically always outnumber slots (mainly due to having other plugins), think carefully about adding slots.
How Can I Avoid the Worst-Case Scenario?
Because the search is comprehensive, it checks each group of dependencies at least once. You can't do anything about that, but what you can affect is the internals of each group. The main concept is what order you put dependencies when using AND (the comma). If you need to have A AND B and find that you don't have A, you can skip checking B since you already know you can't have both; this is a concept known as "short-circuiting" in programming.
Let's say you have { dependencies = { "sick_part", "uncool_part_01|uncool_part_02|uncool_part_03|uncool_part_04|uncool_part_05|uncool_part_06|uncool_part_07"}, },
When you have a weapon that uses none of those parts, they'll start by seeing they lack "sick_part" and immediately stop checking this group. If the order was swapped, they would've needed to check the entire OR chain. That's 1 check vs 7 checks, which is a 700%! Now, realistically, this relatively big difference ends up being a very small impact in the end because modern machines are beasts. However, that doesn't mean you shouldn't aim to optimize, especially when it's as easy as this.
This is applied from gear_settings.lua line 1242: for _, dependency_entry in pairs(fix_data.dependencies) do. This line is saying for each group of dependencies, check each entry (so with the earlier example, the first entry is "sick_part" while the second is "uncool_part_01|uncool_part_02|uncool_part_03|uncool_part_04|uncool_part_05|uncool_part_06|uncool_part_07").
It does a lot of things to actually check, but the main point is at the end of each dependency check within a group:
has_dependencies = has_dependency_possibility
if not has_dependencies then break end
This means "if this whole entry doesn't match, go to the next group (stop checking this group)." That's the short-circuiting in action.
What is the Ampersand (&) Doing in Dependencies?
Good question. You may have seen it pop up in one of the plugins. For example, this fix for the plasmagun in the MT Plugin:
{dependencies = {"!&barrel"}, -- Magazine
barrel = {offset = true, position = vector3_box(0, -.01, 0.04), rotation = vector3_box(0, 0, 0), scale = vector3_box(1.3, 1, 1.5)}},
[sic]
This isn't really explained anywhere. However, digging through gear_settings.lua sheds some light. Starting on line 1256, there is:
if self:cached_find(possibility.possibility, "&") then
possibility.original_item = true
possibility.possibility = self:cached_gsub(possibility.possibility, "&", "")
end
This is inside of the "searching for dependencies" part. It basically means "if the dependency we're currently checking has an &, it's an attachment from the base mod." It does this by flagging the item as a base mod attachment, then removes the ampersand from it.
Wait, but how does it know it's an original item? That actually happens at game launch. It runs
GearSettings.original_item = function(self, gear_id_or_item)
-- Setup master items backup
mod:setup_item_definitions()
-- Get item from potential gear id
local item = self:item_from_gear_id(gear_id_or_item)
local item_name = item and item.name
-- Return original item
return item_name and mod:persistent_table(REFERENCE).item_definitions[item_name]
end
on game start to mark the base mod's things as original, then it puts them into a table for referenced later (which would be now).