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 
		for i, item in pairs(shopItems) do 
	    	    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
        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
		for i, item in pairs(shopItems) do 
		    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
	    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.

Edit 6: Fixed a bug with the code.

371 Likes

Thanks for making this! It is really helpful!

9 Likes

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

6 Likes

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

5 Likes

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

6 Likes

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

5 Likes

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

4 Likes

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.

6 Likes

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

1 Like

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?

3 Likes

If we replace all 'syncedtime.time()'s will there be a critical issue? I don’t mind there being a 3-5 minute delay because I feel like HttpService isn’t worth using.

3 Likes

Nah, not really. As I said, os.time() works fine too, just that if you want to 100% make sure your servers are synced, use the module because it fetches time externally.

Hello, This Was very helpful and was just what I was looking for but I am unsure of any possible ways to create a GUI that works with this. Do you think that you could help me with this or possibly even extend your tutorial to this point?

2 Likes

There is a bug here; you say until shopitem ~= item. This also caused duplicates in my shop.
What should it be instead?

1 Like

Are you following these?

As long as you follow these, that shouldn’t happen. I’ve been using this code for months now and I have not gotten a single duplicate.

Also you are correct, that is a bug, that is on my end.

You’re supposed to nest it in a loop:

for i, item in pairs(shopItems) do -- This will loop through the shop-generated table to check if the item we just generated is the same as any of the items in there.
			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
		end
1 Like

Yeah, I followed that things, but I think it had something to do because the loop wasn’t there. But I fixed it myself in a different way. Thanks anyways for this brilliant tutorial!

2 Likes

Hello, what are my scripts supposed to look like?

1 Like

Awesome guide. I am having a problem though.
I am cloning my shop items out of replicated storage into the player’s shop gui. All of the paths check out with print statements.
When the shop resets, I clear out the old items and it works perfectly, but I can’t get the shop to load when players join the game. It works in studio, but in a live server, items only populate after the shop resets and not when players join. Here is the shop code:

--Your code to update your shop would go here!
		for i, v in pairs(Players:GetPlayers()) do
			local j = v.PlayerGui.AbilityShop.Abilities.Inv.Frame:GetChildren()
			for x, y in pairs(j) do
				if y.ClassName == "Frame" then
					y:Destroy()
				end
			end
            local weapon = currentShopItems[1]
			local display = rs.Shop[weapon]:Clone()
			local wshop = v.PlayerGui:WaitForChild("WeaponShop")
			display.Parent = wshop.Weapon.Inv.Frame
			game.Workspace.Weapon.Value = currentShopItems[1] -- string variable I created to try to load it on player added
         end
      print("Updated shop items.")	
      end
   wait(1)
end

On the player added function I have:

Players.PlayerAdded:Connect(function(player)
      ---datastore stuff
       print(game.Workspace.Weapon.Value) --always prints nil on live server tried adding waits and checks
	   if game.Workspace.Weapon.Value == "Axe" then
	    	local display = rs.Shop.Axe:Clone()
	    	local wshop = player.PlayerGui:WaitForChild("WeaponShop")
	    	display.Parent = wshop.Weapon.Inv.Frame
       elseif game.Workspace.Weapon.Value == "Wand" then
	    	local display = rs.Shop.Wand:Clone()
	    	local wshop = player.PlayerGui:WaitForChild("WeaponShop")
	    	display.Parent = wshop.Weapon.Inv.Frame
    	end
end

Again, this will work in studio and load 2 of the items, but it won’t load any in a live server.

2 Likes

This is a good tutorial, I am using it to make daily and weekly quests, but my problem is that January 1st 1970 was on a Thursday and I want the weekly quests to update on a Monday.

I know that I have to use an offset but I dont know how to apply that offset, here is my script(its a test script btw thats why the variable naming is trash):

local SyncedTime = require(game.ServerStorage.Modules.SyncedTime)

local CurrentDay = nil
local CurrentWeek = nil


local HourOffset = 0

local DayOffset = 4

local Offset = (60 * 60 * HourOffset)


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

local function toDHMS(s)
	return ("%02i:%02i:%02i:%02i"):format(s/86400, s/60^2%24, s/60%60, s%60)
end

while true do
	local Day = math.floor((SyncedTime.time() + Offset) / (60 * 60 * 24))
	local Week = math.floor((SyncedTime.time() + Offset) / (60 * 60 * 24 * 7))
	
	local Time = (math.floor(SyncedTime.time())) - Offset 
	local DayPass = Time % 86400
	local WeekPass = Time % 604800
	
	local TimeLeft = 86400 - DayPass
	local DaysLeft = 604800 - WeekPass
	
	local TimeLeftString = toHMS(TimeLeft)

	print("The time left is ".. TimeLeftString) 

	local TimeLeft2 = toDHMS(DaysLeft)
	
	print("Days left: " .. TimeLeft2)

	if Day ~= CurrentDay then 
		CurrentDay = Day 
	
		print("new day")	
	end
	
	if Week ~= CurrentWeek then
		CurrentWeek = Week
		
		print("new week")
	end
	
	wait(1)
end
3 Likes

Are you trying to update the shop on PlayerAdded? You should only be updating it in the while loop; try looking back at the tutorial, the loop should automatically take care of it because when a server starts the variable we calculate isn’t calculated yet