DMOP Player Save - I want your opinions

I have created a data save library for a game I have been working on. The goal of the library is to be developer-friendly while also providing a powerful player save framework. You can see it here as a free developer item

It functions by using a DataModel stored in ServerStorage. Within the DataModel folder, Player save data can be structured, like so:
image
This setup (assuming DMOP is in ServerScriptService, and Kills is an IntValue) is all a developer would need to do to setup leaderboard stat persistence.

Data is saved using the DataStoreService in a DataStore called PlayerData.

Any Value object who’s property can be saved with DataStore is supported. Folders are considered to be tables by default, and the name of items is their key in the table.

DMOP also comes with advanced functionality, such as declaring indexed arrays:
image
This structure declares that PlayerData[PlayerTags] is an array, and items in that array get their value from the folder name in PlayerTags

DMOP supports keyed arrays as well, which are very similar but also have exclusive functionality:
image
This structure declares that PlayerData[PlayerTags] is an array, and items in that array get their key from the folder name in PlayerTags. In the example pictured above, the value of these items in memory is an empty table (which is the exclusive functionality)

This is where the power of DMOP starts to shine, if you use a Keyed Array, you can further define the structure of the keyed array:
image
This structure declares that PlayerData[PlayerItems] is a keyed array. I have specified that items in that array have an indexed table called ItemTags, as well as an integer value called Count.

After specifying this, data can be added to a Player[PlayerItems] instance following the structure, and it will be saved when they leave or the server shuts down. The Interface:TableToModel() function can be used to instantiate keyed Instances using Lua tables following the structure you have specified:

local model = DataModel.PlayerItems.key -- the structure to follow

Interface:TableToModel(params, model, id).Parent = plr.PlayerItems

here, params is the lua table used for instancing, and id is the id to give the keyed table item you are instancing.

Let me know your thoughts or if you have questions! Don’t be afraid to tell me that this is just bad either.

Example using the most recent structure pictured

Player first joins
image

Instancing script:

local model = game:GetService("ServerStorage").DataModel.PlayerItems.key

local params = {
	Count = 1,
	ItemTags = {
		"Heavy",
		"Sturdy"
	}
}

Interface:TableToModel(params, model, "Rock").Parent = plr.PlayerItems

Player after instancing
image
This is just a one-item example, but you can have as many of these items with unique IDs as you want. The only data saved will be data that follows the structure you have defined

1 Like

Probably the most useful thing i’ve found! Great Work and keep it up!

1 Like

I’m glad you like it! I was praying at least one person would find it helpful. Thank you for the praise

1 Like

10 months later uh? Oh well

Seems quite interesting. I dislike instance based datastore systems, since you can basically do the same and more by using tables stored in a module script, but I can definitely see how, having instances is like, touching and messing with a mechanical system, versus looking at a diagram of it

As for the code, I took a quick look, and it uses basic GetAsync and SetAsync. This means it doesn’t have protection against race conditions (a server loading data before the other server saved it). You should probably look into that

2 Likes

This was the problem I faced because I was using this and I think roblox’s datastores went down or whatever and it kinda broke. Profile store is slightly more complicated to set up but a lot easier in the long run.

1 Like

The key to the game is to avoid race conditions, ie, the data you read has since been modified, since the time you read it, and so writing that data will overwrite those changes. UpdateAsync() is a life saver for this kind of stuff, as it provides the previous data, and modifies it, while ensuring it isn’t changed while that happens (by cancelling the operation, and retrying if it does change)

Commonly, the solution is to simply prevent the possibility of the data changing unexpectedly, which is done by ensuring a single server is allowed to modify the data. This solution isn’t so complicated, and I wrote about it here

Note that avoiding race conditions is not the only thing required to avoid duplication exploits. If exploiters are able to break your datastore on command, they can prevent their own data from updating, and so, in combination with a trading system, could allow for duplication

Another solution, is to leverage UpdateAsync()'s ability to modify data. The idea is to, structure updates to the data, in such a way that if Update1 happens after Update2, the result is the same as if Update1 happened first

For example:

local function UpdateBobux(BobuxDiff)
	Datastore:UpdateAsync(Key,function(Data) 
		Data = Data or {}

		Data.Bobux += BobuxDiff
			
		return Data
	end)
end

Doing UpdateBobux(5) before UpdateBobux(3) gives the same result as UpdateBobux(3) and then UpdateBobux(5). So this way, UpdateAsync() ensures there are no race condition, and the way data is updated ensures the order of updates is insignificant

I’ve made a (probably too complicated) module with this method. Although it is some of my cleanest works.
This method is neat, but definitely less versatile, being able to simply write the whole user data is way more convenient than having to handle updates in this way. The method however, is very resilient, you can literally have multiple servers, updating the same data at once, without data loss

You can make a fairly simple datastore system using these concepts, but datastore systems do tend to accumulate a bunch of features, because, well, it’s useful to be able to add fields to the datastore, without having to manually handle data conversion, and this, and that…


I think it’s a bit of a shame that people nowadays avoid making datastore solutions. I guess so it is something where bugs have a big consequence, but it is also a very interesting topic. Roblox’s datastore api gives you all the tools to make a good datastore system, you just have to understand the intricacies that come with these kinds of asynchronous operation

1 Like