PlayerDataStore Module ~ For an easier transition to using the DataStoreService

I’ve been quite surprised to see the low rate of adoption of the new DataStoreService. Talking a look at things I’m guessing that a big part of the reason is that it’s not so obvious how to easily attach data in the new DataStores to a individual players, and there’s the fact that you really need some caching+intermittent saves inbetween your actual writes to a player’s data, and that data being loaded/saved.

To take down those barriers I’ve written a module that handles both of those things for you. The design of the module is such that you can use it as a sort of “drop in” adapter between code wanting the old style DataPersistence and the new DataStoreService API.

What the module does is automagically cache the DataStoreService data for each player (including offline players if you want that) and batch reads and writes to that player data so that you can have immediate reading and writing of it while letting a intermittent task in the module save the data in batches.

The module also does really nice things such as:
[ul]
[li]Ensure that nothing goes wrong if there is cached data for a player who is not online in the instance, and then that player actually joins the instance. The module will make sure that the cached data is always associated with the player whether online in the place or not.[/li]
[li]Let you plug in custom Serialization / Deseralization for specified keys of the stored data.[/li]
[li]Just like with the old DataPersistence API, data for players who are currently in the server is automacically loaded in and cached unless it was already loaded in manually by some script like a leaderboard while the player was not online.[/li]
[li]Unloads cached data after a certain delay, but only if the user it’s associated with has left the server, and only if no other code in your project is still holding onto references to that SaveData object storing the cached data. Basically, if the data should still be kept in the cache then it is being kept, and you won’t ever be doing more DataStore requests than you need to be by having to re-get it.[/li]
[li]It doesn’t randomly delete your stored data for no reason ← most important reason right there[/li]
[/ul]

The ModuleScript in question is attached. Here’s an example of it at it’s simplest, used exactly like the old DataPersistence API on the Player:

local PlayerDataStore = require(game.ServerScriptService.PlayerDataStore)
--... then in the game logic somewhere:
local saveData = PlayerDataStore:GetSaveData(player)
saveData:Set('Score', (saveData:Get('Score') or 0) + 1)
local PlayerDataStore = require(game.ServerScriptService.PlayerDataStore)
game.Players.ChildAdded:connect(function(player)
local saveData = PlayerDataStore:GetSaveData(player)
saveData:Set('VisitCount', (saveData:Get('VisitCount') or 0) + 1)
end)

…And that’s it! No need to call some sort of “save” explicitly somewhere or something like that, it will just work.

Note on stability: This isn’t totally bulletproof yet, there’s probably still a couple bugs in it somewhere. But, I did spend 3 hours today testing it and I think I have all of the obvious bugs found and fixed, so it is pretty stable: If you’re starting a on a new place and it fits it well I would give it a try.

PS: No association with the unfortunate DataPersistence events of today, just happens to be an interesting cooincidence.

6 Likes

I don’t think uploads work properly here, so here’s a link to the model.

Thanks Seranok (How did you think to go find it in my models??)

Module Source Link in case you really don’t like having to be logged in and go take a model like me. There’s a couple examples underneath the Module if you get it from my models, but the API itself is described in a comment at the start of the module.

Thanks a bunch, stravant! Maybe I can implement this into my Hunger Games and finally rid my inbox of all the “my stats are gone” messages. You know, the real way, instead of mashing the delete button.

I don’t get why people would ever have a hard time with the DataStoreService. I looked at the wiki one time when I was learning how to use it, and was like “Oh, that’s how it works” and have been able to easily use them since. Although, I really only use them for storing the keys from sold developer products because I have a site setup for storing more data, and making it easier to manage.

Stravant ~= Anaminus
But yeah this is a pretty nifty tool

If only my old method of yielding in metamethods would still work, my even nicer solution would work.

I mean, really, you’d do things like (note: this assumes a wrapped player instance)

plr.data.ExampleKey=plr.data.ExampleKey+1

I mean, it doesn’t even need any functions. It’s just magic. But now, I can use UpdateAsync or SetAsync from the __index or __newindex metamethods.

“plr.data.ExampleKey=plr.data.ExampleKey+1”

Since this Module uses a cache inbetween the reads/writes and the actual data store access, you could actually modify it to work exactly like that. I personally don’t think you should be using non-obvious stuff like that in your code, which is why I didn’t write the API of the module to work like that, but you could easily modify it to work that way for your personal use: Just add a metatable to the SaveData objects that redirects __index and __newindex to Get and Set.

There is now an article on this Module on the Wiki:

I must ask… any throttling limit?

Well, if you only call Get and Set then you never have to worry about any throttling limit, the module will make sure to stay under the limit.

However, as for Flush and Update calls, the module will have whatever the underlying DataStore has as a throttling limit, since that’s what it’s using in the backend.

[quote] Well, if you only call Get and Set then you never have to worry about any throttling limit, the module will make sure to stay under the limit.

However, as for Flush and Update calls, the module will have whatever the underlying DataStore has as a throttling limit, since that’s what it’s using in the backend. [/quote]
Awesome… wait, does :Flush() clear the catch? I am confused. :stuck_out_tongue:

Very cool! I probably won’t use as I prefer to make my own back-end (experience :smiley: ).

A question: what’s the best way (or a good way) to give teleport reason with DataStores. Like some sort of system that, when teleporting players, sets a value so the retrieving server knows why they were teleported and from where they came.

" what’s the best way (or a good way) to give teleport reason with DataStores."

By just… doing it? Once you have a system set up for storing data attached to a player (Like this one), then you can just use one of the player’s keys to store a teleport reason.

“wait, does :Flush() clear the catch? I am confused.”

The idea of this module is that it will not actually save to the data store right away when you set keys, but rather wait until a good time and save all of the fields that you changed all at once with a single data store call. Flush() will manually force it to do one of those saves, if for some reason you really want some important changes to be committed to the data store right away.

[quote] " what’s the best way (or a good way) to give teleport reason with DataStores."

By just… doing it? Once you have a system set up for storing data attached to a player (Like this one), then you can just use one of the player’s keys to store a teleport reason. [/quote]

But what about the cache? If I update a value on teleport is it going to be updated in the new server?

“But what about the cache? If I update a value on teleport is it going to be updated in the new server?”

The cache is per-game-server (it has to be, this is just a normal Lua module, I can’t do anything else). There are a couple of gotchas:

  1. You should call a Flush() after writing the TeleportReason to the player but before you do the teleport, to make sure that the TeleportReason and other local data is actually saved to the main data store before the player arrives in the new server and their data is loaded in there.

  2. If you have a large 50+ person “Lobby” server or something like that, then you should set the CACHE_EXPIRY_TIME to be a bit shorter on it. This is because if you teleport out to one of the other universe servers, and then teleport back to the same lobby server all within the CACHE_EXPIRY_TIME the changes in the other server will get lost, because the lobby server will “pick up” the cache entry that still exists on it. For normal games this scenario is almost impossible, but if you have one large lobby server and a lot of small game servers which you can exit back to the lobby from it can come up.

[quote]
2) If you have a large 50+ person “Lobby” server or something like that, then you should set the CACHE_EXPIRY_TIME to be a bit shorter on it. This is because if you teleport out to one of the other universe servers, and then teleport back to the same lobby server all within the CACHE_EXPIRY_TIME the changes in the other server will get lost, because the lobby server will “pick up” the cache entry that still exists on it. For normal games this scenario is almost impossible, but if you have one large lobby server and a lot of small game servers which you can exit back to the lobby from it can come up. [/quote]

Well, honestly, when a player leaves the lobby you should just erase his data from the cache, as it was just temporary.

“when a player leaves the lobby you should just erase his data from the cache, as it was just temporary.”

This is incorrect, if anything the time to erase data from the cache is when the player joins the server, not when they leave. Consider the case where you have a global leaderboard using this module: If you purge the player from the cache then they will be added back to the cache right away as soon as the leaderboard goes to update. The time you need to purge them from the cache is when the player enters the server.

I ment, on leave, flush the data, then get rid of the cached version.
Of course, like you said, force loading on join works also, unless the previous server didn’t save it yet.

Does one’s data get automatically flushed when leaving, or does a script need to manually do that? :stuck_out_tongue:
Other than that question, it is cool. :DDD