Revision Difference
File_Based_Storage#526759
<cat>Dev.Lua</cat>
<validate>This tutorial has several problems:
* Doesn't explain or provide alternatives to data serialization such as <page>util.TableToJSON</page>
* SteamID() during the <page>GM:ShutDown</page> hook can return an empty string and the code doesn't handle this
* Saving code doesn't explicitly define argument order which can cause data to write in the wrong order potentially resulting in data corruption
</validate>
# Introduction
Data Storage is what we use when we want data to persist through map changes and server restarts.
<note>Never trust the client with data. Only store, save, change, and load data from the server. If you require data on the client - Send it from the server</note>
For the sake of understanding the tutorial, imagine data storage as a table distribution of everything we want saved. Ex:
SteamID | Time Played | Deaths |
--------|--------------|--------|
| STEAM_0:0:000002 | 300 | 3 |
| STEAM_0:0:000003 | 80 | 1 |
| Etc... |
Each column holds a variable for a player. The first column is the key, how we differentiate different players (A player's SteamID is a good choice for the key as it's unique). And so each row represents the data of a single player
There are multiple ways of storing data in Garry's Mod. In this tutorial, we will go over File Based Storage
# About
Text files are a simple and effective tool that can be used to store data. File Based Storage does not require any external modules, it relies solely on the <page>file</page>, and it's pretty straightforward. For these reasons, it is the recommended method for beginners.
# Delimiter-separated values
The most common and simplest method of storing data in text files is to split the string using key-characters, separating each player with a key character, and the different variables stored with another key character.
We will now make a File Based Storage system that tracks the player's playing time on the server and their total deaths. Our key characters will be:
* **"\n"** - To separate between players. New line = new player
* **;** - To separate each stored variable.
Our file, that will hold the data (e.g. player_data.txt) will look something like this:
```
SteamID;Playtime;Deaths
SteamID;Playtime;Deaths
...
```
And with some values:
```
STEAM_0:0:0000001;10;0
STEAM_0:0:0000002;300;3
STEAM_0:0:0000003;80;2
```
The first thing we'll do is properly load the data from the text file when the server starts.
What we're doing is creating a table with SteamIDs as keys, and whose values are playtime and deaths
```
local PlayerData = {}
local function LoadPlayerData()
local data = file.Read( "player_data.txt", "DATA" ) -- Read the file
if not data then return end -- File doesn't exist
data = string.Split( data, "\n" ) -- Split the data into lines (Each line represents a palyer)
data = string.Split( data, "\n" ) -- Split the data into lines (Each line represents a player)
for _, line in ipairs( data ) do -- Iterate through each line
local args = string.Split( line, ";" ) -- Split the line into variables
if ( #args < 3 ) then continue end -- The data from the file contains unexpected amount of entries, skip the line
local id = args[1] -- Store the key variable, for comfortability's sake
if not id then return end -- Something is wrong
PlayerData[id] = {} -- Create the table
-- Update our PlayerData table
PlayerData[id].playtime = args[2]
PlayerData[id].deaths = args[3]
end
end
LoadPlayerData() -- Load the data once the server launches
```
Next, we will load the data of each player as they join.
```
hook.Add( "PlayerInitialSpawn", "LoadPlayerData", function( ply )
local sid = ply:SteamID()
local data = PlayerData[sid]
if data then -- If the player exists in the database, load his data
ply.playtime = data.playtime
ply.deaths = data.deaths
else -- If the player doesn't exist in the database, reset the data
ply.playtime = 0
ply.deaths = 0
end
end )
```
Lastly, update the data on the server whenever a player disconnects, and write it whenever the map changes or the server restarts.
```
local function SavePlayerData( ply )
local sid = ply:SteamID()
if not PlayerData[sid] then PlayerData[sid] = {} end
PlayerData[sid].playtime = ply.playtime
PlayerData[sid].deaths = ply.deaths
end
--[[
We only need to write the data into the file once - when the map changes or the server restarts
However, we need to update our PlayerData table (Which is a sorted copy of our text file) when a player disconnects
--]]
hook.Add( "PlayerDisconnected", "SavePlayerData1", SavePlayerData )
hook.Add( "ShutDown", "SavePlayerData2", function()
for _, ply in ipairs( player.GetAll() ) do
SavePlayerData( ply ) -- Save the data of all players currently connected
end
-- Lastly, write our txt file
local str = "" -- This is the string we will write to the file
for id, args in pairs( PlayerData ) do
str = str .. id -- The first stored value is the identifier
for _, arg in pairs( args ) do -- Add each stored variable in our table to the string
str = str .. ";" .. arg
end
str = str .. "\n" -- Before moving on to the next player, start a new line
end
file.Write( "player_data.txt", str )
end )
```
Of course, if we actually want these values to be correct, we should have a code that updates them
```
hook.Add( "PostPlayerDeath", "UpdatePlayerDataDeaths", function( ply )
ply.deaths = ply.deaths + 1
end )
timer.Create( "UpdatePlayerDataTime", 10, 0, function() -- Repeat every 10 seconds because accuracy isn't a necessity when counting playtime
for _, ply in ipairs( player.GetAll() ) do
ply.playtime = ply.playtime + 10 -- Again, accuracy isn't a necessity when counting playtime.
-- If we wanted to be accurate, we'd have to use The TimeConnected function for the first occurance
end
end )
```
The final product:
```
local PlayerData = {}
local function LoadPlayerData()
local data = file.Read( "player_data.txt", "DATA" )
if not data then return end
data = string.Split( data, "\n" )
for _, line in ipairs( data ) do
local args = string.Split( line, ";" )
if ( #args < 3 ) then continue end
local id = args[1]
if not id then return end
PlayerData[id] = {}
PlayerData[id].playtime = args[2]
PlayerData[id].deaths = args[3]
end
end
LoadPlayerData()
hook.Add( "PlayerInitialSpawn", "LoadPlayerData", function( ply )
local sid = ply:SteamID()
local data = PlayerData[sid]
if data then
ply.playtime = data.playtime
ply.deaths = data.deaths
else
ply.playtime = 0
ply.deaths = 0
end
end )
local function SavePlayerData( ply )
local sid = ply:SteamID()
if not PlayerData[sid] then PlayerData[sid] = {} end
PlayerData[sid].playtime = ply.playtime
PlayerData[sid].deaths = ply.deaths
end
hook.Add( "PlayerDisconnected", "SavePlayerData1", SavePlayerData )
hook.Add( "ShutDown", "SavePlayerData2", function()
for _, ply in ipairs( player.GetAll() ) do
SavePlayerData( ply )
end
local str = ""
for id, args in pairs( PlayerData ) do
str = str .. id
for _, arg in pairs( args ) do
str = str .. ";" .. arg
end
str = str .. "\n"
end
file.Write( "player_data.txt", str )
end )
hook.Add( "PostPlayerDeath", "UpdatePlayerDataDeaths", function( ply )
ply.deaths = ply.deaths + 1
end )
timer.Create( "UpdatePlayerDataTime", 10, 0, function()
for _, ply in ipairs( player.GetAll() ) do
ply.playtime = ply.playtime + 10
end
end )
```
<note>When using Delimiter-Separated Values, **do not save any data that the user can edit**. For example, in the above example, if a player with the username **;\n\n;;\n** joined the server, and we were to store his username, it would break the code</note>
<note>Alternatively, you can convert your string to a series of numbers, or a sterilized string that matches your key characters. However, it is recommended to avoid doing this, and use SQL instead</note>