Garry's Mod Wiki

File Based Storage

This tutorial has several problems:

  • Doesn't explain or provide alternatives to data serialization such as util.TableToJSON
  • SteamID() during the GM:ShutDown 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.

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

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 file, 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 --[[ 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 )
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
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

Page Links


Special Pages


Wikis

?

Render Time: 71ms

DB GetPage 8
Generate Html 32
SaveChanges 17
Render Body 0
Render Sidebar 12