TZWZ's personal page
Guide to modding Don't Starve Together
02.07.2026
lua

At one point in my life, I got myself into playing Don't Starve Together (often abbreviated as DST) with some of my friends. I recommend it; great game.

But equipment management at some point got chaotic/irritating, in the sense that you want to use something, you know you had it, yet you don't know where and can't see it. It's tiring after some time. That's how the idea for macroner was born. A mod that will allow players to add macros dynamically, allowing them to (un)equip items by custom shortcuts.

To create that I had to learn how to mod the game. This post is a write-up of knowledge I gained when creating said mod.

Learning the base language to use

The game is modded using the Lua language. So before you begin your modding adventure, you have to learn it. It's pretty simple language. Reading through the online ebook in the getting started guide should be possible in a single afternoon. Knowing most of the concepts shortens the needed time even more. Then there's stdlib documentation, which wasn't of much use for me but contains some ready-made functions when needed.

Finding resources

The things that exist are a Klei forum for modders, a user-managed wiki, and a custom user-managed DST scripts/API page, with a GitHub repo for them.

You can look around, especially that wiki, to find some guides for novices to modding and links to other posts, which sometimes makes you open the same post/page multiple times. Lots of knowledge, lots of links. Personally, didn't find them that much helpful really, except for some tips and tricks (later) and the first step of how to even start, how to create a mod at all (also later).

Creating mod

We have to start somewhere. Let's create an empty mod.

Place for the mod

Firstly, you have to find the mods directory in your DST install path. For Linux, it's something like ~/.steam/steam/steamapps/common/Don't Starve Together/mods or ~/.klei/DoNotStarveTogether/mods. On Windows, it's supposedly C:\Users\[Username]\Documents\Klei\DoNotStarveTogether\mods.

Inside that directory, create a mod directory. For me it was macroner-dst, use whatever name you want. Then create two files - modinfo.lua and modmain.lua. The first one describes your mod - its name, description, icon, available settings, and the like. Second is the starting point of your mod.

Mod configuration

Example modinfo.lua:

name                    = "Macroner"
description             = "Allows to add custom macros"
author                  = "Tzwz"
version                 = "1.0.0"
api_version             = 10

dst_compatible          = true
dont_starve_compatible  = false
all_clients_require_mod = false
client_only_mod         = true

icon_atlas              = "modicon.xml"
icon                    = "modicon.tex"

configuration_options   = {}

name, description and author will be displayed in the game's mod list. Compatibility will be shown there in one form or another too, as it will have some text like "Compatible with Don't Starve Together" below the mod name.

api_version, as far as I could find, is supposed to be 10 and it's the newest version. I saw some code that had this set to 6 or 8, so I guess they updated the API over the years.

icon_atlas and icon are describing your mod icon in DST's custom image format. More on that later, but for now these files may not exist, and it will work anyway.

configuration_options are options that will be shown in the game's mod menu to customize the mod behavior. I didn't have anything to customize, so you will have to figure it out yourself, but from what I saw, that DST API page I mentioned earlier has something to work with.

Now when you launch the game and go into the "Mods" menu, you should see your mod there.

Making debugging easier

Based mostly on things from this forum post.

Force enable mod

When creating a mod, you will constantly crash the game. After each restart, you may be forced to exit and launch the game. To avoid this, we will firstly configure DST to always load our mod. Go to the mods directory (a directory above where you created your modinfo.lua and modmain.lua). It should contain a file named modsettings.lua. Add a line ForceEnableMod("<modname>"), where <modname> is the name of the directory with your mod. For me this line is ForceEnableMod("macroner-dst"). This will always turn on your mod when loading the world, including when it's disabled in the "Mods" menu.

Enabling reloading and in-game console

Now we will enable options that will allow us to restart the server when we need to. Open your modmain.lua and add the following lines:

GLOBAL.CHEATS_ENABLED = true
GLOBAL.require('debugkeys')

This allows us to open the console, reload mods, and restart the server when the game crashes. To open the console, use ~. You can write things there, but for example, print(GLOBAL) doesn't really work. It sends this as a command somewhere, and you never see anything. My guess is it's sent to the server, which prints it in its own logs, and that's about it.

But you can use other commands. The game even gives you autofill for function names and even entity names. You can use c_spawn("shovel") to spawn items and other entities at your cursor position, where shovel can be prefab of any item (basically item kind/blueprint in the game. Don't worry too much, the game will hint you possible values). It doesn't seem to always work if, for example, your mouse is too close to something else than the ground. There's also DebugSpawn("shovel") which does pretty much the same thing.

Another thing I found useful it c_reset() which restarts the server, loading mods anew in the process. Useful for when you change the code and want to reload. No need to restart the game.

When the game eventually crashes, which it surely will, you will get a stack trace and buttons allowing you to exit the game pretty much. You can now press Ctrl+R, which will work just like c_reset() in the console. This probably saved me the most time during development, considering the alternative is to close the game, open the game, and load the world.

Code debugging, seeing things

For debugging, you have print(value) and for k, v in pairs(values) do print(k, v) end I'm not joking, sadly. From my experience, when printing ipairs you may get fewer key-value pairs, and pairs always works.

print calls will be printed in the game console (one opened with ~), but it's not something you can scroll. That's why I found more use in client_log.txt, which on Linux can be found in ~/.klei/DoNotStarveTogether/client_log.txt. It contains all logs, so open the file, go down, and find whatever you want to see. After crash you may want to open this to see fuller picture of stacktrace and the like, before you reload server, so you can modify mod, reload and check if change helped.

Searching for "How to do X"

To find something to work, with you should have a file like scripts_readme.txt in data in the DST install path (one directory above mods you entered to create mod), that is, in <DST path>/data. It should tell something along the lines of entering databundles/, copying scripts.zip somewhere else, and unpacking it. Now you have all the game scripts in front of you. You can look around them to find things that look like something you want to use.

The reality of it is that some objects are inside GLOBAL and some are not. For example, to listen to all keypresses, you have to call GLOBAL.TheInput:AddKeyHandler. On the other hand, if you want to modify some class after it's loaded, then use AddClassPostConstruct, which is not part of GLOBAL, it's just a global function.

Inside scripts you unpacked, you can find the implementation of AddKeyHandler by opening input.lua. The function is there added to the object Input. It's okay, it's being created, it's just not yet added to GLOBAL. Then you search for some AddKey function globally, like AddKeyHandler, you find a call to it in frontend.lua which uses just TheInput:AddKeyHandler. No kind of import, no GLOBAL, nothing. So yeah, you have to guess and check.

Also, sometimes things you try to print may not show up, especially for printing tables, so it's even funnier, and you have to look through scripts to see how things are done.

Using multiple files

From what I could find, the idea is to create a directory scripts/<mod name>, like scripts/macroner, and then use local X = require("macroner/x"), where x is a file scripts/macroner/x.lua. scripts is already resolved, and we add the mod name to have sort of a namespace.

Overwriting methods

Not everything you want to do has a function you can use to attach to an event, like you can for keyboard events. In such cases, you have to do something along the lines of

local origFn = someObj.TheFn
function someObj:TheFn(self, ...)
  -- your code here
  return origFn and origFn(self, ...)
end

This way you can react to an "event" when it happens, as some function will be called then. You may want to call orgFn first and return the result when you return early or do a wrapper like

local runOrig = function() return origFn and origFn(self, ...) end
-- Your code
return runOrig()

This may help for cases when calling the original function may affect what you can/want to access in your code.

Overwriting classes

Sometimes (all the time?), you may need to attach your code to some pre-existing entity, like a UI element. To do that, said UI needs to exist first. That's where AddClassPostConstruct comes in. For example, if you want to attach your UI, let's say a button, somewhere relative to the bottom-right map button, then you have to do something like this:

local ImageButton = require "widgets/imagebutton"
AddClassPostConstruct("widgets/mapcontrols", function(self)
  local btn = self:AddChild(
    ImageButton(
      "images/frontend.xml", "button_long.tex"
    )
  )
  btn:SetPosition(-25, 60, 1)
  btn:SetScale(0.6)
  btn:SetText("hello")
  btn:SetOnClick(function()
    btn:SetText("Clicked at least once")
  end)
end)

The "widgets/mapcontrols" corresponds to widgets/mapcontrols.lua in the unpacked scripts directory. In general, it expects a Class, in scripts it's done using literally that - Class(...). From what I can understand, the value is just a string path to a file in scripts where that Class call is done, without lua at the end.

The SetPosition here is placed relative to self, as our button is placed relative to the center of the mapcontrols widget. The ImageButton value, the information about which image to load, is a bit hard to figure out. Other way that search through scripts you unpacked, then use whatever you find passed as a value to ImageButton (possibly other things are acceptable too, like Image). After finding an image that looks neat to you, you can mess with btn size using things like

btn.text:SetScale(0.6) -- Scale of text
btn.bg:SetScale(0.8, 0.9) -- Scale of background image
btn.text:SetPosition(0, 5) -- Position only of text, relative to btn:SetPosition
btn:SetNormalScale(0.5) -- Normal button size
btn:SetFocusScale(0.55) -- Button size when hovering

Background scaling is neat, as you are able to change size in any way you need to.

Other than that, you can use your own TEX files, if you can make them, explained later on. But overall I think using game assets is a better idea, as this way the mod looks like part of the game, not something custom, out of place, and the like.

Storing values to persistent storage

The game gives us two tools. First is JSON encoder in GLOBAL.json with GLOBAL.json.decode() and GLOBAL.json.encode(). Use this to convert whatever data you have to JSON so it can be stored. Then there's GLOBAL.TheSim and its GLOBAL.TheSim:SetPersistentString() and GLOBAL.TheSim:GetPersistentString().

Creating custom screen

To create a new screen, where you will disable game UI, character control, and the like, you have to create a new Class of type Screen like this:

local Screen = require "widgets/screen"
local Widget = require "widgets/widget"

local myScreen = Class(Screen, function(self)
  Screen._ctor(self, "my_magic_screen")
  self.root = self:AddChild(Widget("macroner:macro_list"))
  self.root:SetVAnchor(GLOBAL.ANCHOR_MIDDLE)
  self.root:SetHAnchor(GLOBAL.ANCHOR_MIDDLE)
end)

return myScreen

The _ctor is required, and the string value can be basically anything. I think it should be unique as it is some kind of screen ID, but I've never met a problem yet. Just in case I name it using mod name as a prefix, like macroner:myScreen. After all that, the self.root:AddChild is how you should be adding your UI elements. The Set*Anchor is basically where we are putting elements (relative to what). Here it's the center of the screen.

To show the screen, use GLOBAL.TheFrontEnd:PushScreen(myScreen) and to close it, use GLOBAL.TheFrontEnd:PopScreen().

Listening to mouse hover on inventory items

Putting most of our knowledge together, we can now write some code to listen to events in inventory bar.

AddClassPostConstruct("widgets/inventorybar", function(self)
  local refresh = self.Refresh
  function self:Refresh(...)
    attachSlotListeners(self.inv, "inv")
    attachSlotListeners(self.equip, "equip")
    attachSlotListeners(self.backpackinv, "backpackinv")
    return refresh and refresh(self, ...)
  end
  attachSlotListeners(self.inv, "inv")
  attachSlotListeners(self.equip, "equip")
  attachSlotListeners(self.backpackinv, "backpackinv")
end)

function attachSlotListeners(slots, key)
  if slots == nil then
    return
  end
  for slotKey, slot in pairs(slots) do
    if not slot.oldGainFocus then
      bindSlotFocus(slotKey, slot, key)
    end
  end
end

function bindSlotFocus(slotKey, slot, key)
  slot.oldGainFocus = slot.OnGainFocus
  slot.oldLoseFocus = slot.OnLoseFocus
  local idx = slot.num or slotKey
  slot.OnGainFocus = function(self, ...)
    print("Hovering slot", idx, "in", slotKey)
    if self.tile and self.tile.item then
      print("Item prefab is", slot.tile.item.prefab)
    end
    return self.oldGainFocus and self.oldGainFocus(self, ...)
  end
  slot.OnLoseFocus = function(self, ...)
    print("Leaving slot", idx, "in", slotKey)
    return self.oldLoseFocus and self.oldLoseFocus(self, ...)
  end
end

We have to "listen" to Refresh as, for example, equipping a backpack likes to basically create an inventory bar from scratch, removing our OnGainFocus and OnLoseFocus. Having old*Focus we can also restore old behavior by removing our binds when they aren't needed anymore:

function detachSlotListeners(slots)
  for _, slot in pairs(slots) do
    if slot.oldGainFocus then
      slot.OnGainFocus = slot.oldGainFocus
      slot.OnLoseFocus = slot.oldLoseFocus
      slot.oldGainFocus = nil
      slot.oldLoseFocus = nil
    end
  end
end

For other functions and fields available, feel free to investigate the unpacked scripts directory. In this case, widgets/inventorybar.lua for the entire bar and widgets/invslot.lua and/or widgets/itemslot.lua for slots.

In general, it seems that whatever you find in scripts you can call AddClassPostConstruct on it and then overwrite any method there is in that object. And on that note, considering we are importing widgets using things like

local Text = require "widgets/text"
local Image = require "widgets/image"

This suggests that basically anything you find in unpacked scripts/widgets - You can add to your screens or add in general to the UI.

Custom images

The game uses a custom texture format, something called the DXT file format? Not really sure and don't really care. The point is that you need a correct .tex file. Sometimes with an accompanying .xml file... There is some content about what the format is and how to create these things, like a page on wiki or threads on the forum. There's also a bunch of tooling for it, like official Klei tools (inactive for years), or a fork of official Klei tools (inactive for a bit less years), or special TEX file tools for DS (inactive for years), or Handsome Matt's tools (inactive for years and archived, telling you to use official tooling).

In the mentioned forum post, you can see a screenshoot of, I think, this app. Upon launching it, it doesn't show anything, doesn't have any way to add a mod to modify images, or anything like that. It seems to be a tool to publish your mod into Steam Workshop. If that's what you want, then sure, go ahead, but that's not what we are trying to do now. There's also some forum thread asking how to use it.

Getting things done

What finally worked for me was getting to Handsome Matt's repository, downloading the most recent release, running it with wine TEXCreator.exe and after exporting the PNG file, creating xml file using this guide.

If that's too much for you, then you can use game assets so the icon is not empty. To find TEX files, look around <DST path>/data/, e.g. in bigportraits or images. To see what you are trying to use, you may look around this repo, which conveniently has all, or at least lots of them, assets converted to PNG format. If XML is missing, then check the dimensions of the PNG file and use the XML content from the guide above used for your own icon.

Related projects