· 6 years ago · Mar 16, 2020, 02:38 PM
1--[[
2 DataStore2: A wrapper for data stores that caches, saves player's data, and uses berezaa's method of saving data.
3 Use require(1936396537) to have an updated version of DataStore2.
4
5 DataStore2(dataStoreName, player) - Returns a DataStore2 DataStore
6
7 DataStore2 DataStore:
8 - Get([defaultValue])
9 - Set(value)
10 - Update(updateFunc)
11 - Increment(value, defaultValue)
12 - BeforeInitialGet(modifier)
13 - BeforeSave(modifier)
14 - Save()
15 - SaveAsync()
16 - OnUpdate(callback)
17 - BindToClose(callback)
18
19 local coinStore = DataStore2("Coins", player)
20
21 To give a player coins:
22
23 coinStore:Increment(50)
24
25 To get the current player's coins:
26
27 coinStore:Get()
28--]]
29
30--Required components
31local DataStoreService = game:GetService("DataStoreService")
32local Players = game:GetService("Players")
33local RunService = game:GetService("RunService")
34local ServerStorage = game:GetService("ServerStorage")
35
36local SavingMethods = require(script.SavingMethods)
37local TableUtil = require(script.TableUtil)
38local Verifier = require(script.Verifier)
39
40local SaveInStudioObject = ServerStorage:FindFirstChild("SaveInStudio")
41local SaveInStudio = SaveInStudioObject and SaveInStudioObject.Value
42
43local function clone(value)
44 if typeof(value) == "table" then
45 return TableUtil.clone(value)
46 else
47 return value
48 end
49end
50
51--DataStore object
52local DataStore = {}
53
54--Internal functions
55function DataStore:Debug(...)
56 if self.debug then
57 print(...)
58 end
59end
60
61function DataStore:_GetRaw()
62 if not self.getQueue then
63 self.getQueue = Instance.new("BindableEvent")
64 end
65
66 if self.getting then
67 self:Debug("A _GetRaw is already in motion, just wait until it's done")
68 self.getQueue.Event:wait()
69 self:Debug("Aaand we're back")
70 return
71 end
72
73 self.getting = true
74
75 local success, value = self.savingMethod:Get()
76
77 self.getting = false
78 if not success then
79 error(tostring(value))
80 end
81
82 self.value = value
83
84 self:Debug("value received")
85 self.getQueue:Fire()
86
87 self.haveValue = true
88end
89
90function DataStore:_Update(dontCallOnUpdate)
91 if not dontCallOnUpdate then
92 for _,callback in pairs(self.callbacks) do
93 callback(self.value, self)
94 end
95 end
96
97 self.haveValue = true
98 self.valueUpdated = true
99end
100
101--Public functions
102
103--[[**
104 <description>
105 Gets the result from the data store. Will yield the first time it is called.
106 </description>
107
108 <parameter name = "defaultValue">
109 The default result if there is no result in the data store.
110 </parameter>
111
112 <parameter name = "dontAttemptGet">
113 If there is no cached result, just return nil.
114 </parameter>
115
116 <returns>
117 The value in the data store if there is no cached result. The cached result otherwise.
118 </returns>
119**--]]
120function DataStore:Get(defaultValue, dontAttemptGet)
121 if dontAttemptGet then
122 return self.value
123 end
124
125 local backupCount = 0
126
127 if not self.haveValue then
128 while not self.haveValue do
129 local success, error = pcall(self._GetRaw, self)
130
131 if not success then
132 if self.backupRetries then
133 backupCount = backupCount + 1
134
135 if backupCount >= self.backupRetries then
136 self.backup = true
137 self.haveValue = true
138 self.value = self.backupValue
139 break
140 end
141 end
142
143 self:Debug("Get returned error:", error)
144 end
145 end
146
147 if self.value ~= nil then
148 for _,modifier in pairs(self.beforeInitialGet) do
149 self.value = modifier(self.value, self)
150 end
151 end
152 end
153
154 local value
155
156 if self.value == nil and defaultValue ~= nil then --not using "not" because false is a possible value
157 value = defaultValue
158 else
159 value = self.value
160 end
161
162 value = clone(value)
163
164 self.value = value
165
166 return value
167end
168
169--[[**
170 <description>
171 The same as :Get only it'll check to make sure all keys in the default data provided
172 exist. If not, will pass in the default value only for that key.
173 This is recommended for tables in case you want to add new entries to the table.
174 Note this is not required for tables, it only provides an extra functionality.
175 </description>
176
177 <parameter name = "defaultValue">
178 A table that will have its keys compared to that of the actual data received.
179 </parameter>
180
181 <returns>
182 The value in the data store will all keys from the default value provided.
183 </returns>
184**--]]
185function DataStore:GetTable(default, ...)
186 assert(default ~= nil, "You must provide a default value with :GetTable.")
187
188 local result = self:Get(default, ...)
189 local changed = false
190
191 assert(typeof(result) == "table", ":GetTable was used when the value in the data store isn't a table.")
192
193 for defaultKey, defaultValue in pairs(default) do
194 if result[defaultKey] == nil then
195 result[defaultKey] = defaultValue
196 changed = true
197 end
198 end
199
200 if changed then
201 self:Set(result)
202 end
203
204 return result
205end
206
207--[[**
208 <description>
209 Sets the cached result to the value provided
210 </description>
211
212 <parameter name = "value">
213 The value
214 </parameter>
215**--]]
216function DataStore:Set(value, _dontCallOnUpdate)
217 self.value = clone(value)
218 self:_Update(_dontCallOnUpdate)
219end
220
221--[[**
222 <description>
223 Calls the function provided and sets the cached result.
224 </description>
225
226 <parameter name = "updateFunc">
227 The function
228 </parameter>
229**--]]
230function DataStore:Update(updateFunc)
231 self.value = updateFunc(self.value)
232 self:_Update()
233end
234
235--[[**
236 <description>
237 Increment the cached result by value.
238 </description>
239
240 <parameter name = "value">
241 The value to increment by.
242 </parameter>
243
244 <parameter name = "defaultValue">
245 If there is no cached result, set it to this before incrementing.
246 </parameter>
247**--]]
248function DataStore:Increment(value, defaultValue)
249 self:Set(self:Get(defaultValue) + value)
250end
251
252--[[**
253 <description>
254 Takes a function to be called whenever the cached result updates.
255 </description>
256
257 <parameter name = "callback">
258 The function to call.
259 </parameter>
260**--]]
261function DataStore:OnUpdate(callback)
262 table.insert(self.callbacks, callback)
263end
264
265--[[**
266 <description>
267 Takes a function to be called when :Get() is first called and there is a value in the data store. This function must return a value to set to. Used for deserializing.
268 </description>
269
270 <parameter name = "modifier">
271 The modifier function.
272 </parameter>
273**--]]
274function DataStore:BeforeInitialGet(modifier)
275 table.insert(self.beforeInitialGet, modifier)
276end
277
278--[[**
279 <description>
280 Takes a function to be called before :Save(). This function must return a value that will be saved in the data store. Used for serializing.
281 </description>
282
283 <parameter name = "modifier">
284 The modifier function.
285 </parameter>
286**--]]
287function DataStore:BeforeSave(modifier)
288 self.beforeSave = modifier
289end
290
291--[[**
292 <description>
293 Takes a function to be called after :Save().
294 </description>
295
296 <parameter name = "callback">
297 The callback function.
298 </parameter>
299**--]]
300function DataStore:AfterSave(callback)
301 table.insert(self.afterSave, callback)
302end
303
304--[[**
305 <description>
306 Adds a backup to the data store if :Get() fails a specified amount of times.
307 Will return the value provided (if the value is nil, then the default value of :Get() will be returned)
308 and mark the data store as a backup store, and attempts to :Save() will not truly save.
309 </description>
310
311 <parameter name = "retries">
312 Number of retries before the backup will be used.
313 </parameter>
314
315 <parameter name = "value">
316 The value to return to :Get() in the case of a failure.
317 You can keep this blank and the default value you provided with :Get() will be used instead.
318 </parameter>
319**--]]
320function DataStore:SetBackup(retries, value)
321 self.backupRetries = retries
322 self.backupValue = value
323end
324
325--[[**
326 <description>
327 Unmark the data store as a backup data store and tell :Get() and reset values to nil.
328 </description>
329**--]]
330function DataStore:ClearBackup()
331 self.backup = nil
332 self.haveValue = false
333 self.value = nil
334end
335
336--[[**
337 <returns>
338 Whether or not the data store is a backup data store and thus won't save during :Save() or call :AfterSave().
339 </returns>
340**--]]
341function DataStore:IsBackup()
342 return self.backup ~= nil --some people haven't learned if x then yet, and will do if x == false then.
343end
344
345--[[**
346 <description>
347 Saves the data to the data store. Called when a player leaves.
348 </description>
349**--]]
350function DataStore:Save()
351 if not self.valueUpdated then
352 warn(("Data store %s was not saved as it was not updated."):format(self.Name))
353 return
354 end
355
356 if RunService:IsStudio() and not SaveInStudio then
357 warn(("Data store %s attempted to save in studio while SaveInStudio is false."):format(self.Name))
358 if not SaveInStudioObject then
359 warn("You can set the value of this by creating a BoolValue named SaveInStudio in ServerStorage.")
360 end
361 return
362 end
363
364 if self.backup then
365 warn("This data store is a backup store, and thus will not be saved.")
366 return
367 end
368
369 if self.value ~= nil then
370 local save = clone(self.value)
371
372 if self.beforeSave then
373 local success, newSave = pcall(self.beforeSave, save, self)
374
375 if success then
376 save = newSave
377 else
378 warn("Error on BeforeSave: "..newSave)
379 return
380 end
381 end
382
383 if not Verifier.warnIfInvalid(save) then return warn("Invalid data while saving") end
384
385 local success, problem = self.savingMethod:Set(save)
386
387 if not success then
388 -- TODO: Something more robust than this
389 error("save error! " .. tostring(problem))
390 end
391
392 for _, afterSave in pairs(self.afterSave) do
393 local success, err = pcall(afterSave, save, self)
394
395 if not success then
396 warn("Error on AfterSave: "..err)
397 end
398 end
399
400 print("saved "..self.Name)
401 end
402end
403
404--[[**
405 <description>
406 Asynchronously saves the data to the data store.
407 </description>
408**--]]
409function DataStore:SaveAsync()
410 coroutine.wrap(DataStore.Save)(self)
411end
412
413--[[**
414 <description>
415 Add a function to be called before the game closes. Fired with the player and value of the data store.
416 </description>
417
418 <parameter name = "callback">
419 The callback function.
420 </parameter>
421**--]]
422function DataStore:BindToClose(callback)
423 table.insert(self.bindToClose, callback)
424end
425
426--[[**
427 <description>
428 Gets the value of the cached result indexed by key. Does not attempt to get the current value in the data store.
429 </description>
430
431 <parameter name = "key">
432 The key you're indexing by.
433 </parameter>
434
435 <returns>
436 The value indexed.
437 </returns>
438**--]]
439function DataStore:GetKeyValue(key)
440 return (self.value or {})[key]
441end
442
443--[[**
444 <description>
445 Sets the value of the result in the database with the key and the new value. Attempts to get the value from the data store. Does not call functions fired on update.
446 </description>
447
448 <parameter name = "key">
449 The key to set.
450 </parameter>
451
452 <parameter name = "newValue">
453 The value to set.
454 </parameter>
455**--]]
456function DataStore:SetKeyValue(key, newValue)
457 if not self.value then
458 self.value = self:Get({})
459 end
460
461 self.value[key] = newValue
462end
463
464local CombinedDataStore = {}
465
466do
467 function CombinedDataStore:BeforeInitialGet(modifier)
468 self.combinedBeforeInitialGet = modifier
469 end
470
471 function CombinedDataStore:BeforeSave(modifier)
472 self.combinedBeforeSave = modifier
473 end
474
475 function CombinedDataStore:Get(defaultValue, dontAttemptGet)
476 local tableResult = self.combinedStore:Get({})
477 local tableValue = tableResult[self.combinedName]
478
479 if not dontAttemptGet then
480 if tableValue == nil then
481 tableValue = defaultValue
482 else
483 if self.combinedBeforeInitialGet and not self.combinedInitialGot then
484 tableValue = self.combinedBeforeInitialGet(tableValue)
485 end
486 end
487 end
488
489 self.combinedInitialGot = true
490 tableResult[self.combinedName] = clone(tableValue)
491 self.combinedStore:Set(tableResult, true)
492 return tableValue
493 end
494
495 function CombinedDataStore:Set(value, dontCallOnUpdate)
496 local tableResult = self.combinedStore:GetTable({})
497 tableResult[self.combinedName] = value
498 self.combinedStore:Set(tableResult, dontCallOnUpdate)
499 self:_Update(dontCallOnUpdate)
500 end
501
502 function CombinedDataStore:Update(updateFunc)
503 self:Set(updateFunc(self:Get()))
504 self:_Update()
505 end
506
507 function CombinedDataStore:OnUpdate(callback)
508 if not self.onUpdateCallbacks then
509 self.onUpdateCallbacks = { callback }
510 else
511 self.onUpdateCallbacks[#self.onUpdateCallbacks + 1] = callback
512 end
513 end
514
515 function CombinedDataStore:_Update(dontCallOnUpdate)
516 if not dontCallOnUpdate then
517 for _, callback in pairs(self.onUpdateCallbacks or {}) do
518 callback(self:Get(), self)
519 end
520 end
521
522 self.combinedStore:_Update(true)
523 end
524
525 function CombinedDataStore:SetBackup(retries)
526 self.combinedStore:SetBackup(retries)
527 end
528end
529
530local DataStoreMetatable = {}
531
532DataStoreMetatable.__index = DataStore
533
534--Library
535local DataStoreCache = {}
536
537local DataStore2 = {}
538local combinedDataStoreInfo = {}
539
540--[[**
541 <description>
542 Run this once to combine all keys provided into one "main key".
543 Internally, this means that data will be stored in a table with the key mainKey.
544 This is used to get around the 2-DataStore2 reliability caveat.
545 </description>
546
547 <parameter name = "mainKey">
548 The key that will be used to house the table.
549 </parameter>
550
551 <parameter name = "...">
552 All the keys to combine under one table.
553 </parameter>
554**--]]
555function DataStore2.Combine(mainKey, ...)
556 for _, name in pairs({...}) do
557 combinedDataStoreInfo[name] = mainKey
558 end
559end
560
561function DataStore2.ClearCache()
562 DataStoreCache = {}
563end
564
565function DataStore2:__call(dataStoreName, player)
566 assert(typeof(dataStoreName) == "string" and typeof(player) == "Instance", ("DataStore2() API call expected {string dataStoreName, Instance player}, got {%s, %s}"):format(typeof(dataStoreName), typeof(player)))
567 if DataStoreCache[player] and DataStoreCache[player][dataStoreName] then
568 return DataStoreCache[player][dataStoreName]
569 elseif combinedDataStoreInfo[dataStoreName] then
570 local dataStore = DataStore2(combinedDataStoreInfo[dataStoreName], player)
571
572 dataStore:BeforeSave(function(combinedData)
573 for key in pairs(combinedData) do
574 if combinedDataStoreInfo[key] then
575 local combinedStore = DataStore2(key, player)
576 local value = combinedStore:Get(nil, true)
577 if value ~= nil then
578 if combinedStore.combinedBeforeSave then
579 value = combinedStore.combinedBeforeSave(clone(value))
580 end
581 combinedData[key] = value
582 end
583 end
584 end
585
586 return combinedData
587 end)
588
589 local combinedStore = setmetatable({
590 combinedName = dataStoreName,
591 combinedStore = dataStore
592 }, {
593 __index = function(self, key)
594 return CombinedDataStore[key] or dataStore[key]
595 end
596 })
597
598 if not DataStoreCache[player] then
599 DataStoreCache[player] = {}
600 end
601
602 DataStoreCache[player][dataStoreName] = combinedStore
603 return combinedStore
604 end
605
606 local dataStore = {}
607
608 dataStore.Name = dataStoreName
609 dataStore.UserId = player.UserId
610
611 dataStore.callbacks = {}
612 dataStore.beforeInitialGet = {}
613 dataStore.afterSave = {}
614 dataStore.bindToClose = {}
615 dataStore.savingMethod = SavingMethods.OrderedBackups.new(dataStore)
616
617 setmetatable(dataStore, DataStoreMetatable)
618
619 local event, fired = Instance.new("BindableEvent"), false
620
621 game:BindToClose(function()
622 if not fired then
623 event.Event:wait()
624 end
625
626 local value = dataStore:Get(nil, true)
627
628 for _, bindToClose in pairs(dataStore.bindToClose) do
629 bindToClose(player, value)
630 end
631 end)
632
633 local playerLeavingConnection
634 playerLeavingConnection = player.AncestryChanged:Connect(function()
635 if player:IsDescendantOf(game) then return end
636 playerLeavingConnection:Disconnect()
637 dataStore:Save()
638 event:Fire()
639 fired = true
640
641 delay(40, function() --Give a long delay for people who haven't figured out the cache :^(
642 DataStoreCache[player] = nil
643 end)
644 end)
645
646 if not DataStoreCache[player] then
647 DataStoreCache[player] = {}
648 end
649
650 DataStoreCache[player][dataStoreName] = dataStore
651
652 return dataStore
653end
654
655return setmetatable(DataStore2, DataStore2)