Garry's Mod Wiki

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>⤶ ⤶ ⤶