File Based Storage

From Garry's Mod
Jump to: navigation, search

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

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:000001 10 0
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 library, 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
		local id = args[1] -- Store the key variable, for comfortability's sake
		if not id then return end -- Something is wrong
		-- 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, ";" )
		local id = args[1]
		if not id then return end
		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

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

Personal tools
Navigation