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.”
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.
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.