· 6 years ago · Mar 07, 2020, 10:18 AM
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
30local Players = game:GetService("Players")
31local RunService = game:GetService("RunService")
32local ServerStorage = game:GetService("ServerStorage")
33
34local Constants = require(script.Constants)
35local Promise = require(script.Promise)
36local SavingMethods = require(script.SavingMethods)
37local Settings = require(script.Settings)
38local TableUtil = require(script.TableUtil)
39local Verifier = require(script.Verifier)
40
41local SaveInStudioObject = ServerStorage:FindFirstChild("SaveInStudio")
42local SaveInStudio = SaveInStudioObject and SaveInStudioObject.Value
43
44local function clone(value)
45 if typeof(value) == "table" then
46 return TableUtil.clone(value)
47 else
48 return value
49 end
50end
51
52--DataStore object
53local DataStore = {}
54
55--Internal functions
56function DataStore:Debug(...)
57 if self.debug then
58 print(...)
59 end
60end
61
62function DataStore:_GetRaw()
63 if self.getRawPromise then
64 return self.getRawPromise
65 end
66
67 self.getRawPromise = self.savingMethod:Get():andThen(function(value)
68 self.value = value
69 self:Debug("value received")
70 self.haveValue = true
71 end):finally(function()
72 self.getting = false
73 end)
74
75 return self.getRawPromise
76end
77
78function DataStore:_Update(dontCallOnUpdate)
79 if not dontCallOnUpdate then
80 for _,callback in pairs(self.callbacks) do
81 callback(self.value, self)
82 end
83 end
84
85 self.haveValue = true
86 self.valueUpdated = true
87end
88
89--Public functions
90
91function DataStore:Get(defaultValue, dontAttemptGet)
92 if dontAttemptGet then
93 return self.value
94 end
95
96 local backupCount = 0
97
98 if not self.haveValue then
99 while not self.haveValue do
100 local success, error = self:_GetRaw():await()
101
102 if not success then
103 if self.backupRetries then
104 backupCount = backupCount + 1
105
106 if backupCount >= self.backupRetries then
107 self.backup = true
108 self.haveValue = true
109 self.value = self.backupValue
110 break
111 end
112 end
113
114 self:Debug("Get returned error:", error)
115 end
116 end
117
118 if self.value ~= nil then
119 for _,modifier in pairs(self.beforeInitialGet) do
120 self.value = modifier(self.value, self)
121 end
122 end
123 end
124
125 local value
126
127 if self.value == nil and defaultValue ~= nil then --not using "not" because false is a possible value
128 value = defaultValue
129 else
130 value = self.value
131 end
132
133 value = clone(value)
134
135 self.value = value
136
137 return value
138end
139
140function DataStore:GetAsync(...)
141 local args = { ... }
142 return Promise.async(function(resolve)
143 resolve(self:Get(unpack(args)))
144 end)
145end
146
147function DataStore:GetTable(default, ...)
148 local success, result = self:GetTableAsync(default, ...):await()
149 if not success then
150 error(result)
151 end
152 return result
153end
154
155function DataStore:GetTableAsync(default, ...)
156 assert(default ~= nil, "You must provide a default value.")
157
158 return self:GetAsync(default, ...):andThen(function(result)
159 return Promise.async(function(resolve)
160 local changed = false
161 assert(
162 typeof(result) == "table",
163 ":GetTable/:GetTableAsync was used when the value in the data store isn't a table."
164 )
165
166 for defaultKey, defaultValue in pairs(default) do
167 if result[defaultKey] == nil then
168 result[defaultKey] = defaultValue
169 changed = true
170 end
171 end
172
173 if changed then
174 self:Set(result)
175 end
176
177 resolve(result)
178 end)
179 end)
180end
181
182function DataStore:Set(value, _dontCallOnUpdate)
183 self.value = clone(value)
184 self:_Update(_dontCallOnUpdate)
185end
186
187function DataStore:Update(updateFunc)
188 self.value = updateFunc(self.value)
189 self:_Update()
190end
191
192function DataStore:Increment(value, defaultValue)
193 self:Set(self:Get(defaultValue) + value)
194end
195
196function DataStore:IncrementAsync(add, defaultValue)
197 self:GetAsync(defaultValue):andThen(function(value)
198 return Promise.promisify(function()
199 self:Set(value + add)
200 end)()
201 end)
202end
203
204function DataStore:OnUpdate(callback)
205 table.insert(self.callbacks, callback)
206end
207
208function DataStore:BeforeInitialGet(modifier)
209 table.insert(self.beforeInitialGet, modifier)
210end
211
212function DataStore:BeforeSave(modifier)
213 self.beforeSave = modifier
214end
215
216function DataStore:AfterSave(callback)
217 table.insert(self.afterSave, callback)
218end
219
220--[[**
221 <description>
222 Adds a backup to the data store if :Get() fails a specified amount of times.
223 Will return the value provided (if the value is nil, then the default value of :Get() will be returned)
224 and mark the data store as a backup store, and attempts to :Save() will not truly save.
225 </description>
226
227 <parameter name = "retries">
228 Number of retries before the backup will be used.
229 </parameter>
230
231 <parameter name = "value">
232 The value to return to :Get() in the case of a failure.
233 You can keep this blank and the default value you provided with :Get() will be used instead.
234 </parameter>
235**--]]
236function DataStore:SetBackup(retries, value)
237 self.backupRetries = retries
238 self.backupValue = value
239end
240
241--[[**
242 <description>
243 Unmark the data store as a backup data store and tell :Get() and reset values to nil.
244 </description>
245**--]]
246function DataStore:ClearBackup()
247 self.backup = nil
248 self.haveValue = false
249 self.value = nil
250end
251
252--[[**
253 <returns>
254 Whether or not the data store is a backup data store and thus won't save during :Save() or call :AfterSave().
255 </returns>
256**--]]
257function DataStore:IsBackup()
258 return self.backup ~= nil --some people haven't learned if x then yet, and will do if x == false then.
259end
260
261--[[**
262 <description>
263 Saves the data to the data store. Called when a player leaves.
264 </description>
265**--]]
266function DataStore:Save()
267 local success, result = self:SaveAsync():await()
268
269 if success then
270 print("saved " .. self.Name)
271 else
272 error(result)
273 end
274end
275
276--[[**
277 <description>
278 Asynchronously saves the data to the data store.
279 </description>
280**--]]
281function DataStore:SaveAsync()
282 return Promise.async(function(resolve, reject)
283 if not self.valueUpdated then
284 warn(("Data store %s was not saved as it was not updated."):format(self.Name))
285 resolve(false)
286 return
287 end
288
289 if RunService:IsStudio() and not SaveInStudio then
290 warn(("Data store %s attempted to save in studio while SaveInStudio is false."):format(self.Name))
291 if not SaveInStudioObject then
292 warn("You can set the value of this by creating a BoolValue named SaveInStudio in ServerStorage.")
293 end
294 resolve(false)
295 return
296 end
297
298 if self.backup then
299 warn("This data store is a backup store, and thus will not be saved.")
300 resolve(false)
301 return
302 end
303
304 if self.value ~= nil then
305 local save = clone(self.value)
306
307 if self.beforeSave then
308 local success, result = pcall(self.beforeSave, save, self)
309
310 if success then
311 save = result
312 else
313 reject(result, Constants.SaveFailure.BeforeSaveError)
314 return
315 end
316 end
317
318 local problem = Verifier.testValidity(save)
319 if problem then
320 reject(problem, Constants.SaveFailure.InvalidData)
321 return
322 end
323
324 return self.savingMethod:Set(save):andThen(function()
325 resolve(true, save)
326 end)
327 end
328 end):andThen(function(saved, save)
329 if saved then
330 for _, afterSave in pairs(self.afterSave) do
331 local success, err = pcall(afterSave, save, self)
332
333 if not success then
334 warn("Error on AfterSave: "..err)
335 end
336 end
337
338 self.valueUpdated = false
339 end
340 end)
341end
342
343function DataStore:BindToClose(callback)
344 table.insert(self.bindToClose, callback)
345end
346
347function DataStore:GetKeyValue(key)
348 return (self.value or {})[key]
349end
350
351function DataStore:SetKeyValue(key, newValue)
352 if not self.value then
353 self.value = self:Get({})
354 end
355
356 self.value[key] = newValue
357end
358
359local CombinedDataStore = {}
360
361do
362 function CombinedDataStore:BeforeInitialGet(modifier)
363 self.combinedBeforeInitialGet = modifier
364 end
365
366 function CombinedDataStore:BeforeSave(modifier)
367 self.combinedBeforeSave = modifier
368 end
369
370 function CombinedDataStore:Get(defaultValue, dontAttemptGet)
371 local tableResult = self.combinedStore:Get({})
372 local tableValue = tableResult[self.combinedName]
373
374 if not dontAttemptGet then
375 if tableValue == nil then
376 tableValue = defaultValue
377 else
378 if self.combinedBeforeInitialGet and not self.combinedInitialGot then
379 tableValue = self.combinedBeforeInitialGet(tableValue)
380 end
381 end
382 end
383
384 self.combinedInitialGot = true
385 tableResult[self.combinedName] = clone(tableValue)
386 self.combinedStore:Set(tableResult, true)
387 return clone(tableValue)
388 end
389
390 function CombinedDataStore:Set(value, dontCallOnUpdate)
391 local tableResult = self.combinedStore:GetTable({})
392 tableResult[self.combinedName] = value
393 self.combinedStore:Set(tableResult, dontCallOnUpdate)
394 self:_Update(dontCallOnUpdate)
395 end
396
397 function CombinedDataStore:Update(updateFunc)
398 self:Set(updateFunc(self:Get()))
399 self:_Update()
400 end
401
402 function CombinedDataStore:Save()
403 self.combinedStore:Save()
404 end
405
406 function CombinedDataStore:OnUpdate(callback)
407 if not self.onUpdateCallbacks then
408 self.onUpdateCallbacks = { callback }
409 else
410 self.onUpdateCallbacks[#self.onUpdateCallbacks + 1] = callback
411 end
412 end
413
414 function CombinedDataStore:_Update(dontCallOnUpdate)
415 if not dontCallOnUpdate then
416 for _, callback in pairs(self.onUpdateCallbacks or {}) do
417 callback(self:Get(), self)
418 end
419 end
420
421 self.combinedStore:_Update(true)
422 end
423
424 function CombinedDataStore:SetBackup(retries)
425 self.combinedStore:SetBackup(retries)
426 end
427end
428
429local DataStoreMetatable = {}
430
431DataStoreMetatable.__index = DataStore
432
433--Library
434local DataStoreCache = {}
435
436local DataStore2 = {}
437local combinedDataStoreInfo = {}
438
439--[[**
440 <description>
441 Run this once to combine all keys provided into one "main key".
442 Internally, this means that data will be stored in a table with the key mainKey.
443 This is used to get around the 2-DataStore2 reliability caveat.
444 </description>
445
446 <parameter name = "mainKey">
447 The key that will be used to house the table.
448 </parameter>
449
450 <parameter name = "...">
451 All the keys to combine under one table.
452 </parameter>
453**--]]
454function DataStore2.Combine(mainKey, ...)
455 for _, name in pairs({...}) do
456 combinedDataStoreInfo[name] = mainKey
457 end
458end
459
460function DataStore2.ClearCache()
461 DataStoreCache = {}
462end
463
464function DataStore2.SaveAll(player)
465 if DataStoreCache[player] then
466 for _, dataStore in pairs(DataStoreCache[player]) do
467 if dataStore.combinedStore == nil then
468 dataStore:Save()
469 end
470 end
471 end
472end
473
474function DataStore2.PatchGlobalSettings(patch)
475 for key, value in pairs(patch) do
476 assert(Settings[key] ~= nil, "No such key exists: " .. key)
477 -- TODO: Implement type checking with this when osyris' t is in
478 Settings[key] = value
479 end
480end
481
482function DataStore2.__call(_, dataStoreName, player)
483 assert(
484 typeof(dataStoreName) == "string" and typeof(player) == "Instance",
485 ("DataStore2() API call expected {string dataStoreName, Instance player}, got {%s, %s}")
486 :format(
487 typeof(dataStoreName),
488 typeof(player)
489 )
490 )
491
492 if DataStoreCache[player] and DataStoreCache[player][dataStoreName] then
493 return DataStoreCache[player][dataStoreName]
494 elseif combinedDataStoreInfo[dataStoreName] then
495 local dataStore = DataStore2(combinedDataStoreInfo[dataStoreName], player)
496
497 dataStore:BeforeSave(function(combinedData)
498 for key in pairs(combinedData) do
499 if combinedDataStoreInfo[key] then
500 local combinedStore = DataStore2(key, player)
501 local value = combinedStore:Get(nil, true)
502 if value ~= nil then
503 if combinedStore.combinedBeforeSave then
504 value = combinedStore.combinedBeforeSave(clone(value))
505 end
506 combinedData[key] = value
507 end
508 end
509 end
510
511 return combinedData
512 end)
513
514 local combinedStore = setmetatable({
515 combinedName = dataStoreName,
516 combinedStore = dataStore,
517 }, {
518 __index = function(_, key)
519 return CombinedDataStore[key] or dataStore[key]
520 end
521 })
522
523 if not DataStoreCache[player] then
524 DataStoreCache[player] = {}
525 end
526
527 DataStoreCache[player][dataStoreName] = combinedStore
528 return combinedStore
529 end
530
531 local dataStore = {}
532
533 dataStore.Name = dataStoreName
534 dataStore.UserId = player.UserId
535
536 dataStore.callbacks = {}
537 dataStore.beforeInitialGet = {}
538 dataStore.afterSave = {}
539 dataStore.bindToClose = {}
540 dataStore.savingMethod = SavingMethods[Settings.SavingMethod].new(dataStore)
541
542 setmetatable(dataStore, DataStoreMetatable)
543
544 local event, fired = Instance.new("BindableEvent"), false
545
546 game:BindToClose(function()
547 if not fired then
548 spawn(function()
549 player.Parent = nil -- Forces AncestryChanged to fire and save the data
550 end)
551
552 event.Event:wait()
553 end
554
555 local value = dataStore:Get(nil, true)
556
557 for _, bindToClose in pairs(dataStore.bindToClose) do
558 bindToClose(player, value)
559 end
560 end)
561
562 local playerLeavingConnection
563 playerLeavingConnection = player.AncestryChanged:Connect(function()
564 if player:IsDescendantOf(game) then return end
565 playerLeavingConnection:Disconnect()
566 dataStore:SaveAsync():andThen(function()
567 print("player left, saved " .. dataStoreName)
568 end):catch(function(error)
569 -- TODO: Something more elegant
570 warn("error when player left! " .. error)
571 end):finally(function()
572 event:Fire()
573 fired = true
574 end)
575
576 delay(40, function() --Give a long delay for people who haven't figured out the cache :^(
577 DataStoreCache[player] = nil
578 end)
579 end)
580
581 if not DataStoreCache[player] then
582 DataStoreCache[player] = {}
583 end
584
585 DataStoreCache[player][dataStoreName] = dataStore
586
587 return dataStore
588end
589
590DataStore2.Constants = Constants
591
592return setmetatable(DataStore2, DataStore2)