Skip to content

Saving data

Config files are for settings that the server owner changes. Saving data is for things your plugin remembers by itself, like how many times each player has joined. In this lesson you'll learn how to open a data file, save and load values, and update old saved data when your plugin changes. You'll finish with a plugin that counts every player's joins and remembers them after a restart.

Config and storage

A file you read and write from your plugin is called a storage object, and it works a lot like config, but the names are a bit different. config has setDefault, set and clear, and a storage object has setDefaultValue, setValue and clearValue. Also, config is called with a dot, like config.set(...), while a storage object is called with a colon, like data:setValue(...).

A storage object can open any YAML file, so you can also use one for an extra config file, like messages.yml. Just don't open config.yml that way, because config already looks after it.

Part 1: Save and read a value

To store something, first open a file. Add this to your main.lua:

main.lua
local data = plugin.getStorageObject("data.yml")

plugin.registerEvent("PlayerJoinEvent", function(event)
    local total = data:getValue("total-joins") or 0
    total = total + 1

    data:setValue("total-joins", total)
    data:save()

    event:getPlayer():sendMessage("The server has had " .. total .. " joins!")
end)

plugin.getStorageObject("data.yml") opens a file called data.yml in your plugin's folder, and creates it if it isn't there yet. The ending of the name matters. Use .yml, which is easy to read and edit.

data:getValue("total-joins") reads a value. The first time, nothing has been saved yet, so it gives back nil. That's what or 0 is for: it means "use the saved number, and if there's nothing saved, use 0". It only picks a fallback for that one line and doesn't save anything. The saving happens afterwards, with setValue and save. Then we add one, and data:setValue stores the new number. That only changes it in memory, so data:save() writes it to the file. Without save, the number is lost when the server restarts.

Try joining a few times and then look at plugins/YourPluginName/data.yml.

Part 2: Save data for each player

One counter for the whole server isn't very useful, so let's give every player their own. To tell players apart we use their unique ID, which never changes, even if they change their name. Replace your join event with this:

main.lua
local data = plugin.getStorageObject("data.yml")

plugin.registerEvent("PlayerJoinEvent", function(event)
    local player = event:getPlayer()
    local key = "joins." .. tostring(player:getUniqueId())
    local joins = (data:getValue(key) or 0) + 1

    data:setValue(key, joins)
    data:save()

    player:sendMessage("Welcome back, " .. player:getName() .. "! This is join number " .. joins .. ".")
end)

Here's what each line does:

  • local player = event:getPlayer() is the player who just joined.
  • local key = ... makes the label that this player's count is saved under. Every player needs their own label, so we build one from their unique ID, a long code like 0b2f4c1e-... that belongs to one player forever. tostring turns the ID into text, so it can be glued onto "joins.", which gives a label like joins.0b2f4c1e-.... The dot groups every player's count under one joins: heading in the file.
  • local joins = (data:getValue(key) or 0) + 1 works out the new count. It reads this player's saved count, or 0 if they've never joined, and then adds one. So the first time it's 0 + 1, which is 1, and the next time it's 1 + 1, which is 2. The brackets make sure the reading happens before the + 1.
  • data:setValue(key, joins) remembers the new count, and data:save() writes it to the file.
  • The last line sends the player a message with their count.

Join a few times and check that the number goes up, then restart the server and join again to see that it remembers.

Part 3: Look up and remove data

Let's add two commands. The first shows your count, and the second resets it. Add them below the join event:

main.lua
plugin.addCommand({
    name = "joins",
    description = "See how many times you have joined",
    usage = "/joins"
}, function(event)
    local sender = event.getSender()

    if event.isPlayerSender() then
        local key = "joins." .. tostring(sender:getUniqueId())
        local joins = data:getValue(key) or 0
        sender:sendMessage("You have joined " .. joins .. " time(s).")
    else
        sender:sendMessage("Only players have a join count.")
    end
end)

plugin.addCommand({
    name = "resetjoins",
    description = "Reset your join count",
    usage = "/resetjoins"
}, function(event)
    local sender = event.getSender()

    if event.isPlayerSender() then
        local key = "joins." .. tostring(sender:getUniqueId())
        local removed = data:clearValue(key)

        if removed then
            data:save()
            sender:sendMessage("Your join count was reset.")
        else
            sender:sendMessage("You didn't have a join count to reset.")
        end
    else
        sender:sendMessage("Only players have a join count.")
    end
end)

Both commands start by making the player's key, the same way as in the join event, and /joins uses it to read the count. /resetjoins uses data:clearValue(key), which removes the saved count and tells you whether there was one to remove. It gives back true if it removed something and false if there was nothing there. We save that answer as removed, and the if uses it to pick the right message.

If you want to check a value without reading it, data:exists(key) gives back true or false.

Part 4: Update old data

Just like with config, your plugin will change, and the data players have already built up shouldn't be lost. Saved data you read with or, like (data:getValue(key) or 0), already copes with something that's new, because an old file simply doesn't have it yet and the or supplies a starting value.

Moving data takes a bit more. Say version 1 of your plugin saved the server count as total-joins, and version 2 wants it under stats.total-joins. Add this near the top of your main.lua, right after you open the file:

main.lua
local data = plugin.getStorageObject("data.yml")

-- Version 2 moved "total-joins" to "stats.total-joins"
if (data:getValue("data-version") or 1) < 2 then
    local old = data:getValue("total-joins")
    if old then
        data:setValue("stats.total-joins", old)
        data:clearValue("total-joins")
    end
    data:setValue("data-version", 2)
    data:save()
end

Old data has no data-version, so or 1 treats it as version 1, and it goes into the if. If the old total-joins value is there, we copy it to the new place and remove the old one. Then we mark the file as version 2 and save, so this only ever runs once. New data skips the copy, since there's nothing to move.

Unlike config.getValue, a storage object's getValue gives you a real nil when nothing is saved, so if old then works.

Next time you change the layout, bump the number and add another if below this one.

Putting it together

Combined, your main.lua is a join counter that every player gets their own count in:

main.lua
local data = plugin.getStorageObject("data.yml")

plugin.registerEvent("PlayerJoinEvent", function(event)
    local player = event:getPlayer()
    local key = "joins." .. tostring(player:getUniqueId())
    local joins = (data:getValue(key) or 0) + 1

    data:setValue(key, joins)
    data:save()

    event:setJoinMessage(player:getName() .. " joined the game! (join number " .. joins .. ")")
end)

plugin.addCommand({
    name = "joins",
    description = "See how many times you have joined",
    usage = "/joins"
}, function(event)
    local sender = event.getSender()

    if event.isPlayerSender() then
        local key = "joins." .. tostring(sender:getUniqueId())
        local joins = data:getValue(key) or 0
        sender:sendMessage("You have joined " .. joins .. " time(s).")
    else
        sender:sendMessage("Only players have a join count.")
    end
end)

plugin.addCommand({
    name = "resetjoins",
    description = "Reset your join count",
    usage = "/resetjoins"
}, function(event)
    local sender = event.getSender()

    if event.isPlayerSender() then
        local key = "joins." .. tostring(sender:getUniqueId())
        local removed = data:clearValue(key)

        if removed then
            data:save()
            sender:sendMessage("Your join count was reset.")
        else
            sender:sendMessage("You didn't have a join count to reset.")
        end
    else
        sender:sendMessage("Only players have a join count.")
    end
end)

One thing changed in the join event. Instead of sending a message to just that player, event:setJoinMessage(...) changes the message everyone sees when someone joins, so the server announces "Steve joined the game! (join number 3)".

Try these:

  • Join the server a few times and watch the number go up.
  • Restart the server and join again, and the count carries on.
  • Type /joins to see your count.
  • Type /resetjoins, then /joins to see it start from zero.

Quick reference

These are the things you can do with a storage object. Call them with a colon, like data:save(). Values can be text, numbers, true or false, or tables.

Call What it does
plugin.getStorageObject(file) Opens a .yml file in your plugin's folder, and creates it if it isn't there
data:getValue(name) Gives back a saved value, or nil if nothing is saved
data:setValue(name, value) Changes a value, which isn't written to the file until you save
data:setDefaultValue(name, value) Sets a value only if it's missing, and gives back true if it did
data:exists(name) Gives back true if something is saved there
data:clearValue(name) Removes a value, and gives back true if there was one
data:save() Writes your changes to the file

That's the end of Basics for now. You now know how to make commands, react to events, send colorful messages, work with players, read the Spigot docs, and keep settings and data. More is coming in Advanced.