How to Make Server-Synced Daily Shops

Introduction

Have you ever wondered how to make a self-rotating daily shop that would be completely synced with all servers, like Arsenal’s?

Do you also want rarity to be taken into account with the items generated? Then this is the tutorial for you! A few weeks ago, I had a lot of trouble trying to figure out how to do this, but thanks to the help of @VitalWinter and the code I had already, I was able to make it happen perfectly!

In this tutorial you will learn how to:

  • Code a self-rotating daily shop that will be synced no matter what server you’re in
  • Stop item duplicates with 100% accuracy
  • Add rarity to the items that are being generated
  • Make a timer to display the amount of time until the next reset

Let’s get started!

Retrieving a synced time

I know that this will also probably work fine with using os.time(), but I’ve seen several reports of it being inaccurate and out of sync up to 3-5 minutes in different servers. I don’t know if this is fixed now, but I use a module which retrieves time externally which eliminates the possibility of anything being out of sync.

Before we do anything, make sure you have HTTP requests enabled in studio. You can do this by going to Game Settings --> Options --> and then turn on Enable HTTP Requests.

First, we need to use this synced time module made by @XAXA, which retrieves time from the Google website.

Copy and paste the code below into a ModuleScript, and then place it into ServerStorage or ReplicatedStorage. I recommend putting it into ServerStorage because daily rotated shop logic should be done completely on the server, so there’s no need replicating it to all of the clients. Unless you are also going to use this module for clients, keep it in ServerStorage. Otherwise, put it in ReplicatedStorage. Name it something that represents what it does, like “SyncedTime.”

image

Synced time module
local monthStrMap = {
	Jan=1,
	Feb=2,
	Mar=3,
	Apr=4,
	May=5,
	Jun=6,
	Jul=7,
	Aug=8,
	Sep=9,
	Oct=10,
	Nov=11,
	Dec=12
}
local HttpService = game:GetService("HttpService")
local function RFC2616DateStringToUnixTimestamp(dateStr)
	local day, monthStr, year, hour, min, sec = dateStr:match(".*, (.*) (.*) (.*) (.*):(.*):(.*) .*")
	local month = monthStrMap[monthStr]
	local date = {
		day=day,
		month=month,
		year=year,
		hour=hour,
		min=min,
		sec=sec
	}
	
	return os.time(date)
end

local isInited = false
local originTime = nil
local responseTime = nil
local responseDelay = nil
local function inited()
	return isInited
end

local function init()
	if not isInited then
		local ok = pcall(function()
			local requestTime = tick()
			local response = HttpService:RequestAsync({Url="http://google.com"}) 
			local dateStr = response.Headers.date
			originTime = RFC2616DateStringToUnixTimestamp(dateStr)
			responseTime = tick()
			-- Estimate the response delay due to latency to be half the rtt time
			responseDelay = (responseTime-requestTime)/2
		end)
		if not ok then
			warn("Cannot get time from google.com. Make sure that http requests are enabled!")
			originTime = os.time()
			responseTime = tick()
			responseDelay = 0
		end
		
		isInited = true
	end
end

local function time()
	if not isInited then
		init()
	end
	
	return originTime + tick()-responseTime - responseDelay
end

return {
	inited=inited,
	init=init,
	time=time
}

Alright, nice! Now we have step 1 down.

Starting the server script

Now, it’s time to start on the script that actually updates all of the items.
Make a new script in ServerScriptService and name it whatever you want, but obviously I recommend you to name it to represent what it does to avoid confusion.

image

First, we need to require the module in the server script and make a request to the Google website to retrieve time if it hasn’t already.

local syncedtime = require(game.ServerStorage.Utilities.SyncedTime)
syncedtime.init() -- Will make the request to google.com if it hasn't already.

Then, we are going to define another function that we will use to format time later on.

local function toHMS(s)
    return string.format("%02i:%02i:%02i", s/60^2, s/60%60, s%60)
end

After this, you should make a table and store the names the items you’re going to rotate inside of it. I’ll make a sample one we are going to use for this tutorial. Make sure every item has a UNIQUE name. There shouldn’t be any duplicates in here.

local ItemTable = {
    "Banana",
    "Apple",
    "Grape",
    "Strawberry",
    "Orange",
    "Peach",
    "Mango",
    "Pineapple",
    "Pear"
}

Alright! Now that we have these down, we are going to start making a function that will retrieve available items from this table. We are going to use Random.new() and supply it with a seed, which is going to be the very reason why we are able to generate the same set of items for each server. If the seed changes, the set of items will change.

function getAvailableItems(day, numberofitems) 
	local rng = Random.new(day) -- We are going to learn how to get this "day" seed later.
	local shopItems = {} -- This will be the table that will contain your shop items that get generated.
	
	for i = 1, numberofitems do -- this controls the number of items you will want to get in the shop. This should be less than the # of items in your item table.
		local shopitem = ItemTable[rng:NextInteger(1,#ItemTable)] -- Gets a random item in the table. This will generate the same sequence of items if the seed is the same.
		table.insert(shopItems, shopitem) -- Inserts the item inside of the table
	end
	
	return shopItems -- returns the table so we can use it later
end

And there we have it, a simple function. Unfortunately though, it really isn’t that simple. You’d want to prevent duplicates, right? Let’s add another check to make sure that doesn’t happen. Basically what we are going to do is copy the original table, and then remove the duplicate (if there is one found) from the copy of the table. Then, we have the shop “respin” from the copy of the table, now with the duplicate value removed from it. This makes duplication prevention 100% accurate because we’re only choosing from things that we know haven’t been chosen.

function getAvailableItems(day, numberofitems) 
	local rng = Random.new(day) -- We are going to learn how to get this "day" seed later.
	local shopItems = {} -- This will be the table that will contain your shop items that get generated.
	
	local function shallowCopy(original) -- function to copy the table so we can remove values from it.
	    local copy = {}
	    for key, value in pairs(original) do
	        copy[key] = value
	    end
	    return copy
	end
	
	local function Generate() -- Generate funciton
		local shopitem = ItemTable[rng:NextInteger(1,#ItemTable)] -- Gets a random item in the table. This will generate the same sequence of items if the seed is the same.
		local itemtablecopy = shallowCopy(ItemTable) -- Copy the table 
		
		local duplicate = table.find(shopItems, shopitem) --Finds a duplicate
		
		if duplicate then -- If there is a duplicate and it's not nil...
			print("Respinning..")
			for number, v in pairs(itemtablecopy) do 
				for i = 1, #shopItems do
					if v == shopItems[i] then
			        	table.remove(itemtablecopy, number) -- if an item in the copied table is the same as a value inside of our shop items already, then it removes it
			        end
				end
			end

			repeat
				shopitem = itemtablecopy[rng:NextInteger(1,#itemtablecopy)] -- keep randomnly generating an item from the items that we have left
			until shopitem ~= item -- until you get one that the shop table doesn't have yet
		end

		table.insert(shopItems, shopitem) -- Inserts the item inside of the table
	end
	
	repeat
		Generate()
	until #shopItems >= numberofitems -- keep generating items until we get to the # of items we want.
	
	return shopItems -- returns the table so we can use it later
end

There we go! Now our function is great!

Adding Weight (Rarity) to the Items

Most games have rarity with their items. Rarity makes items more exciting to get and adds a LOT of value to them. So, let’s add rarity with our items! If you don’t want rarity with your items and just want it to be completely random, you can move on to the next part of the tutorial.

How to add Rarity

Splitting our items into different tables

First, we have to obviously split each item based on their rarity, so let’s seperate them into different tables. I am going to use three for this tutorial - Common, Rare, and Legendary. You should name the tables after their rarity, respectively.

local commonItems = {
    "Banana",
    "Apple",
    "Grape",
    "Strawberry"
}

local rareItems = {
	"Orange",
    "Peach",
    "Mango"
}

local legendaryItems = {
	"Pineapple",
    "Pear"
}

Adding Weight Numbers

Next, we have to make weights for each rarity to control how frequently an item will come up. These shouldn’t go over 100 for the sake of the tutorial and for it not to become complicated.

local weights = {
    Common = 100, -- Your most common rarity should have the max value, 100. You will see why later.
    Rare = 25,
    Legendary = 2
}

Implementing the rarity into our function

Alright, now let’s go back to our item generator function. Let’s add the rarity to it. Read it thoroughly for a good explanation. We are doing the same technique as last time, just that this time we are only copying the table of the rarity that a duplicate was found in. If you do it right, you should have 100% accuracy.

function getAvailableItems(day, numberofitems) 
	local rng = Random.new(day) -- We are going to learn how to get this "day" seed later.
	local shopItems = {} -- This will be the table that will contain your shop items that get generated.
	
	local function shallowCopy(original) -- function to copy the table so we can remove values from it.
	    local copy = {}
	    for key, value in pairs(original) do
	        copy[key] = value
	    end
	    return copy
	end
	
	local function Generate()
		local shopitem -- initialize our ShopItem variable.
		local returnedRarity -- We will need this for duplicate prevention
		
		
		local function GenerateWeightedItem() -- wrap this in a function so that it's easier to use for our duplicate check
			local rarity -- initialize our rarity variable
			local weightNumber = rng:NextNumber(0, 100) -- Use NextNumber instead of NextInteger so that you can use decimals in your weight values. NextNumber is much more precise. We limit it to 100, our max weight value
			
			local weightitem -- initialize a variable for the weighted item
			
			if weightNumber < weights.Legendary then 
				print("Legendary") 
				weightitem = legendaryItems[rng:NextInteger(1,#legendaryItems)] 
				rarity = legendaryItems  -- return the table
			elseif weightNumber < weights.Rare then 
				print("Rare")
				weightitem = rareItems[rng:NextInteger(1,#rareItems)] 
				rarity = rareItems
			else
				print("Common")
				weightitem = commonItems[rng:NextInteger(1,#commonItems)] 
				rarity = commonItems
			end
			
			return weightitem, rarity -- Return both these values so we can use it later. We will need the "rarity" table for duplicate prevention
		end
		
		shopitem, returnedRarity = GenerateWeightedItem() -- Do the function, and now we have both the item and the returned rarity table
		local itemtablecopy = shallowCopy(returnedRarity) -- Copies the table we returned
		
		--Duplicate check below
		
		local duplicate = table.find(shopItems, shopitem) --Finds a duplicate
		
		if duplicate then -- If there is a duplicate and it's not nil...
			print("Respinning..")
			
			for number, v in pairs(itemtablecopy) do 
				for i = 1, #shopItems do
					if v == shopItems[i] then
						table.remove(itemtablecopy, number) -- if an item in the copied table is the same as a value inside of our shop items already, then it removes it
			        end
				end
			end
		
			repeat
				shopitem = itemtablecopy[rng:NextInteger(1,#itemtablecopy)] -- keep randomnly generating an item from the items that we have left in the copied table
			until shopitem ~= item -- until you get one that the shop table doesn't have yet
		end
	
		
		table.insert(shopItems, shopitem) -- Inserts the item inside of the table
	end
	
	repeat
		Generate() -- keep generating..
	until #shopItems >= numberofitems -- Until we get our desired # of items.
	
	return shopItems -- returns the table so we can use it later]]--
end

Let me explain this:

What we are doing with our weight values is creating intervals. The bigger the interval, the more times an item with that rarity will come up. With my weight table, Common is 100, Rare is 25, and Legendary is 2. We are also using :NextNumber() to generate a number from 1 to 100 (Decimals are included, so you can add decimals in your weight table.). Remember, you don’t have to worry about numbers being different in servers because we supplied a seed to Random.new(), which means that it will generate the same sequence of numbers in every server.

This means:

  • 0 to less than 2 = Legendary

  • 2 to less than 25 = Rare

  • 25 to 100 = Common

If you want more rarities, add more weights to your weight table and add extra else-ifs to the conditional. Make sure the conditions are going in order from LEAST to GREATEST (aka most to least rare), like my table.

So far, we’ve made a function for generating items. Now, it’s time for the next part of the tutorial, which is going to handle the daily updating of the shop.

Daily Rotation

We’re going to have to do some math to get what we want. Let’s start by initializing our variables.

Calculating Time Offset

Our SyncedTime module is basically a reliable version of os.time(). They still both return proper Unix Time, which is 00:00:00 UTC on January 1st, 1970. When I was making my shop, I wanted mine to reset every day at 5 PM PST (my time zone). Luckily for me, I didn’t have to offset my shop’s reset time because coincidentally, 00:00:00 UTC is already 5:00 PM PST. Since we’re doing something daily and not weekly, we don’t need to worry about what day of the week it was. If you’re doing something weekly, monthly, etc, you need to consider those variables and offset the time based on that.

To find out what you want to offset your time to, you first need to find out what 00:00:00 UTC is in your timezone. To do that, just go on google and type in this:

"00:00:00 UTC to (your timezone here)"

Then, add or subtract the amount of hours you need to get to your desired time (You shouldn’t be going over 24, because it’s not necessary). For example, if my time zone was EST, and I wanted my shop to offset at 5PM I’d do this:

8:00 PM EST - 3 hours = 5:00 PM EST
Our calculated time offset would be -3.

Initializing Variables

local currentDay -- initialize our CurrentDay variable.
local currentShopItems = {} -- These are going to be the actual items that get displayed in a shop.
local hourOffset = 0 -- I stated earlier that I didn't need an offset, lol, but change this to your desired offset 

local offset = (60 * 60 * hourOffset) -- This ends up being how many hours you put into hourOffset Because 60 seconds = 1 minute, 1 minute * 60 = 1 hour, and 1 hour * hourOffset is the amount of hours

Now that you’ve got your offset and your variables ready, we can proceed.

Writing Code for Daily Rotation

Now, we are actually going to start writing code for our daily rotation. We are going to use a while loop to accomplish this. This will also make a string that you can use to display as a timer until the next reset. If you are still going to write more code under this, wrap this in a coroutine.

Remember syncedtime.time() is the same thing as os.time() but way more reliable and in sync.

while true do
	local day = math.floor((syncedtime.time() + offset) / (60 * 60 * 24)) -- flooring is EXTREMELY important here, because that means the number won't change unless it becomes an integer higher. This is the seed we're going to supply to our getAvailableItems function. This would output something like 18431. By dividing this by 24 hours and adding our offset, we know that if the number changes, THEN a day has passed since the epoch +  our own offset.
	local t = (math.floor(syncedtime.time())) - offset -- Sets the time to your offset time (ex. 8 pm PST)
	local daypass = t % 86400 -- Using % gives us the remainder, which in our case is how far into a day we are
	local timeleft = 86400 - daypass -- Now we can easily calculate how much time is left by subtracting a day (86,400 seconds) by daypass
	local timeleftstring = toHMS(timeleft) -- This will format the time to HH:MM:SS (ex. 05:42:03)
		
	print("The time left is ".. timeleftstring) -- You can comment this out later, but test it with the printing to see if it works.
	
    --Obviously, you'd use "timeleftstring" to display a timer or something

	if day ~= currentDay then -- If the day variable isn't equal to currentDay (so when the day variable changes...)
		currentDay = day -- update the currentDay variable
		currentShopItems = getAvailableItems(day, 5) -- Get 5 items and supply the day as the seed, then set our currentShopItems table to the table we get from this function.
		print("The items for today are these: a ".. table.concat(currentShopItems, ", ")) -- prints out the items you get
        --Your code to update your shop would go here!
		print("Updated shop items.")		
	end
    wait(1)
end

The especially useful thing about this function is that when a new server is created, it will automatically update the shop, because in that server, the currentDay variable wouldn’t be set to anything yet.

Output (without rarity)

Today’s items:
The items for today are these: a Pear, Banana, Grape, Strawberry, Apple

Tomorrow’s items: (just manually changed to offset for the shop to reset again for the sake of the tutorial)

The items for today are these: a Pear, Orange, Pineapple, Apple, Mango

No duplicates!

Output (with rarity)

Today’s items:
The items for today are these: a Pear, Banana, Mango, Strawberry, Orange

2 common items:

  • Banana, Strawberry

2 rare items:

  • Mango and Orange

1 legendary item:

  • Pear

And no duplicates!

Tomorrow’s items: (just manually changed to offset for the shop to reset again for the sake of the tutorial)
The items for today are these: a Grape, Strawberry, Banana, Apple, Orange

4 common items:

  • Grape, Strawberry, Banana, Apple

1 rare item:

  • Orange

Again, no duplicates!

Conclusion

I hoped you enjoyed this tutorial! I hope this will be useful to a lot of people the same way it was to me. I find this really cool and sort of essential to a game, and it’s weird that no one talks about it, lol

Please correct me if I did anything wrong, this is my first tutorial! This should work though.

Reminders:

  • Make sure the amount of items you’re generating is LESS than your total amount of items! Otherwise, duplicates can’t be avoided.
  • Make sure everything in your item tables have a unique name! If you test it with a table that has everything with the same name, then it’s going to infinitely spin and try looking for another value in the table when there isn’t because they’re all the same!

Happy "shopping!"

Edit: The prevention of duplicates works but isnt 100% accurate, fixing right now!
Edit 2: Duplicate prevention for without rarity has been updated! It is now 100% accurate. Redoing duplicate prevention for rarity right now!

Edit 3: DUPLICATION PREVENTION FOR BOTH TYPES HAVE BEEN UPDATED FOR 100% ACCURACY!!! Let’s go!!

Edit 4: Improved duplication prevention! Found an instance where you could have a duplicate and fixed it. Now it should for sure be 100% accurate.

Edit 5: Looking back at this I realized that some of the code was redundant/unnecessary, so I’ve updated it. If anything is wrong, just tell me and I’ll fix it.

111 Likes

Thanks for making this! It is really helpful!

2 Likes

No problem. Also thank you for being the first reply :sweat_smile:

3 Likes

I’ve been needing something like this for a while now, thanks so much for your tutorial :smiling_face_with_three_hearts:

2 Likes

You’re welcome! It makes me happy that I’m getting replies now

2 Likes

This is a really nice tutorial, I would love to see more tutorials of you!

2 Likes

Hi! Would this work with Dictionaries? I have been trying to get the name, price and image ID, would that be possible?

1 Like

Yes, of course. You can have a main dictionary with keys as the name and the values as tables with all that information, like the price, ImageID, etc. Then, get the daily shop to generate names by providing it with a table of names, like in the tutorial. From there you can use those names to index the original dictionary and fetch the info, this is what I do and this should work for you. I have a huge module (my Archive) that has all of the information for things in my game, and once I use the daily to generate some names, I use them to index the Archive and fetch all the info for that item.

1 Like

Okay so I have figured it out but how do I get the items rarity? I tried currentShopItems[1].rarity but no work. Ill keep trying

This should be used to generate names. Then, use those names to index a table to fetch an item and its info.

Also could I see some code?

2 Likes

That is so epic dude. :sunglasses: :ok_hand:

1 Like