Inventories: How do you make and secure them?

Introduction

For the sake of my sanity, this tutorial will only go over the server side
Whenever I log on to the Developer Forums to see what new creations are being released I almost always see a notification pop up, what is it? It’s a heart on one of my posts about how I create inventories. I did a few quick searches to find out that nobody has a real tutorial on how to make a secure and easily manageable inventory.

This tutorial will be broken in to two sections, a intermediate level as well as a hard level. The intermediate level will be using intermediate level scripting, you won’t be required to use any fancy metamethods, etc… But as usual this does cost some convenience. The hard level will use an Object Orientated Style of inventory, I highly recommend actually looking in to the code and reading it, if you don’t understand it then you should take some time to figure out what everything does beyond the documentation that I put. Please don’t just copy and paste the code because that doesn’t help anyone.

Security

Not only will this tutorial be focusing on how you can make an inventory, but how you can secure it. The last thing you want as a developer is your games economy being stripped down and becoming useless because of an exploiter who can manipulate his/her inventory.

I know what you’re thinking, you can just add sanity checks on the server! While this is true, and sanity checks are what we’ll be focusing on, you need to also keep in mind as to how you’re giving players items. For example, if a user does a quest and then in return they earn 5 gold, you should also make sure that the quest can’t be manipulated in any way to become farmable via exploits. At the bottom of this page is a list of do’s and dont’s, I highly recommend looking in to them and making sure you’re being as secure as possible.

Intermediate Level

The intermediate level will require knowledge of the following.

  • Tables
  • Modules (Optional)

For the sake of this tutorial not being a book, we’ll be using one item for testing, the item will be referred to as an axe.

Let’s hop right in to how you can make your first ever inventory! The first thing we’ll want to do is figure out how we want our inventory to function. For our case I’ll be using a module script with two tables, one table will store our cached inventories, the other will store the inventory functions.

local Inventories = {} -- Our stored/cached inventories.
local Inventory = {} -- The table with our inventory functions

return Inventory -- Module script, return functions

Boom! Now we have two tables and they’ll work as they should in our module script.

Creating an inventory

Next up we need to implement a way to actually create an inventory. We’ll make a new function called “new”, this will create a new inventory!

We want to make sure that the arguments passed to our function are valid, to do this we add a check to make sure they’re not nil.

function Inventory.new(player, contents) -- Hey! Our new function, it takes a player and contents.
	if not player then
		return -- The player that we specified doesn't exist.
	end
end

Cool! We have a function set up to create an inventory, but truth be told… It does nothing right now! So, how do we fix that? Well the way we have it set up right now is that it’ll accept an argument called contents. In a perfect world “contents” would always be a table. However this isn’t a perfect world. Assuming you’re sending something from a datastore and contents is nil, we’ll need to make a new inventory for the user.

So what do we do? We check to see if contents is a nil value or not, if it is then make the user a new, empty, inventory. Or in our case give the user one axe. For this tutorial the number next to the item name will be the count of how many of that item a user has.

function Inventory.new(player, contents) -- Hey! Our new function, it takes a player and contents.
	if not player then
		return -- The player that we specified doesn't exist.
	end
	-- alternatively you could do "contents = contents or {}", but for the sake of this tutorial we won't be doing that.
	if not contents then -- We have no contents, let's make some.
		contents = { -- Setting our contents to a table
			["Axe"] = 1, -- Hey! The user has one axe. If you don't want the user to have any items by default then simply remove this line.
		}
	end
end

Believe it or not the hard part is pretty much over. We now have contents of an inventory as well as the player. From here on out its just manipulating the users contents to give or remove items from them! Simple right?

So from here we’ll put our users inventory inside of our cached inventories (we don’t want to make an inventory each time so we do this) Then we’ll return the inventory.

Inventories[player] = contents
return Inventories[player]

Boom! We’ve successfully created a new inventory with whatever contents it is that you need. For our purposes I won’t be going over using DataStores so your players save items. There is a really good tutorial which can be found here about that.

So? Our entire function looks like this.

function Inventory.new(player, contents) -- Hey! Our new function, it takes a player and contents.
	if not player then
		return -- The player that we specified doesn't exist.
	end
	if not contents then -- We have no contents, let's make some.
		contents = { -- Setting our contents to a table
			["Axe"] = 1, -- Hey! The user has one axe.
		}
	end
	Inventories[player] = contents -- Assign the users contents to our cache
	return Inventories[player] -- return the inventory, (semi useless b/c its just content but still)
end

Getting our players inventory

We know how to make an inventory which is great, but what if we want to get the contents of the inventory (we could easily change values this way or display the items, etc…) Well it’s simple. We just make a function called get… and return the users cached table.

function Inventory:Get(player)
	if not player then
		return -- Not going over this because I already did in the "new" function
	end
	return Inventories[player] -- Return the cached inventory!
end

This will either return nil, if the inventory doesn’t exist, or it’ll return the contents of the inventory (in a table form)

Adding items to our inventory

Now… Creating inventories is really helpful, but you can’t do much without being able to add items to an inventory, etc… How do we add an item to an inventory? It’s simple, we get the users inventory and then we manipulate the index.

We’ll want the player, the name of the item to add, and the amount of that item we’re adding. Because we love being fancy we’ll make the amount argument optional.

The first thing to do is create the function and get the users inventory, which can be seen below. We also want to make sure the user has an inventory to add an item to, if they don’t then return.

function Inventory:Add(player, name, amount)
	if not player or not name then
		return -- Not going over this because I already did in the "new" function
	end
	local inventory = Inventory:Get(player)
	if not inventory then
		return -- The user doesn't have an inventory, we can't add an item
	end
end

Now that we’ve recieved the inventory we want to add an item to it, it’s not as easy as it seems but it’s still rather easy. We need to check first if the user already has whatever item you’re trying to give them. If they do then simply add one to the count, if not then we’ll put the item in the users inventory.

if inventory[name] then -- Check if the user has the item you're giving
	inventory[name] = inventory[name] + amount or 1 -- They do, just add the amount or 1 
else
	inventory[name] = amount or 1 -- They don't, give them the amount of items or 1
end

And just like that we have a function which will successfully add an item to the users inventory. Simple! The entire function is as follows.

function Inventory:Add(player, name, amount)
	if not player or not name then
		return -- Not going over this because I already did in the "new" function
	end
	local inventory = Inventory:Get(player)
	if not inventory then
		return -- The user doesn't have an inventory, we can't add an item
	end
	if inventory[name] then -- Check if the user has the item you're giving
		inventory[name] = inventory[name] + amount or 1 -- They do, just add the amount or 1 
	else
		inventory[name] = amount or 1 -- They don't, give them the amount of items or 1
	end
end

Removing items from our inventory

I’m going to make this short and simple as a lot of it is the same as the adding. The only difference is that we want to make sure we don’t remove items in to the negatives. How do we do that? We add a check to make sure that if we subtract the amount given it won’t be less then 0, if it is then we remove the item from the inventory completely.

if inventory[name] then -- Check if the user has the item you're giving
	if inventory[name] - (amount or 1) < 0 then -- Check if the amount your removing is more then the user has
		inventory[name] = nil -- Delete the item completely
	else 
		inventory[name] = inventory[name] - amount or 1 -- Remove the amount or one item.
	end
end

The entire code snippet can be found here.

function Inventory:Remove(player, name, amount)
	if not player or not name then
		return -- Not going over this because I already did in the "new" function
	end
	local inventory = Inventory:Get(player)
	if not inventory then
		return -- The user doesn't have an inventory, we can't add an item
	end
	if inventory[name] then -- Check if the user has the item you're giving
		if inventory[name] - (amount or 1) < 0 then -- Check if the amount your removing is more then the user has
			inventory[name] = nil -- Delete the item completely
		else 
			inventory[name] = inventory[name] - amount or 1 -- Remove the amount or one item.
		end
	end
end

Usage
Congratulations you’ve now made a pretty simple inventory system. But how do you use it? I scripted mine in a modulescript, therefor you’ll need to require that module script and then use go from there.

Here are some code samples below.

local inventory_module = require(script.Inventory)
local Players = game:GetService("Players")

function addItem(player, name, amount)
	inventory_module:Add(player, name, amount)
end

function removeItem(player, name, amounnt)
	inventory_module:Remove(player, name, amounnt)
end

Players.PlayerAdded:Connect(function(player)
	inventory_module.new(player)
	
	wait(3)
	
	addItem(player, "Axe", 2) -- or addItem(player, "Axe")
	
	wait(3)
	
	removeItem(player, "Axe", 1)
	
	print(inventory_module:Get(player).Axe) -- Prints 1
end)

Entire Intermediate Product

local Inventories = {}
local Inventory = {}

function Inventory:Remove(player, name, amount)
	if not player or not name then
		return -- Not going over this because I already did in the "new" function
	end
	local inventory = Inventory:Get(player)
	if not inventory then
		return -- The user doesn't have an inventory, we can't add an item
	end
	if inventory[name] then -- Check if the user has the item you're giving
		if inventory[name] - (amount or 1) < 0 then -- Check if the amount your removing is more then the user has
			inventory[name] = nil -- Delete the item completely
		else 
			inventory[name] = inventory[name] - amount or 1 -- Remove the amount or one item.
		end
	end
end

function Inventory:Add(player, name, amount)
	if not player or not name then
		return -- Not going over this because I already did in the "new" function
	end
	local inventory = Inventory:Get(player)
	if not inventory then
		return -- The user doesn't have an inventory, we can't add an item
	end
	if inventory[name] then -- Check if the user has the item you're giving
		inventory[name] = inventory[name] + amount or 1 -- They do, just add the amount or 1 
	else
		inventory[name] = amount or 1 -- They don't, give them the amount of items or 1
	end
end

function Inventory:Get(player)
	if not player then
		return -- Not going over this because I already did in the "new" function
	end
	return Inventories[player] -- Return the cached inventory!
end

function Inventory.new(player, contents) -- Hey! Our new function, it takes a player and contents.
	if not player then
		return -- The player that we specified doesn't exist.
	end
	if not contents then -- We have no contents, let's make some.
		contents = { -- Setting our contents to a table
			["Axe"] = 1, -- Hey! The user has one axe.
		}
	end
	Inventories[player] = contents -- Assign the users contents to our cache
	return Inventories[player] -- return the inventory, (semi useless b/c its just content but still)
end

return Inventory

Hard Level

The intermediate level will require knowledge of the following.

  • Tables
  • Modules
  • Metatables

For the sake of this tutorial not restricting you, you’ll be required to figure out how you want your items stored.

Now I say this is difficult, but it’s really not, you just need to take it slow and problem solve it. If you don’t understand the required knowledge then don’t get frustrated, I recommend using the resources available to you and learn it.

With that being said, I’m not going to do baby steps and explain everything, this is meant to be a tutorial but you also are meant to have a good knowledge of the list above!

Alright, so this will be scripted in a modulescript. I would recommend making a Framework for the best use however how you handle it is up to you. We’ll start off by creating a function which will create a new inventory.

Our setup will be as follows.

local Inventory = {}

function Inventory.new()
	
end

return Inventory

Inside of we’ll make a new metatable and return it, this will be the backbones of our entire inventory and will handle it. We want to use the index function for an Object Orientated type system. The reason that I don’t set the index to Inventory, (ie; __index = inventory) is so we can manipulate the function to run a piece of code each time we access the table. This is good if you want to fire an event each time you update the inventory, rather then posting a line of code to fire the client at the bottom of each function you can just do it once in the __index.

function Inventory.new(contents)
	return setmetatable({
		Contents = contents, -- Store contents of the inventory
	}, {
		__index = function(self, index)
			-- Check if the index called on our inventory exists
			if Inventory[index] then
				-- Check if the inventories index is a function, if so return
				-- the function
				if type(Inventory[index] == "function") then
					return function(self, ...)
						-- The reason that we do a constructor like this is so
						-- you can fire an event after each function to do things such
						-- as update the inventory, etc...
						return Inventory[index](self, ...)
					end
				else
					-- Return the index
					return Inventory[index]
				end
			end
		end
	})
end

Adding an item
This code sample is going to be pretty much exactly like the beginner version, the only difference is that we’ll be referencing self. If you don’t know what “self” is then please follow the beginner tutorial and learn how metatables work before revisiting here.

function Inventory:Add(item, amount)
	-- Check the item argument and make sure it's specified
	if not item then
		return -- It's not specified, return
	end
	
	-- Check if the user has the item, if they do then add
	-- the amount to it, if not then define it to the amount or 1
	if not self.Contents[item] then
		-- Defining the item to the amount or 1
		self.Contents[item] = amount or 1
	else
		-- They have the item, adding a count to it.
		self.Contents[item] = self.Contents[item] + amount or 1
	end
end

Removing an item
Once again, this code sample will be really similar to the beginner version as well as the adding of an item.

function Inventory:Remove(item, amount)
	-- Ensure that the item you want to remove exists as
	-- well as that the user has the item.
	if not item or not self.Contents[item] then
		return
	end
	
	-- Check if removing the said amount would result in
	-- a negative, if it does then remove the item completely
	if self.Contents[item] - (amount or 1) < 0 then
		self.Contents[item] = nil -- Remove the item
	else
		-- Remove the amount specified from the items count
		self.Contents[item] = self.Contents[item] - amount or 1
	end
end

Completed Version

local Inventory = {}

function Inventory:Remove(item, amount)
	-- Ensure that the item you want to remove exists as
	-- well as that the user has the item.
	if not item or not self.Contents[item] then
		return
	end
	
	-- Check if removing the said amount would result in
	-- a negative, if it does then remove the item completely
	if self.Contents[item] - (amount or 1) < 0 then
		self.Contents[item] = nil -- Remove the item
	else
		-- Remove the amount specified from the items count
		self.Contents[item] = self.Contents[item] - amount or 1
	end
end

function Inventory:Add(item, amount)
	-- Check the item argument and make sure it's specified
	if not item then
		return -- It's not specified, return
	end
	
	-- Check if the user has the item, if they do then add
	-- the amount to it, if not then define it to the amount or 1
	if not self.Contents[item] then
		-- Defining the item to the amount or 1
		self.Contents[item] = amount or 1
	else
		-- They have the item, adding a count to it.
		self.Contents[item] = self.Contents[item] + amount or 1
	end
end

function Inventory.new(contents)
	return setmetatable({
		Contents = contents, -- Store contents of the inventory
	}, {
		__index = function(self, index)
			-- Check if the index called on our inventory exists
			if Inventory[index] then
				-- Check if the inventories index is a function, if so return
				-- the function
				if type(Inventory[index] == "function") then
					return function(self, ...)
						-- The reason that we do a constructor like this is so
						-- you can fire an event after each function to do things such
						-- as update the inventory, etc...
						return Inventory[index](self, ...)
					end
				else
					-- Return the index
					return Inventory[index]
				end
			end
		end
	})
end

return Inventory

Disclaimer
Yes, the “hard” version was rushed, it was meant to be. I don’t want beginners copying and pasting the code and not learning from it. It being somewhat incomplete and not having usage examples allows me to make sure that people at least are doing some research about it.

Security

Existance
When letting a user pick up an item from the client you want to make sure that the item they're picking up actually exists. How do you do this? You can send a model or a unique id of the item.

When the user picks up an item fire the event with the arguments being the item they’re picking up, on the server make sure that it exists and is a valid item.

local Event = game:GetService("ReplicatedStorage"):WaitForChild("PickupItem")
local Items_Folder = game:GetService("Workspace"):WaitForCild("GroundItems")

Event.OnServerEvent:Connect(function(player, item)
   if not item or not Items_Folder:FindFirstChild(item) then
      return
   end
   -- Add a radius check, (as seen below) and then give the user the item.
end)
Radius Checks
When making an inventory and allowing users to pick up items from the client one of the most important safety check is making sure that the user is within range to pick up an item, otherwise they can potentially pick up an item half way across the map (using exploits)
local distance = (HumanoidRootPart.Position - Object.Position).Magnitude
if distance > 5 then
   return
end

-- Give the user an item here, they're in range.
Realism
This one is the easiest to program. Make sure that the item the user is giving themselves is realistic. If the user is trying to pick up 10,000,000 Diamonds then you can most likely assume that it's a form of cheat. Keep in mind that doing this sort of check could result in some unfortunate loss, (ie; if a user truly does have 10,000,000 diamonds and you remove them from the inventory)
113 Likes

Awesome tutorial!

Definitely will help out a lot of people.

Thank you for this :slightly_smiling_face:

1 Like

Really cool article NodeSupport! I myself aspire to be a professional scripter aswell and I believe this shall help me on my path of progression, growth and determination!

Haven’t read the (whole) tutorial but just wanted to say that inventories are one of the most widely requested and wanted systems in games alone and this should help many people. The core of it is just using tables but combining with modules makes it more easily usable.

4 Likes

Thank you so much! I’ve been wanting to make one for so long with OOP!

Hey so I tried to make an inventory with this tutorial in mind and I made a pretty simple one, could you check it and see if it’s good?

2 Likes

You also might want to generate a unique ID to prevent any possible duplication glitches users might find.

2 Likes

It doesn’t really look like you used too much from this tutorial. But overall it’s looking good.

Your inventory tables and meta are on the client though, which should never be done. With your current exploiters could spawn themselves items, etc…

1 Like

Yea I didnt want to copy the tutorial word for word youknow haha, but thank you for taking the time to read my script :smile:

This tutorial is mainly to cover how to structure and securely create an inventory. If a duplication glitch is found then it’s the users job to patch that glitch with whatever vulnerability they add.

Better safe than sorry if you game has a large userbase, someone finds a duplication glitch, and now everyone knows how to do it until you patch it. But, if you implemented a GUID system for all items, this problem can be avoided.

1 Like

Simple, easy, quick to read. Overall just perfect, this is a great tutorial for developers to use when creating their first game. Good job.

I like the idea of this, I like the OOP-style it is easy to use, however, for inventory systems such as this, I would advocate ECS, as it is much easier in this case and is a much more performant than the used paradigm.

Definitely a quality post, I would love to see people introduce patterns with some elaborate summary of them in the future. I believe it is a very bad practice to mindlessly teach out these patterns without properly arguing why you should write it this way.

How would I make a Gui that goes along with this?

A better option would be to log every item separately, using a GUID (global-unique-identifier) as the table’s index. By just keeping the items as number counts, you limit yourself from:

  • being able to log every individual item’s data
  • being able to set unique data for an item
  • Will have a harder time rolling back data or deleting certain items from a player’s inventory (since you can’t log the time of when someone received that data)
2 Likes

How would you make 2 of the same items but non stackable

For a game I’ve been working on recently, and it’s a good habit in general, I have every item have its owned unique identifier, alongside meta information.

If the item isn’t stackable then store that inside the meta information, alongside the stack size, check if the stack size is the max stack (if it’s not stackable, its one in this case) if it’s the max size, create a new item stack, if not, add to an existing one.

how would i be able to

  • being able to log every individual item’s data
  • being able to set unique data for an item
  • Will have a harder time rolling back data or deleting certain items from a player’s inventory

This tutorial has been a great help in making my game

1 Like