Revision Difference
File_Based_Storage#515085
<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⤶
}}⤶
⤶
# 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 |⤶
| colspan="3" | 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)⤶
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⤶
⤶
--<page>⤶
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⤶
--</page>⤶
⤶
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>⤶
⤶
⤶