Hi all,
I’m trying to make a daily leaderboard, but I’m stuck on the correct architecture and hoping to get some feedback from more experienced devs. It’s for a parkour-type game for which I want to make a Top 5 of the fastest times of the day.
Attempt 1 - Ordered DataStore
At first, I tried using an OrderedDataStore. Which made sense, I put the userID as a Key and the Time as a Value. The OrderedDataStore then nicely ordered the times from fastest to slowest, so I just need to get the top 5, and I’m done.
BUT, I wanted only times for today. So in my first version, I just put the date in the name of the OrderedDataStore like such:
local DataStoreService = game:GetService("DataStoreService")
local LeaderboardStore = DataStoreService:GetOrderedDataStore("DailyLeaderboard" .. DATE)
But this would create a backlog of OrderedDatastores from previous days, and there is no API/Function to delete an old DataStore. (As far as I found in the documentation, at least). So I abandoned this as it didn’t feel like a clean architecture to pile up unused DataStores. It’s also explicitly mentioned in the documentation that you should limit the number of tables.
Attempt 2 - SortedMap in MemoryStore
So then I learned about the MemoryStore, it had an expiration parameter, so I thought, “perfect”! I just create a function that calculates the seconds left of today, put it as an expiration period, and all the daily entries expire at midnight, which would give me a clean Leaderboard every time at midnight.
But, the sorted map of a Memorystore sorts on the key instead of the value. Although the documentation says the key needs to be a string, I managed to store the number of seconds as an integer, and it works and sorts fine. I just put the userID as a value. And again, I have a sorted top 5 of the fastest times.
But it created two new issues.
- Because the UserId is now the value, it would allow players to get multiple entries in the daily leaderboard. I fixed this with a secondary DataStore that stores the last fastest time of a given userID. So that way, I can check, was the player quicker than last his/her last attempt? If so, add a new entry and delete the old one. So that issue I managed to fix. (Although now that I’m writing it, I would still need to reset the secondary datastore at midnight, but I think I could fix that somehow)
- If people finish on the same time mark in seconds. For example, if two players finished in 8 seconds, the second player wouldn’t be able to store his time as the unique key of 8 seconds already exists. And I didn’t figure out how to resolve it. You could add the user_id in the key, but sorting numbers alphabetically doesn’t work with numbers like 1, 11, 2, 3
Attempt 3 - Prefixes
Then I read in the documentation of DataStores again that you can add prefixes. So I thought ok, let’s add a prefix of the date to the user ID key. So it would be, for example, 26072023/userid1 as a key. When I want to display the top five, I filter on the prefix of today, and I have a top 5 again. I would then need to create a throttled separate thread to delete the old data record by record with a 0.10 wait time in between. Not super great, but I would have a DataStore and no old irrelevant data.
BUT in the documentation of the OrderedDataStore there are no mentions of prefixes, and the function “ListKeysAsync” to get a list of keys with a prefix doesn’t exist for OrderedDataStores. I wrongly assumed it would be inherited from GlobalDataStore. But it’s only part of the regular DataStore.
Attempt 4 - Full manual approach
Another way I thought as an option, was a full manual approach. Didn’t implement it yet. But the concept was when a player finishes the parkour, it would fetch a latest top 5, manually add the new entry if it was faster, do a sort and store the end result again, for example as a JSON table or something.
But when I gave it more thought it could create conditions. For example, when two players finish at the same time. They both get the latest top 5, do their adding and sorting and depending how all those async processes go, the second player could overwrite the data of the first.
(Although less likely than duplicate keys in the memory store, it doesn’t feel right, and there should be a better/simpler method.)
And now I’m out of ideas for how I should approach this. #PleaseHelp