Coding your mod

Right, you’ve got everything set up, you’ve created your main.lua file, and you’re ready to code.

Here’s the full source code for our “Subnautica 2 Cheat” mod. We’ll walk through each section below:

---@type table
local UEHelpers = require("UEHelpers")

---@type string
local MOD_NAME = "BeginnersGuideCheatMod"

---@type boolean
local debugMode = false

-- Simple helper function to log messages with the mod name as a prefix
---@param msg string
local function log(msg)
    print(string.format("[%s] %s\n", MOD_NAME, msg))
end

-- Hold a reference to the player's attribute set so we can modify it each tick
---@type UUWESurvivalAttributeSet|nil
local attrSet = nil

-- Function to find the player's survival attribute set in memory
---@return UUWESurvivalAttributeSet|nil
local function findPlayerAttrSet()
    ---@type UUWESurvivalAttributeSet[]|nil
    local all = FindAllOf("UWESurvivalAttributeSet")
    if not all then return nil end
    for _, set in ipairs(all) do
        if set:IsValid() and set:GetFullName():find("BP_Character_01_C") then
            return set
        end
    end
    return nil
end

-- When the player spawns, find and set the survival attribute set
NotifyOnNewObject("/Game/Blueprints/Character/player/BP_Character_01.BP_Character_01_C", function()
    ExecuteInGameThread(function()
        attrSet = nil
        attrSet = findPlayerAttrSet()
        if attrSet then
            log("Player character created - survival attributes found! Infinite Oxygen, Food, and Water enabled!")
        end
    end)
end)

-- Loop every 500ms to keep oxygen, food, and water at their maximum values
LoopAsync(500, function()
    ExecuteInGameThread(function()
        if not attrSet or not attrSet:IsValid() then
            -- If we're in debug mode, don't wait for the player to respawn. This allows us to hot reload the mod
            -- by setting debugMode to true
            if(debugMode) then
                log("Survival attribute set not found or invalid. Attempting to find it again...")
                attrSet = findPlayerAttrSet()
            end
            return
        end

        -- Oxygen
        ---@type number
        local maxOxygen = attrSet.MaxOxygen.CurrentValue
        attrSet.Oxygen.BaseValue = maxOxygen
        attrSet.Oxygen.CurrentValue = maxOxygen

        -- Food
        ---@type number
        local maxFood = attrSet.MaxFood.CurrentValue
        attrSet.Food.BaseValue = maxFood
        attrSet.Food.CurrentValue = maxFood

        -- Water
        ---@type number
        local maxWater = attrSet.MaxWater.CurrentValue
        attrSet.Water.BaseValue = maxWater
        attrSet.Water.CurrentValue = maxWater
    end)
    return false
end)

log("Loaded!")

Annotations

You’ll see in the code that we “annotate” the local variable declarations and function parameters and return values. Annotations are those lines prefixed by “—”. For example:

-- Hold a reference to the player's attribute set so we can modify it each tick
---@type UUWESurvivalAttributeSet|nil
local attrSet = nil

Annotations are just there to help VS Code identify the various types and classes. When you explicitly specify the type or class of a variable or function, via an annotation, VS Code can highlight possible issues and incompatibilities, as well as offer code completion suggestions as you’re developing your mod. It’s well worth using annotation wherever possible, as it will just make your life easier, and save you lots of potential headaches in the future.

Setup and logging

---@type table
local UEHelpers = require("UEHelpers")

---@type string
local MOD_NAME = "BeginnersGuideCheatMod"

---@type boolean
local debugMode = true

-- Simple helper function to log messages with the mod name as a prefix
---@param msg string
local function log(msg)
    print(string.format("[%s] %s\n", MOD_NAME, msg))
end

UEHelpers is a utility library bundled with UE4SS that provides some handy convenience functions. We’re not using it a lot here, but it’s good practice to include it. You can see what functions it offers in the UE4SS GitHub repository. Our little log helper simply prefixes our print output with the mod name, which makes it easy to spot our messages in the UE4SS console.

Having a debugMode is really useful. You can refer to this in your code to add additional functionality and logging that you use while you’re developing the mod. For example, you might log additional information or debug messages, or you might skip or add specific functionality when debugging is enabled. You can quickly change this between true and false as you’re working. Just remember to ship your mod with debugMode set to false.

Finding the player’s attribute set

-- Hold a reference to the player's attribute set so we can modify it each tick
---@type UUWESurvivalAttributeSet|nil
local attrSet = nil

-- Function to find the player's survival attribute set in memory
---@return UUWESurvivalAttributeSet|nil
local function findPlayerAttrSet()
    ---@type UUWESurvivalAttributeSet[]|nil
    local all = FindAllOf("UWESurvivalAttributeSet")
    if not all then return nil end
    for _, set in ipairs(all) do
        if set:IsValid() and set:GetFullName():find("BP_Character_01_C") then
            return set
        end
    end
    return nil
end

This is where our earlier investigation pays off. We know from the UE4SS Live View that the class we want is UWESurvivalAttributeSet, and we know it lives on “BP_Character_01_C”. We also know that there may be multiple instances of UWESurvivalAttributeSet in the game world, so we can’t just grab the first one - we filter by “BP_Character_01_C” in the full name to make sure we get the player’s instance specifically.

The attrSet variable is declared outside the function so we can hold onto the reference once we’ve found it, rather than searching on every poll.

The for _, set in ipairs(all) do construct is the Lua equivalent of a for each loop, that you might be familiar with from other languages. It allows you to iterate over lists or arrays, though be aware that those are referred to in Lua speak as “tables”!

Finding the reference on spawn

-- When the player spawns, find and set the survival attribute set
NotifyOnNewObject("/Game/Blueprints/Character/player/BP_Character_01.BP_Character_01_C", function()
    ExecuteInGameThread(function()
        attrSet = nil
        attrSet = findPlayerAttrSet()
        if attrSet then
            log("Player character created - survival attributes found! Infinite Oxygen, Food, and Water enabled!")
        end
    end)
end)

Rather than searching for the attribute set on every loop tick, we use NotifyOnNewObject() to watch for the player character being created. This fires both on initial spawn and on respawn after death, which means we always have a fresh reference. We explicitly set attrSet to nil before re-acquiring it to ensure we’re never holding a stale reference to an old instance.

The asset path ““/Game/Blueprints/Character/player/BP_Character_01.BP_Character_01_C” is the “concrete” player character class that we identified in FModel. This is distinct from the base BP_SN2PlayerCharacter class in Blueprints/Core. Using the concrete class ensures the callback fires at the right point in the character’s initialisation.

Polling and updating the attributes

-- Loop every 500ms to keep oxygen, food, and water at their maximum values
LoopAsync(500, function()
    ExecuteInGameThread(function()
        if not attrSet or not attrSet:IsValid() then
            -- If we're in debug mode, don't wait for the player to respawn. This allows us to hot reload the mod
            -- by setting debugMode to true
            if(debugMode) then
                log("Survival attribute set not found or invalid. Attempting to find it again...")
                attrSet = findPlayerAttrSet()
            end
            return
        end

        -- Oxygen
        ---@type number
        local maxOxygen = attrSet.MaxOxygen.CurrentValue
        attrSet.Oxygen.BaseValue = maxOxygen
        attrSet.Oxygen.CurrentValue = maxOxygen

        -- Food
        ---@type number
        local maxFood = attrSet.MaxFood.CurrentValue
        attrSet.Food.BaseValue = maxFood
        attrSet.Food.CurrentValue = maxFood

        -- Water
        ---@type number
        local maxWater = attrSet.MaxWater.CurrentValue
        attrSet.Water.BaseValue = maxWater
        attrSet.Water.CurrentValue = maxWater
    end)
    return false
end)

This loop runs every 500 milliseconds, and you can obviously tweak that to whatever poll frequency you want. On each “tick” it first checks whether we have a valid reference to the attribute set - if not, do nothing. We’re waiting at this point for the player to spawn and for the survival attributes component to be found. Once found, it reads the current max value for each attribute and sets both BaseValue and CurrentValue to that max. We set both because Unreal’s Gameplay Ability System maintains these separately, and setting only one may not produce the result you expect.

Testing the mod

Save main.lua and launch the game. UE4SS will load your mod automatically on startup. Watch the UE4SS console - you should see:

[Subnautica2CheatMod] Loaded!

And once you’re in game and the player character is initialised:

[Subnautica2CheatMod] Player character created - survival attributes found! Infinite Oxygen, Food, and Water enabled!

If you make changes to main.lua while the game is running, you can reload all mods without restarting by pressing Ctrl+R while the game window has focus. Note that this won’t work if focus is on the UE4SS console window - click back into the game first. This is called “hot reload” and is the most amazing thing I’ve ever come across, especially having spent most of my time working with Unity mods! It makes tweaking and testing so much quicker, it is, quite literally, unreal!

Note

When you make a change then “hot reload”, you may notice your changes aren’t taking effect. Remember that our particular mod is effectively “triggered” by the player object being created. So you typically would need to quit and reload to see your changes. We’ve accomodated for this by checking the debugMode variable in the loop. So if you’re testing with hot loading, don’t forget to set debugMode to true to force the mod to find the SurvivalAttributeSet each time.

Building on this pattern

You’ll see a pattern here:

  • Watch for object creation with NotifyOnNewObject
  • Find a class with FindAllOf
  • Filter to the right instance
  • Hold a reference
  • Poll to keep values updated

It’s a pretty handy pattern to keep in mind, and you might come across it in other mods you see and build yourself. Once you’re comfortable with it, you can apply it to almost anything you find through Live View and the Lua types. The survival attributes are a good example, but the same approach works for health, energy, temperature, or any other attribute set in the game.


This site uses Just the Docs, a documentation theme for Jekyll.