Garry's Mod Wiki

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>