Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- local CACHE_EXPIRY_TIME = 60*10
- local PASSIVE_SAVE_FREQUENCY = 60*1
- local PASSIVE_GRANULARITY = 5
- local SERIALIZE = {}
- local DESERIALIZE = {}
- local SAVE_ON_LEAVE = true
- local DEBUG = false
- if game.Players.LocalPlayer then
- error("PlayerDataStore requested on the client, is a server-only module.", 2)
- end
- local function DeepCopy(tb)
- if type(tb) == 'table' then
- local new = {}
- for k, v in pairs(tb) do
- new[k] = DeepCopy(v)
- end
- return new
- else
- return tb
- end
- end
- local function SpawnNow(func)
- local ev = Instance.new('BindableEvent')
- ev.Event:connect(func)
- ev:Fire()
- end
- local SaveData = {}
- function SaveData.new(playerDataStore, userId)
- local this = {}
- this.userId = userId
- this.lastSaved = 0
- this.locked = false
- this.unlocked = Instance.new('BindableEvent')
- this.onupdate = Instance.new('BindableEvent')
- this.regendata = Instance.new('BindableEvent')
- this.dataSet = nil
- this.dirtyKeySet = {}
- this.ownedKeySet = {}
- local function ownKey(key)
- this.ownedKeySet[key] = true
- end
- local function dirtyKey(key)
- this.dirtyKeySet[key] = true
- end
- local function markAsTouched(key)
- ownKey(key)
- playerDataStore:markAsTouched(this)
- end
- local function markAsDirty(key)
- ownKey(key)
- dirtyKey(key)
- playerDataStore:markAsDirty(this)
- end
- function this:makeReady(data)
- this.dataSet = data
- this.lastSaved = tick()
- playerDataStore:markAsTouched(this)
- this.regendata:Fire()
- end
- function this:waitForUnlocked()
- while this.locked do
- this.unlocked.Event:wait()
- end
- end
- function this:lock()
- this.locked = true
- end
- function this:unlock()
- this.locked = false
- this.unlocked:Fire()
- this.regendata:Fire()
- end
- function this:Get(key)
- if type(key) ~= 'string' then
- error("Bad argument #1 to SaveData::Get() (string expected)", 2)
- end
- if DEBUG then
- print("SaveData<"..this.userId..">::Get("..key..")")
- end
- markAsTouched(key)
- local value = this.dataSet[key]
- if value == nil and DESERIALIZE[key] then
- local v = DESERIALIZE[key](nil)
- this.dataSet[key] = v
- return v
- else
- return value
- end
- end
- function this:Set(key, value, allowErase)
- if type(key) ~= 'string' then
- error("Bad argument #1 to SaveData::Set() (string expected)", 2)
- end
- if value == nil and not allowErase then
- error("Attempt to SaveData::Set('"..key.."', nil) without allowErase = true", 2)
- end
- if DEBUG then
- print("SaveData<"..this.userId..">::Set("..key..", "..tostring(value)..")")
- end
- markAsDirty(key)
- local oldvalue = this.dataSet[key]
- this.dataSet[key] = value
- this.onupdate:Fire(key, value, oldvalue)
- end
- function this:Update(keyList, func)
- if type(keyList) ~= 'table' then
- error("Bad argument #1 to SaveData::Update() (table of keys expected)", 2)
- end
- if type(func) ~= 'function' then
- error("Bad argument #2 to SaveData::Update() (function expected)", 2)
- end
- if DEBUG then
- print("SaveData<"..this.userId..">::Update("..table.concat(keyList, ", ")..", "..tostring(func)..")")
- end
- playerDataStore:doUpdate(this, keyList, func)
- end
- function this:Flush()
- if DEBUG then
- print("SaveData<"..this.userId..">::Flush()")
- end
- playerDataStore:doSave(this)
- end
- return this
- end
- local PlayerDataStore = {}
- function PlayerDataStore.new(DATASTORE_NAME, INITIAL_DATA)
- local this = {}
- local DataStoreService = game:GetService('DataStoreService')
- local mDataStore = DataStoreService:GetDataStore(DATASTORE_NAME)
- local mUserIdSaveDataCache = setmetatable({}, {__mode = 'v'})
- local mTouchedSaveDataCacheSet = {}
- local mOnlinePlayerSaveDataMap = {}
- local mDirtySaveDataSet = {}
- local mOnRequestUserIdSet = {}
- local mRequestCompleted = Instance.new('BindableEvent')
- local mSavingCount = 0
- local INITIAL_DATA = INITIAL_DATA or {}
- local function userIdToKey(userId)
- return 'PlayerData_'..userId
- end
- function this:markAsTouched(saveData)
- if DEBUG then print("PlayerDataStore::markAsTouched("..saveData.userId..")") end
- mTouchedSaveDataCacheSet[saveData] = true
- saveData.lastTouched = tick()
- mUserIdSaveDataCache[saveData.userId] = saveData
- end
- function this:markAsDirty(saveData)
- if DEBUG then print("PlayerDataStore::markAsDirty("..saveData.userId..")") end
- this:markAsTouched(saveData)
- mDirtySaveDataSet[saveData] = true
- mUserIdSaveDataCache[saveData.userId] = saveData
- end
- local function initialData(userId)
- return DeepCopy(INITIAL_DATA)
- end
- local function collectDataToSave(saveData)
- local toSave = {}
- local toErase = {}
- for key, _ in pairs(saveData.dirtyKeySet) do
- local value = saveData.dataSet[key]
- if value ~= nil then
- if SERIALIZE[key] then
- toSave[key] = SERIALIZE[key](value)
- else
- toSave[key] = DeepCopy(value)
- end
- else
- table.insert(toErase, key)
- end
- saveData.dirtyKeySet[key] = nil
- end
- return toSave, toErase
- end
- function this:doSave(saveData)
- if DEBUG then print("PlayerDataStore::doSave("..saveData.userId..") {") end
- saveData.lastSaved = tick()
- mDirtySaveDataSet[saveData] = nil
- if next(saveData.dirtyKeySet) then
- local toSave, toErase = collectDataToSave(saveData)
- saveData:waitForUnlocked()
- saveData:lock()
- mSavingCount = mSavingCount + 1
- mDataStore:UpdateAsync(userIdToKey(saveData.userId), function(oldData)
- if not oldData then
- oldData = initialData(saveData.userId)
- end
- if DEBUG then print("\tattempting save:") end
- for key, data in pairs(toSave) do
- if DEBUG then print("\t\tsaving `"..key.."` = "..tostring(data)) end
- oldData[key] = data
- end
- for _, key in pairs(toErase) do
- if DEBUG then print("\t\tsaving `"..key.."` = nil [ERASING])") end
- oldData[key] = nil
- end
- return oldData
- end)
- if DEBUG then print("\t saved.") end
- mSavingCount = mSavingCount - 1
- saveData:unlock()
- elseif DEBUG then
- print("\tnothing to save")
- end
- if DEBUG then print("}") end
- end
- function this:doUpdate(saveData, keyList, updaterFunc)
- if DEBUG then print("PlayerDataStore::doUpdate("..saveData.userId..", {"..table.concat(keyList, ", ").."}, "..tostring(updaterFunc)..") {") end
- saveData:waitForUnlocked()
- saveData:lock()
- mSavingCount = mSavingCount + 1
- saveData.lastSaved = tick()
- mDirtySaveDataSet[saveData] = nil
- local updateKeySet = {}
- for _, key in pairs(keyList) do
- saveData.ownedKeySet[key] = true
- updateKeySet[key] = true
- end
- local toSave, toErase = collectDataToSave(saveData)
- mDataStore:UpdateAsync(userIdToKey(saveData.userId), function(oldData)
- if DEBUG then print("\ttrying update:") end
- if not oldData then
- oldData = initialData(saveData.userId)
- end
- local valueList = {}
- for i, key in pairs(keyList) do
- local value = saveData.dataSet[key]
- if value == nil and DESERIALIZE[key] then
- valueList[i] = DESERIALIZE[key](nil)
- else
- valueList[i] = value
- end
- end
- local results = {updaterFunc(unpack(valueList, 1, #keyList))}
- for i, result in pairs(results) do
- local key = keyList[i]
- if SERIALIZE[key] then
- local serialized = SERIALIZE[key](result)
- if DEBUG then print("\t\tsaving result: `"..key.."` = "..tostring(serialized).." [SERIALIZED]") end
- oldData[key] = serialized
- else
- if DEBUG then print("\t\tsaving result: `"..key.."` = "..tostring(result)) end
- oldData[key] = result
- end
- saveData.dataSet[key] = result
- end
- for key, value in pairs(toSave) do
- if not updateKeySet[key] then
- if DEBUG then print("\t\tsaving unsaved value: `"..key.."` = "..tostring(value)) end
- oldData[key] = value
- end
- end
- for _, key in pairs(toErase) do
- if not updateKeySet[key] then
- if DEBUG then print("\t\tsaving unsaved value: `"..key.."` = nil [ERASING]") end
- oldData[key] = nil
- end
- end
- print("Save Size", #game.HttpService:JSONEncode(oldData)) -- TO REMOVE
- return oldData
- end)
- mSavingCount = mSavingCount - 1
- saveData:unlock()
- if DEBUG then print("}") end
- end
- local function doLoad(userId)
- if DEBUG then print("PlayerDataStore::doLoad("..userId..") {") end
- local saveData;
- saveData = mUserIdSaveDataCache[userId]
- if saveData then
- if DEBUG then print("\tRecord was already in cache") end
- this:markAsTouched(saveData)
- if DEBUG then print("}") end
- return saveData
- end
- if mOnRequestUserIdSet[userId] then
- if DEBUG then print("\tRecord already requested, wait for it...") end
- while true do
- saveData = mRequestCompleted.Event:wait()()
- if saveData.userId == userId then
- this:markAsTouched(saveData)
- if DEBUG then
- print("\tRecord successfully retrieved by another thread")
- print("}")
- end
- return saveData
- end
- end
- else
- if DEBUG then print("\tRequest record...") end
- mOnRequestUserIdSet[userId] = true
- local data = mDataStore:GetAsync(userIdToKey(userId)) or initialData(userId)
- for key, value in pairs(data) do
- if DESERIALIZE[key] then
- data[key] = DESERIALIZE[key](value)
- end
- end
- saveData = SaveData.new(this, userId)
- saveData:makeReady(data)
- this:markAsTouched(saveData)
- mOnRequestUserIdSet[userId] = nil
- mRequestCompleted:Fire(function() return saveData end)
- if DEBUG then
- print("\tRecord successfully retrieved from data store")
- print("}")
- end
- return saveData
- end
- end
- local function HandlePlayer(player)
- if DEBUG then print("PlayerDataStore> Player "..player.userId.." Entered > Load Data") end
- local saveData = doLoad(player.userId)
- if player.Parent then
- mOnlinePlayerSaveDataMap[player] = saveData
- end
- end
- game.Players.PlayerAdded:connect(HandlePlayer)
- for _, player in pairs(game.Players:GetChildren()) do
- if player:IsA('Player') then
- HandlePlayer(player)
- end
- end
- game.Players.PlayerRemoving:connect(function(player)
- local oldSaveData = mOnlinePlayerSaveDataMap[player]
- mOnlinePlayerSaveDataMap[player] = nil
- if SAVE_ON_LEAVE and oldSaveData then
- if DEBUG then print("PlayerDataStore> Player "..player.userId.." Left with data to save > Save Data") end
- this:doSave(oldSaveData)
- end
- end)
- game.OnClose = function()
- if DEBUG then print("PlayerDataStore> OnClose Shutdown\n\tFlushing...") end
- this:FlushAll()
- if DEBUG then print("\tFlushed, additional wait...") end
- while mSavingCount > 0 do
- wait()
- end
- if DEBUG then print("\tShutdown completed normally.") end
- end
- local function removeTimedOutCacheEntries()
- local now = tick()
- for saveData, _ in pairs(mTouchedSaveDataCacheSet) do
- if (now - saveData.lastTouched) > CACHE_EXPIRY_TIME then
- if mDirtySaveDataSet[saveData] then
- if DEBUG then print(">> Cache expired for: "..saveData.userId..", has unsaved changes, wait.") end
- SpawnNow(function() this:doSave(saveData) end)
- else
- if DEBUG then print(">> Cache expired for: "..saveData.userId..", removing.") end
- mTouchedSaveDataCacheSet[saveData] = nil
- end
- end
- end
- end
- local function passiveSaveUnsavedChanges()
- local now = tick()
- for saveData, _ in pairs(mDirtySaveDataSet) do
- if (now - saveData.lastSaved) > PASSIVE_SAVE_FREQUENCY then
- if DEBUG then print("PlayerDataStore>> Passive save for: "..saveData.userId) end
- SpawnNow(function()
- this:doSave(saveData)
- end)
- end
- end
- end
- spawn(function()
- while true do
- removeTimedOutCacheEntries()
- passiveSaveUnsavedChanges()
- wait(PASSIVE_GRANULARITY)
- end
- end)
- function this:GetSaveData(player)
- if not player or not player:IsA('Player') then
- error("Bad argument #1 to PlayerDataStore::GetSaveData(), Player expected", 2)
- end
- return doLoad(player.userId)
- end
- function this:GetSaveDataById(userId)
- if type(userId) ~= 'number' then
- error("Bad argument #1 to PlayerDataStore::GetSaveDataById(), userId expected", 2)
- end
- return doLoad(userId)
- end
- function this:FlushAll()
- local savesRunning = 0
- local complete = Instance.new('BindableEvent')
- for saveData, _ in pairs(mDirtySaveDataSet) do
- SpawnNow(function()
- savesRunning = savesRunning + 1
- this:doSave(saveData)
- savesRunning = savesRunning - 1
- if savesRunning <= 0 then
- complete:Fire()
- end
- end)
- end
- if savesRunning > 0 then
- complete.Event:wait()
- end
- end
- return this
- end
- return PlayerDataStore
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement