Skip to content

Storing Data⚓︎

MWSE offers various ways to store data persistently. Be at across save sessions for the same character or even persistently across different player characters. To store data that is meant to persist between different characters, you can use provided json API. It provides a set of functions needed to save Lua's values to json files and load the data to Lua's tables. This process is called serialization. One requirement is that the value can be serialized. Even functions used to work with configuration files mwse.saveConfig and mwse.loadConfig are implemented with json API.

Serialization⚓︎

Serializible values are Lua's primitive types:

  • boolean
  • number
  • string
  • table's made of above values

You can't serialize MWSE classes such as tes3reference, tes3mobilePlayer, etc.

Persistent Storage for the Same Player Character⚓︎

Besides saving your data to files, you can save your data to some of the MWSE's classes that have a data or tempData property. Those are:

  • tes3combatSession.data
  • tes3itemData.data
  • tes3reference.data
  • tes3itemData.tempData
  • tes3reference.tempData

Note

Both data and tempData tables can only be used to store serializible data.

Data stored in the data table on a certain object will persist between savegame sessions, while data stored in tempData table will be cleared on game reload. There are some peculiarities when working with these tables:

  • Each of the table fields needs to be declared one by one.
  • Not every object can have Lua data. You can check that with myRef.supportsLuaData property.
  • The data field on a tes3reference won't persist between savegames if the reference isn't marked as modified (NPCs and creatures are usually marked as modified by other parts of the engine). Make sure to mark your reference as modified explicitly: myRef.modified = true

Example: creating a table inside data table on the player's reference

-- Correct way
tes3.player.data.myMod = {}
tes3.player.data.myMod.var1 = {}
tes3.player.data.myMod.var2 = 32

-- Wrong way, this would rise an error
tes3.player.data.myMod = {
    var1 = {},
    var2 = 32
}


-- On the other hand we can save our created
-- table to a local variable for easy access
-- Let's create our table first
tes3.player.data.myMod = {}

-- Now let's save it to a local variable
local myData = tes3.player.data.myMod

-- Let's save something now
myData.var1 = {}
myData.var2 = 32

The field data on the player's reference could be the perfect way to store some mod-related player statistics. For example, a mod implementing a karma system could save the player's current karma level inside that table, so that it can persist between save sessions. On the other hand, the data that the mod's user would want to persist between whichever character the player or savegame is played would be saved to a configuration file instead. For instance, keybindings for new abilities are a good candidate for that.

Usage of data table⚓︎

Here an example of a simple mod is given which stores some variables to tes3.player.data and tes3.player.tempData.

local shrineIds = {
    ["fields of kummu"] = true,
    ["vivec, temple"] = true,
    ["vivec, puzzle canal, center"] = true,
    ["gnisis, temple"] = true,
    ["koal cave"] = true,
    ["ghostgate, temple"] = true
}
local regions = {
    ["balmora"] = true,
    ["seyda neen"] = true,
    ["ald-ruhn"] = true
}

-- This is the default layout for the table
-- stored on tes3.player.data
---@class myData
local defaults = {
    regionalBounties = {
        -- This table is unused in this example, but
        -- it shows how to  initialize a sub-table.
        ["balmora"] = 0,
        ["seyda neen"] = 0,
        ["ald-ruhn"] = 0
    },
    karma = 0,
    shrinesVisited = {}
}

--- This function will recursively set all the fields on our
--- tes3.player.data table if they don't exist already
---@param data table
---@param t table
local function initTableValues(data, t)
    for k, v in pairs(t) do
        -- If a field already exists - we initialized the data
        -- table for this character before. Don't do anything.
        if data[k] == nil then
            if type(v) ~= "table" then
                data[k] = v
            elseif v == {} then
                data[k] = {}
            else
                -- Fill out the sub-tables
                data[k] = {}
                initTableValues(data[k], v)
            end
        end
    end
end

--- This is a standard function that will create
--- a table for our mod's storage in tes3.player.data
local function initializeData()
    local data = tes3.player.data
    data.myMod = data.myMod or {}
    local myData = data.myMod
    initTableValues(myData, defaults)
end
event.register(tes3.event.loaded, initializeData)

--- This is a convinience function to get our storage
---@return myData
local function getData()
    return tes3.player.data.myMod
end

--- This function will handle updating the player's Current
--- karma level, and handle if it passes over -100 or 100
---@param delta integer
local function modKarma(delta)
    local myData = getData()
    local oldKarma = myData.karma
    myData.karma = myData.karma + delta

    local karma = myData.karma
    local absOldKarma = math.abs(oldKarma)
    local absKarma = math.abs(karma)
    local barrierCrossed = ((absOldKarma >= 100) and (absKarma < 100)) or
                           ((absKarma >= 100) and (absOldKarma < 100))

    if barrierCrossed then
        -- Let's log to see what's happening
        tes3.messageBox("Current karma: %s", karma)
        if karma < -100 then
            tes3.messageBox("Because of your deeds your karma now reached evil level.")

            -- Now let's store that to tes3.player.tempData for later use
            -- We can use table.getset to create a table for our mod in
            -- tes3.player.tempData if it doesn't exist yet.
            local temp = table.getset(tes3.player.tempData, "myMod", {})

            -- Now actually store player's karma range
            temp.encounter = "bad"

        elseif karma < 100 then
            -- -100 < karma < 100
            tes3.messageBox("Because of your deeds your karma now reached neutral level.")
            local temp = table.getset(tes3.player.tempData, "myMod", {})
            temp.encounter = "neutral"

        elseif karma >= 100 then
            tes3.messageBox("Through your deeds your karma now reached good level.")
            local temp = table.getset(tes3.player.tempData, "myMod", {})
            temp.encounter = "good"
        end
    end
end

---@param e cellChangedEventData
local function onCellChange(e)
    local cell = e.cell
    local cellId = cell.id:lower()

    -- Check if visited cell is on
    -- the list of valid shrines
    if shrineIds[cellId] then
        local data = getData()

        -- Make sure player didn't already visit this shrine,
        -- not to award the karma boost twice.
        if not data.shrinesVisited[cellId] then
            -- Player visited this shrine for the first time,
            -- award some karma points.
            modKarma(50)

            -- Let's save that to the list of visited shrines in
            -- a table on tes3.player.data, so it can persist
            -- between savegames.
            data.shrinesVisited[cellId] = true
        end
    end
end
event.register(tes3.event.cellChanged, onCellChange)


---@param e lockPickEventData|trapDisarmEventData
local function onPick(e)
    local doorReference = e.reference
    local hasAccess = tes3.hasOwnershipAccess({ target = doorReference })

    if not hasAccess then
        tes3.messageBox("That isn't allowed!")
        modKarma(-25)
        local cellId = tes3.player.cell.id:lower()
        if regions[cellId] then
            local myData = getData()
            myData.regionalBounties[cellId] = myData.regionalBounties[cellId] + 25
        end

    end
end
event.register(tes3.event.lockPick, onPick)
event.register(tes3.event.trapDisarm, onPick)

---@param e calcRestInterruptEventData
local function onCalcRestInterrupt(e)
    local temp = tes3.player.tempData.myMod
    if not temp then return end

    local roll = math.random(100)
    if roll > 60 then return end

    local encounterType = temp.encounter
    if encounterType == "good" then
        -- The player has good karma, let's block
        -- the rest interruption
        e.count = 0

        -- Log what happened. This kind of message wouldn't
        -- be in the final release of the mod.
        tes3.messageBox("Blocking encounter!")
    elseif encounterType == "neutral" then
        local roll = math.random(100)
        if roll < 50 then
            e.count = 0
            tes3.messageBox("Blocking encounter!")
        end
    else
        -- Player is a bad guy, let's increase the amount
        -- of spawned creatures by a random number.
        local mod = math.random(10)
        e.count = e.count + mod
        tes3.messageBox("Increased the spawned enemy count!")
    end
end
event.register(tes3.event.calcRestInterrupt, onCalcRestInterrupt)