local X={AutoSaveProfiles=30,RobloxWriteCooldown=7,ForceLoadMaxSteps=8,AssumeDeadSessionLock=1800;IssueCountForCriticalState=5;IssueLast=120,CriticalStateLast=120,MetaTagsUpdatedValues={ProfileCreateTime=true;SessionLoadCount=true;ActiveSession=true;ForceLoadSession=true;LastUpdate=true}}local Y do local X={}local w={}function w.Disconnect(X)local Y=X._listener if Y~=nil then local w=X._script_signal local A=w._fire_pointer_stack local e=w._listeners_next local J=w._listeners_back for X=1,w._stack_count,1 do if A[X]==Y then A[X]=e[Y]end end if w._tail_listener==Y then local X=J[Y]if X~=nil then e[X]=nil J[Y]=nil else w._head_listener=nil end w._tail_listener=X elseif w._head_listener==Y then local X=e[Y]J[X]=nil e[Y]=nil w._head_listener=X else local X=e[Y]local w=J[Y]if X~=nil or w~=nil then e[w]=X J[X]=w e[Y]=nil J[Y]=nil end end X._listener=nil w._listener_count-=1 end if X._disconnect_listener~=nil then X._disconnect_listener(X._disconnect_param)X._disconnect_listener=nil end end local A={}function A.Connect(e,X,Y,A)if type(X)~="\102\117\110\099\116\105\111\110"then error("\091\077\097\100\119\111\114\107\083\099\114\105\112\116\083\105\103\110\097\108\093\058 \079\110\108\121 \102\117\110\099\116\105\111\110\115 \099\097\110 \098\101 \112\097\115\115\101\100 \116\111 \083\099\114\105\112\116\083\105\103\110\097\108\058\067\111\110\110\101\099\116\040\041")end local J=e._tail_listener if J==nil then e._head_listener=X e._tail_listener=X e._listener_count+=1 elseif J~=X and e._listeners_next[X]==nil then e._listeners_next[J]=X e._listeners_back[X]=J e._tail_listener=X e._listener_count+=1 end return{_listener=X;_script_signal=e;_disconnect_listener=Y;_disconnect_param=A;Disconnect=w.Disconnect}end function A.GetListenerCount(X)return X._listener_count end function A.Fire(X,...)local Y=X._fire_pointer_stack local w=X._stack_count+1 X._stack_count=w local A=X._listeners_next Y[w]=X._head_listener while true do local X=Y[w]Y[w]=A[X]if X~=nil then(coroutine.wrap(X))(...)else break end end X._stack_count-=1 end function A.FireUntil(Y,X,...)local w=Y._fire_pointer_stack local A=Y._stack_count+1 Y._stack_count=A local e=Y._listeners_next w[A]=Y._head_listener while true do local Y=w[A]w[A]=e[Y]if Y~=nil then(coroutine.wrap(Y))(...)if X()~=true then w[A]=nil break end else break end end Y._stack_count-=1 end function X.NewScriptSignal()return{_fire_pointer_stack={};_stack_count=0,_listener_count=0;_listeners_next={},_listeners_back={},_head_listener=nil,_tail_listener=nil;Connect=A.Connect;GetListenerCount=A.GetListenerCount,Fire=A.Fire;FireUntil=A.FireUntil}end local e=game:GetService("\082\117\110\083\101\114\118\105\099\101")local J=e.Heartbeat Y={NewScriptSignal=X.NewScriptSignal,HeartbeatWait=function(X)if X==nil or X==0 then return J:Wait()else local Y=0 while Y<=X do local X=J:Wait()Y=Y+X end return Y end end;ConnectToOnClose=function(X,Y)if(game:GetService("\082\117\110\083\101\114\118\105\099\101")):IsStudio()==false or Y==true then game:BindToClose(X)end end}end local w={ServiceLocked=false,IssueSignal=Y.NewScriptSignal(),CorruptionSignal=Y.NewScriptSignal(),CriticalState=false;CriticalStateSignal=Y.NewScriptSignal();ServiceIssueCount=0;_active_profile_stores={};_auto_save_list={},_issue_queue={};_critical_state_start=0,_mock_data_store={},_user_mock_data_store={},_use_mock_data_store=false}local A=w._active_profile_stores local e=w._auto_save_list local J=w._issue_queue local P=game:GetService("\068\097\116\097\083\116\111\114\101\083\101\114\118\105\099\101")local i=game:GetService("\082\117\110\083\101\114\118\105\099\101")local D=game.PlaceId local G=game.JobId local I=1 local x=os.clock()local Q=0 local F=0 local H=0 local p=0 local o=i:IsStudio()local b=false local a=false local u=w._mock_data_store local r=w._user_mock_data_store local z={}local W={}local function C(X)local Y={}for X,w in pairs(X)do if type(w)=="\116\097\098\108\101"then Y[X]=C(w)else Y[X]=w end end return Y end local function q(X,Y)for Y,w in pairs(Y)do if type(Y)=="\115\116\114\105\110\103"then if X[Y]==nil then if type(w)=="\116\097\098\108\101"then X[Y]=C(w)else X[Y]=w end elseif type(X[Y])=="\116\097\098\108\101"and type(w)=="\116\097\098\108\101"then q(X[Y],w)end end end end local function c(X,Y,w)return string.format("\091\083\116\111\114\101\058\"\037\115\"\059\037\115\075\101\121\058\"\037\115\"\093",X,Y~=nil and string.format("\083\099\111\112\101\058\"\037\115\"\059",Y)or"",w)end local function O(X,Y)if W[X]~=nil then W[X][Y]=nil if next(W[X])==nil then W[X]=nil end end end local function m(Y,w)if W[Y]~=nil then if W[Y][w]~=nil then local A=W[Y][w]local e=A.Queue if A.CleanupJob==nil then A.CleanupJob=i.Heartbeat:Connect(function()if os.clock()-A.LastWrite>X.RobloxWriteCooldown and#e==0 then A.CleanupJob:Disconnect()O(Y,w)end end)end elseif next(W[Y])==nil then W[Y]=nil end end end local function L(w,A,e)if W[A]==nil then W[A]={}end if W[A][e]==nil then W[A][e]={LastWrite=0;Queue={};CleanupJob=nil}end local J=W[A][e]local P=J.Queue if J.CleanupJob~=nil then J.CleanupJob:Disconnect()J.CleanupJob=nil end if os.clock()-J.LastWrite>X.RobloxWriteCooldown and#P==0 then J.LastWrite=os.clock()return w()else table.insert(P,w)while true do if os.clock()-J.LastWrite>X.RobloxWriteCooldown and P[1]==w then table.remove(P,1)J.LastWrite=os.clock()return w()end Y.HeartbeatWait()end end end local function k(X,Y)local w=W[X]if w~=nil then w=w[Y]return w==nil or#w.Queue==0 end return true end local function v()while b==true do Y.HeartbeatWait()end end local function y(X)while X._is_pending==true do Y.HeartbeatWait()end end local function d(X,Y,A,e)warn("\091\080\114\111\102\105\108\101\083\101\114\118\105\099\101\093\058 \068\097\116\097\083\116\111\114\101 \065\080\073 \101\114\114\111\114 "..(c(Y,A,e)..(" - \""..(tostring(X).."\""))))table.insert(J,os.clock())w.IssueSignal:Fire(tostring(X),Y,e)end local function n(X,Y,A)warn("\091\080\114\111\102\105\108\101\083\101\114\118\105\099\101\093\058 \082\101\115\111\108\118\101\100 \112\114\111\102\105\108\101 \099\111\114\114\117\112\116\105\111\110 "..c(X,Y,A))w.CorruptionSignal:Fire(X,A)end local function N(X,Y,w,A)local e=X[Y]if e==nil then e={}X[Y]=e end local J=A(e[w])if J==nil then return nil else e[w]=C(J)return C(e[w])end end local function t(X)return X[1]==D and X[2]==G end local function T(X,w,A,e)local J local P=false local i,D=pcall(function()if A.WipeProfile~=true then local P=function(X)if X=="\080\082\079\070\073\076\069\095\087\073\080\069\068"then X=nil end local Y=false local w=false local e={0,{}}if X==nil then Y=true elseif type(X)~="\116\097\098\108\101"then Y=true w=true end if type(X)=="\116\097\098\108\101"then if type(X.Data)=="\116\097\098\108\101"and(type(X.MetaData)=="\116\097\098\108\101"and type(X.GlobalUpdates)=="\116\097\098\108\101")then X.WasCorrupted=false e=X.GlobalUpdates if A.ExistingProfileHandle~=nil then A.ExistingProfileHandle(X)end elseif X.Data==nil and(X.MetaData==nil and type(X.GlobalUpdates)=="\116\097\098\108\101")then X.WasCorrupted=false e=X.GlobalUpdates Y=true else Y=true w=true end end if Y==true then X={GlobalUpdates=e}if A.MissingProfileHandle~=nil then A.MissingProfileHandle(X)end end if A.EditProfile~=nil then A.EditProfile(X)end if w==true then X.WasCorrupted=true end return X end if e==true then J=N(r,X._profile_store_lookup,w,P)Y.HeartbeatWait()elseif a==true then J=N(u,X._profile_store_lookup,w,P)Y.HeartbeatWait()else J=L(function()return X._global_data_store:UpdateAsync(w,P)end,X._profile_store_lookup,w)end else if e==true then local A=r[X._profile_store_lookup]if A~=nil then A[w]=nil end P=true Y.HeartbeatWait()elseif a==true then local A=u[X._profile_store_lookup]if A~=nil then A[w]=nil end P=true Y.HeartbeatWait()else J=X._global_data_store:UpdateAsync(w,function()return"\080\082\079\070\073\076\069\095\087\073\080\069\068"end)if J=="\080\082\079\070\073\076\069\095\087\073\080\069\068"then P=true end end end end)if A.WipeProfile==true then return P elseif i==true and type(J)=="\116\097\098\108\101"then if J.WasCorrupted==true then n(X._profile_store_name,X._profile_store_scope,w)end return J else d(D~=nil and D or"\085\110\100\101\102\105\110\101\100 \101\114\114\111\114",X._profile_store_name,X._profile_store_scope,w)return nil end end local function M(X)local Y=table.find(e,X)if Y~=nil then table.remove(e,Y)if Y<I then I=I-1 end if e[I]==nil then I=1 end end end local function S(X)table.insert(e,I,X)if#e>1 then I=I+1 elseif#e==1 then x=os.clock()end end local function B(X)local Y=X._profile_store local w=X._is_user_mock==true and Y._mock_loaded_profiles or Y._loaded_profiles w[X._profile_key]=nil if next(Y._loaded_profiles)==nil and next(Y._mock_loaded_profiles)==nil then local X=table.find(A,Y)if X~=nil then table.remove(A,X)end end M(X)local e local J local P=X.MetaData.ActiveSession if P~=nil then e=P[1]J=P[2]end X._release_listeners:Fire(e,J)end local function f(X,Y,w)local A=X.GlobalUpdates local e=A._pending_update_lock local J=A._pending_update_clear for X,w in ipairs(w[2])do local P for X,Y in ipairs(Y[2])do if Y[1]==w[1]then P=Y break end end local i=false if P==nil or w[2]>P[2]or w[3]~=P[3]then i=true end if i==true then if w[3]==false then local X=false for Y,A in ipairs(e)do if w[1]==A then X=true break end end if X==false then A._new_active_update_listeners:Fire(w[1],w[4])end end if w[3]==true then local X=false for Y,A in ipairs(J)do if w[1]==A then X=true break end end if X==false then A._new_locked_update_listeners:FireUntil(function()return table.find(J,w[1])==nil end,w[1],w[4])end end end end end local function j(w,A)if type(w.Data)~="\116\097\098\108\101"then n(w._profile_store._profile_store_name,w._profile_store._profile_store_scope,w._profile_key)error("\091\080\114\111\102\105\108\101\083\101\114\118\105\099\101\093\058 \080\082\079\070\073\076\069 \068\065\084\065 \067\079\082\082\085\080\084\069\068 \068\085\082\073\078\071 \082\085\078\084\073\077\069\033 \080\114\111\102\105\108\101\058 "..w:Identify())end if A==true then B(w)end H=H+1 local e=w.MetaData.SessionLoadCount local J=true while J==true do if A~=true then J=false end local P=T(w._profile_store,w._profile_key,{ExistingProfileHandle=nil;MissingProfileHandle=nil;EditProfile=function(X)local Y=X.MetaData.ActiveSession local J=X.MetaData.ForceLoadSession local P=X.MetaData.SessionLoadCount local i=false local D=false if type(Y)=="\116\097\098\108\101"then i=t(Y)and P==e end if type(J)=="\116\097\098\108\101"then D=not t(J)end if i==true then local Y=X.GlobalUpdates local e=Y[2]local J=w.GlobalUpdates local P=J._pending_update_lock local i=J._pending_update_clear for X=1,#e,1 do for Y,w in ipairs(P)do if e[X][1]==w then e[X][3]=true break end end end for X,Y in ipairs(i)do for X=1,#e,1 do if e[X][1]==Y and e[X][3]==true then table.remove(e,X)break end end end X.Data=w.Data X.MetaData.MetaTags=w.MetaData.MetaTags X.MetaData.LastUpdate=os.time()if A==true or D==true then X.MetaData.ActiveSession=nil end end end},w._is_user_mock)if P~=nil then J=false local Y=w.GlobalUpdates local A=Y._updates_latest local i=P.GlobalUpdates Y._updates_latest=i local D=w.MetaData local G=P.MetaData for X in pairs(X.MetaTagsUpdatedValues)do D[X]=G[X]end D.MetaTagsLatest=G.MetaTags local I=P.MetaData.ActiveSession local x=P.MetaData.SessionLoadCount local Q=false if type(I)=="\116\097\098\108\101"then Q=t(I)and x==e end local F=w:IsActive()if Q==true then if F==true then f(w,A,i)end else if F==true then B(w)end m(w._profile_store._profile_store_lookup,w._profile_key)if w._hop_ready==false then w._hop_ready=true w._hop_ready_listeners:Fire()end end w.MetaTagsUpdated:Fire(w.MetaData.MetaTagsLatest)elseif J==true then Y.HeartbeatWait()end end H=H-1 end local Z={}Z.__index=Z function Z.GetActiveUpdates(X)local Y={}for w,A in ipairs(X._updates_latest[2])do if A[3]==false then local w=false if X._pending_update_lock~=nil then for X,Y in ipairs(X._pending_update_lock)do if A[1]==Y then w=true break end end end if w==false then table.insert(Y,{A[1],A[4]})end end end return Y end function Z.GetLockedUpdates(X)local Y={}for w,A in ipairs(X._updates_latest[2])do if A[3]==true then local w=false if X._pending_update_clear~=nil then for X,Y in ipairs(X._pending_update_clear)do if A[1]==Y then w=true break end end end if w==false then table.insert(Y,{A[1],A[4]})end end end return Y end function Z.ListenToNewActiveUpdate(Y,X)if type(X)~="\102\117\110\099\116\105\111\110"then error("\091\080\114\111\102\105\108\101\083\101\114\118\105\099\101\093\058 \079\110\108\121 \097 \102\117\110\099\116\105\111\110 \099\097\110 \098\101 \115\101\116 \097\115 \108\105\115\116\101\110\101\114 \105\110 \071\108\111\098\097\108\085\112\100\097\116\101\115\058\076\105\115\116\101\110\084\111\078\101\119\065\099\116\105\118\101\085\112\100\097\116\101\040\041")end local w=Y._profile if Y._update_handler_mode==true then error("\091\080\114\111\102\105\108\101\083\101\114\118\105\099\101\093\058 \067\097\110\'\116 \108\105\115\116\101\110 \116\111 \110\101\119 \103\108\111\098\097\108 \117\112\100\097\116\101\115 \105\110 \080\114\111\102\105\108\101\083\116\111\114\101\058\071\108\111\098\097\108\085\112\100\097\116\101\080\114\111\102\105\108\101\065\115\121\110\099\040\041")elseif Y._new_active_update_listeners==nil then error("\091\080\114\111\102\105\108\101\083\101\114\118\105\099\101\093\058 \067\097\110\'\116 \108\105\115\116\101\110 \116\111 \110\101\119 \103\108\111\098\097\108 \117\112\100\097\116\101\115 \105\110 \118\105\101\119 \109\111\100\101")elseif w:IsActive()==false then return{Disconnect=function() end}end return Y._new_active_update_listeners:Connect(X)end function Z.ListenToNewLockedUpdate(Y,X)if type(X)~="\102\117\110\099\116\105\111\110"then error("\091\080\114\111\102\105\108\101\083\101\114\118\105\099\101\093\058 \079\110\108\121 \097 \102\117\110\099\116\105\111\110 \099\097\110 \098\101 \115\101\116 \097\115 \108\105\115\116\101\110\101\114 \105\110 \071\108\111\098\097\108\085\112\100\097\116\101\115\058\076\105\115\116\101\110\084\111\078\101\119\076\111\099\107\101\100\085\112\100\097\116\101\040\041")end local w=Y._profile if Y._update_handler_mode==true then error("\091\080\114\111\102\105\108\101\083\101\114\118\105\099\101\093\058 \067\097\110\'\116 \108\105\115\116\101\110 \116\111 \110\101\119 \103\108\111\098\097\108 \117\112\100\097\116\101\115 \105\110 \080\114\111\102\105\108\101\083\116\111\114\101\058\071\108\111\098\097\108\085\112\100\097\116\101\080\114\111\102\105\108\101\065\115\121\110\099\040\041")elseif Y._new_locked_update_listeners==nil then error("\091\080\114\111\102\105\108\101\083\101\114\118\105\099\101\093\058 \067\097\110\'\116 \108\105\115\116\101\110 \116\111 \110\101\119 \103\108\111\098\097\108 \117\112\100\097\116\101\115 \105\110 \118\105\101\119 \109\111\100\101")elseif w:IsActive()==false then return{Disconnect=function() end}end return Y._new_locked_update_listeners:Connect(X)end function Z.LockActiveUpdate(Y,X)if type(X)~="\110\117\109\098\101\114"then error("\091\080\114\111\102\105\108\101\083\101\114\118\105\099\101\093\058 \073\110\118\097\108\105\100 \117\112\100\097\116\101\095\105\100")end local w=Y._profile if Y._update_handler_mode==true then error("\091\080\114\111\102\105\108\101\083\101\114\118\105\099\101\093\058 \067\097\110\'\116 \108\111\099\107 \097\099\116\105\118\101 \103\108\111\098\097\108 \117\112\100\097\116\101\115 \105\110 \080\114\111\102\105\108\101\083\116\111\114\101\058\071\108\111\098\097\108\085\112\100\097\116\101\080\114\111\102\105\108\101\065\115\121\110\099\040\041")elseif Y._pending_update_lock==nil then error("\091\080\114\111\102\105\108\101\083\101\114\118\105\099\101\093\058 \067\097\110\'\116 \108\111\099\107 \097\099\116\105\118\101 \103\108\111\098\097\108 \117\112\100\097\116\101\115 \105\110 \118\105\101\119 \109\111\100\101")elseif w:IsActive()==false then error("\091\080\114\111\102\105\108\101\083\101\114\118\105\099\101\093\058 \080\082\079\070\073\076\069 \069\088\080\073\082\069\068 - \067\097\110\'\116 \108\111\099\107 \097\099\116\105\118\101 \103\108\111\098\097\108 \117\112\100\097\116\101\115")end local A=nil for Y,w in ipairs(Y._updates_latest[2])do if w[1]==X then A=w break end end if A~=nil then local w=false for Y,A in ipairs(Y._pending_update_lock)do if X==A then w=true break end end if w==false and A[3]==false then table.insert(Y._pending_update_lock,X)end else error("\091\080\114\111\102\105\108\101\083\101\114\118\105\099\101\093\058 \080\097\115\115\101\100 \110\111\110-\101\120\105\115\116\097\110\116 \117\112\100\097\116\101\095\105\100")end end function Z.ClearLockedUpdate(Y,X)if type(X)~="\110\117\109\098\101\114"then error("\091\080\114\111\102\105\108\101\083\101\114\118\105\099\101\093\058 \073\110\118\097\108\105\100 \117\112\100\097\116\101\095\105\100")end local w=Y._profile if Y._update_handler_mode==true then error("\091\080\114\111\102\105\108\101\083\101\114\118\105\099\101\093\058 \067\097\110\'\116 \099\108\101\097\114 \108\111\099\107\101\100 \103\108\111\098\097\108 \117\112\100\097\116\101\115 \105\110 \080\114\111\102\105\108\101\083\116\111\114\101\058\071\108\111\098\097\108\085\112\100\097\116\101\080\114\111\102\105\108\101\065\115\121\110\099\040\041")elseif Y._pending_update_clear==nil then error("\091\080\114\111\102\105\108\101\083\101\114\118\105\099\101\093\058 \067\097\110\'\116 \099\108\101\097\114 \108\111\099\107\101\100 \103\108\111\098\097\108 \117\112\100\097\116\101\115 \105\110 \118\105\101\119 \109\111\100\101")elseif w:IsActive()==false then error("\091\080\114\111\102\105\108\101\083\101\114\118\105\099\101\093\058 \080\082\079\070\073\076\069 \069\088\080\073\082\069\068 - \067\097\110\'\116 \099\108\101\097\114 \108\111\099\107\101\100 \103\108\111\098\097\108 \117\112\100\097\116\101\115")end local A=nil for Y,w in ipairs(Y._updates_latest[2])do if w[1]==X then A=w break end end if A~=nil then local w=false for Y,A in ipairs(Y._pending_update_clear)do if X==A then w=true break end end if w==false and A[3]==true then table.insert(Y._pending_update_clear,X)end else error("\091\080\114\111\102\105\108\101\083\101\114\118\105\099\101\093\058 \080\097\115\115\101\100 \110\111\110-\101\120\105\115\116\097\110\116 \117\112\100\097\116\101\095\105\100")end end function Z.AddActiveUpdate(Y,X)if type(X)~="\116\097\098\108\101"then error("\091\080\114\111\102\105\108\101\083\101\114\118\105\099\101\093\058 \073\110\118\097\108\105\100 \117\112\100\097\116\101\095\100\097\116\097")end if Y._new_active_update_listeners~=nil then error("\091\080\114\111\102\105\108\101\083\101\114\118\105\099\101\093\058 \067\097\110\'\116 \097\100\100 \097\099\116\105\118\101 \103\108\111\098\097\108 \117\112\100\097\116\101\115 \105\110 \108\111\097\100\101\100 \080\114\111\102\105\108\101\059 \085\115\101 \080\114\111\102\105\108\101\083\116\111\114\101\058\071\108\111\098\097\108\085\112\100\097\116\101\080\114\111\102\105\108\101\065\115\121\110\099\040\041")elseif Y._update_handler_mode~=true then error("\091\080\114\111\102\105\108\101\083\101\114\118\105\099\101\093\058 \067\097\110\'\116 \097\100\100 \097\099\116\105\118\101 \103\108\111\098\097\108 \117\112\100\097\116\101\115 \105\110 \118\105\101\119 \109\111\100\101\059 \085\115\101 \080\114\111\102\105\108\101\083\116\111\114\101\058\071\108\111\098\097\108\085\112\100\097\116\101\080\114\111\102\105\108\101\065\115\121\110\099\040\041")end local w=Y._updates_latest local A=w[1]+1 w[1]=A table.insert(w[2],{A,1;false,X})end function Z.ChangeActiveUpdate(w,X,Y)if type(X)~="\110\117\109\098\101\114"then error("\091\080\114\111\102\105\108\101\083\101\114\118\105\099\101\093\058 \073\110\118\097\108\105\100 \117\112\100\097\116\101\095\105\100")end if type(Y)~="\116\097\098\108\101"then error("\091\080\114\111\102\105\108\101\083\101\114\118\105\099\101\093\058 \073\110\118\097\108\105\100 \117\112\100\097\116\101\095\100\097\116\097")end if w._new_active_update_listeners~=nil then error("\091\080\114\111\102\105\108\101\083\101\114\118\105\099\101\093\058 \067\097\110\'\116 \099\104\097\110\103\101 \097\099\116\105\118\101 \103\108\111\098\097\108 \117\112\100\097\116\101\115 \105\110 \108\111\097\100\101\100 \080\114\111\102\105\108\101\059 \085\115\101 \080\114\111\102\105\108\101\083\116\111\114\101\058\071\108\111\098\097\108\085\112\100\097\116\101\080\114\111\102\105\108\101\065\115\121\110\099\040\041")elseif w._update_handler_mode~=true then error("\091\080\114\111\102\105\108\101\083\101\114\118\105\099\101\093\058 \067\097\110\'\116 \099\104\097\110\103\101 \097\099\116\105\118\101 \103\108\111\098\097\108 \117\112\100\097\116\101\115 \105\110 \118\105\101\119 \109\111\100\101\059 \085\115\101 \080\114\111\102\105\108\101\083\116\111\114\101\058\071\108\111\098\097\108\085\112\100\097\116\101\080\114\111\102\105\108\101\065\115\121\110\099\040\041")end local A=w._updates_latest local e=nil for Y,w in ipairs(A[2])do if X==w[1]then e=w break end end if e~=nil then if e[3]==true then error("\091\080\114\111\102\105\108\101\083\101\114\118\105\099\101\093\058 \067\097\110\'\116 \099\104\097\110\103\101 \108\111\099\107\101\100 \103\108\111\098\097\108 \117\112\100\097\116\101")end e[2]=e[2]+1 e[4]=Y else error("\091\080\114\111\102\105\108\101\083\101\114\118\105\099\101\093\058 \080\097\115\115\101\100 \110\111\110-\101\120\105\115\116\097\110\116 \117\112\100\097\116\101\095\105\100")end end function Z.ClearActiveUpdate(Y,X)if type(X)~="\110\117\109\098\101\114"then error("\091\080\114\111\102\105\108\101\083\101\114\118\105\099\101\093\058 \073\110\118\097\108\105\100 \117\112\100\097\116\101\095\105\100 \097\114\103\117\109\101\110\116")end if Y._new_active_update_listeners~=nil then error("\091\080\114\111\102\105\108\101\083\101\114\118\105\099\101\093\058 \067\097\110\'\116 \099\108\101\097\114 \097\099\116\105\118\101 \103\108\111\098\097\108 \117\112\100\097\116\101\115 \105\110 \108\111\097\100\101\100 \080\114\111\102\105\108\101\059 \085\115\101 \080\114\111\102\105\108\101\083\116\111\114\101\058\071\108\111\098\097\108\085\112\100\097\116\101\080\114\111\102\105\108\101\065\115\121\110\099\040\041")elseif Y._update_handler_mode~=true then error("\091\080\114\111\102\105\108\101\083\101\114\118\105\099\101\093\058 \067\097\110\'\116 \099\108\101\097\114 \097\099\116\105\118\101 \103\108\111\098\097\108 \117\112\100\097\116\101\115 \105\110 \118\105\101\119 \109\111\100\101\059 \085\115\101 \080\114\111\102\105\108\101\083\116\111\114\101\058\071\108\111\098\097\108\085\112\100\097\116\101\080\114\111\102\105\108\101\065\115\121\110\099\040\041")end local w=Y._updates_latest local A=nil local e=nil for Y,w in ipairs(w[2])do if X==w[1]then A=Y e=w break end end if e~=nil then if e[3]==true then error("\091\080\114\111\102\105\108\101\083\101\114\118\105\099\101\093\058 \067\097\110\'\116 \099\108\101\097\114 \108\111\099\107\101\100 \103\108\111\098\097\108 \117\112\100\097\116\101")end table.remove(w[2],A)else error("\091\080\114\111\102\105\108\101\083\101\114\118\105\099\101\093\058 \080\097\115\115\101\100 \110\111\110-\101\120\105\115\116\097\110\116 \117\112\100\097\116\101\095\105\100")end end local h={}h.__index=h function h.IsActive(X)local Y=X._is_user_mock==true and X._profile_store._mock_loaded_profiles or X._profile_store._loaded_profiles return Y[X._profile_key]==X end function h.GetMetaTag(Y,X)local w=Y.MetaData if w==nil then return nil end return Y.MetaData.MetaTags[X]end function h.SetMetaTag(w,X,Y)if type(X)~="\115\116\114\105\110\103"then error("\091\080\114\111\102\105\108\101\083\101\114\118\105\099\101\093\058 \116\097\103\095\110\097\109\101 \109\117\115\116 \098\101 \097 \115\116\114\105\110\103")elseif string.len(X)==0 then error("\091\080\114\111\102\105\108\101\083\101\114\118\105\099\101\093\058 \073\110\118\097\108\105\100 \116\097\103\095\110\097\109\101")end if w._view_mode==true then error("\091\080\114\111\102\105\108\101\083\101\114\118\105\099\101\093\058 \067\097\110\'\116 \115\101\116 \109\101\116\097 \116\097\103 \105\110 \118\105\101\119 \109\111\100\101")end if w:IsActive()==false then error("\091\080\114\111\102\105\108\101\083\101\114\118\105\099\101\093\058 \080\082\079\070\073\076\069 \069\088\080\073\082\069\068 - \077\101\116\097 \116\097\103\115 \099\097\110\'\116 \098\101 \115\101\116")end w.MetaData.MetaTags[X]=Y end function h.Reconcile(X)q(X.Data,X._profile_store._profile_template)end function h.ListenToRelease(Y,X)if type(X)~="\102\117\110\099\116\105\111\110"then error("\091\080\114\111\102\105\108\101\083\101\114\118\105\099\101\093\058 \079\110\108\121 \097 \102\117\110\099\116\105\111\110 \099\097\110 \098\101 \115\101\116 \097\115 \108\105\115\116\101\110\101\114 \105\110 \080\114\111\102\105\108\101\058\076\105\115\116\101\110\084\111\082\101\108\101\097\115\101\040\041")end if Y._view_mode==true then error("\091\080\114\111\102\105\108\101\083\101\114\118\105\099\101\093\058 \067\097\110\'\116 \108\105\115\116\101\110 \116\111 \080\114\111\102\105\108\101 \114\101\108\101\097\115\101 \105\110 \118\105\101\119 \109\111\100\101")end if Y:IsActive()==false then local w local A local e=Y.MetaData.ActiveSession if e~=nil then w=e[1]A=e[2]end X(w,A)return{Disconnect=function() end}else return Y._release_listeners:Connect(X)end end function h.Save(X)if X._view_mode==true then error("\091\080\114\111\102\105\108\101\083\101\114\118\105\099\101\093\058 \067\097\110\'\116 \115\097\118\101 \080\114\111\102\105\108\101 \105\110 \118\105\101\119 \109\111\100\101")end if X:IsActive()==false then error("\091\080\114\111\102\105\108\101\083\101\114\118\105\099\101\093\058 \080\082\079\070\073\076\069 \069\088\080\073\082\069\068 - \067\097\110\'\116 \115\097\118\101 \080\114\111\102\105\108\101")end if k(X._profile_store._profile_store_lookup,X._profile_key)==true then M(X)S(X);(coroutine.wrap(j))(X)end end function h.Release(X)if X._view_mode==true then error("\091\080\114\111\102\105\108\101\083\101\114\118\105\099\101\093\058 \067\097\110\'\116 \114\101\108\101\097\115\101 \080\114\111\102\105\108\101 \105\110 \118\105\101\119 \109\111\100\101")end if X:IsActive()==true then(coroutine.wrap(j))(X,true)end end function h.ListenToHopReady(Y,X)if type(X)~="\102\117\110\099\116\105\111\110"then error("\091\080\114\111\102\105\108\101\083\101\114\118\105\099\101\093\058 \079\110\108\121 \097 \102\117\110\099\116\105\111\110 \099\097\110 \098\101 \115\101\116 \097\115 \108\105\115\116\101\110\101\114 \105\110 \080\114\111\102\105\108\101\058\076\105\115\116\101\110\084\111\072\111\112\082\101\097\100\121\040\041")end if Y._view_mode==true then error("\091\080\114\111\102\105\108\101\083\101\114\118\105\099\101\093\058 \067\097\110\'\116 \108\105\115\116\101\110 \116\111 \072\111\112\082\101\097\100\121 \105\110 \118\105\101\119 \109\111\100\101")end if Y._hop_ready==true then(coroutine.wrap(X))()return{Disconnect=function() end}else return Y._hop_ready_listeners:Connect(X)end end function h.Identify(X)return c(X._profile_store._profile_store_name,X._profile_store._profile_store_scope,X._profile_key)end local g={}g.__index=g function g.LoadProfileAsync(i,e,J,P)if i._profile_template==nil then error("\091\080\114\111\102\105\108\101\083\101\114\118\105\099\101\093\058 \080\114\111\102\105\108\101 \116\101\109\112\108\097\116\101 \110\111\116 \115\101\116 - \080\114\111\102\105\108\101\083\116\111\114\101\058\076\111\097\100\080\114\111\102\105\108\101\065\115\121\110\099\040\041 \108\111\099\107\101\100 \102\111\114 \116\104\105\115 \080\114\111\102\105\108\101\083\116\111\114\101")end if type(e)~="\115\116\114\105\110\103"then error("\091\080\114\111\102\105\108\101\083\101\114\118\105\099\101\093\058 \112\114\111\102\105\108\101\095\107\101\121 \109\117\115\116 \098\101 \097 \115\116\114\105\110\103")elseif string.len(e)==0 then error("\091\080\114\111\102\105\108\101\083\101\114\118\105\099\101\093\058 \073\110\118\097\108\105\100 \112\114\111\102\105\108\101\095\107\101\121")end if type(J)~="\102\117\110\099\116\105\111\110"and(J~="\070\111\114\099\101\076\111\097\100"and J~="\083\116\101\097\108")then error("\091\080\114\111\102\105\108\101\083\101\114\118\105\099\101\093\058 \073\110\118\097\108\105\100 \110\111\116\095\114\101\108\101\097\115\101\100\095\104\097\110\100\108\101\114")end if w.ServiceLocked==true then return nil end y(i)local I=P==z for X,Y in ipairs(A)do if Y._profile_store_lookup==i._profile_store_lookup then local X=I==true and Y._mock_loaded_profiles or Y._loaded_profiles if X[e]~=nil then error("\091\080\114\111\102\105\108\101\083\101\114\118\105\099\101\093\058 \080\114\111\102\105\108\101 "..(c(i._profile_store_name,i._profile_store_scope,e).." \105\115 \097\108\114\101\097\100\121 \108\111\097\100\101\100 \105\110 \116\104\105\115 \115\101\115\115\105\111\110"))end end end F=F+1 local x=J=="\070\111\114\099\101\076\111\097\100"local H=0 local p=x local o=false local b=J=="\083\116\101\097\108"while w.ServiceLocked==false do local P=I==true and i._mock_profile_load_jobs or i._profile_load_jobs local a local u=Q+1 Q=u local r=P[e]if r~=nil then r[1]=u while r[2]==nil do Y.HeartbeatWait()end if r[1]==u then a=r[2]P[e]=nil else F=F-1 return nil end else r={u;nil}P[e]=r r[2]=T(i,e,{ExistingProfileHandle=function(Y)if w.ServiceLocked==false then local w=Y.MetaData.ActiveSession local A=Y.MetaData.ForceLoadSession if w==nil then Y.MetaData.ActiveSession={D,G}Y.MetaData.ForceLoadSession=nil elseif type(w)=="\116\097\098\108\101"then if t(w)==false then local w=Y.MetaData.LastUpdate if w~=nil then if os.time()-w>X.AssumeDeadSessionLock then Y.MetaData.ActiveSession={D,G}Y.MetaData.ForceLoadSession=nil return end end if o==true or b==true then local X=false if A~=nil then X=t(A)end if X==true or b==true then Y.MetaData.ActiveSession={D,G}Y.MetaData.ForceLoadSession=nil end elseif p==true then Y.MetaData.ForceLoadSession={D;G}end else Y.MetaData.ForceLoadSession=nil end end end end;MissingProfileHandle=function(X)X.Data=C(i._profile_template)X.MetaData={ProfileCreateTime=os.time();SessionLoadCount=0,ActiveSession={D;G},ForceLoadSession=nil,MetaTags={}}end,EditProfile=function(X)if w.ServiceLocked==false then local Y=X.MetaData.ActiveSession if Y~=nil and t(Y)==true then X.MetaData.SessionLoadCount=X.MetaData.SessionLoadCount+1 X.MetaData.LastUpdate=os.time()end end end},I)if r[1]==u then a=r[2]P[e]=nil else F=F-1 return nil end end if a~=nil then local P=a.MetaData.ActiveSession if type(P)=="\116\097\098\108\101"then if t(P)==true then a.MetaData.MetaTagsLatest=C(a.MetaData.MetaTags)local X={_updates_latest=a.GlobalUpdates;_pending_update_lock={},_pending_update_clear={},_new_active_update_listeners=Y.NewScriptSignal();_new_locked_update_listeners=Y.NewScriptSignal(),_profile=nil}setmetatable(X,Z)local J={Data=a.Data;MetaData=a.MetaData;MetaTagsUpdated=Y.NewScriptSignal(),GlobalUpdates=X,_profile_store=i,_profile_key=e;_release_listeners=Y.NewScriptSignal();_hop_ready_listeners=Y.NewScriptSignal();_hop_ready=false,_load_timestamp=os.clock();_is_user_mock=I}setmetatable(J,h)X._profile=J if next(i._loaded_profiles)==nil and next(i._mock_loaded_profiles)==nil then table.insert(A,i)end if I==true then i._mock_loaded_profiles[e]=J else i._loaded_profiles[e]=J end S(J)if w.ServiceLocked==true then j(J,true)J=nil end F=F-1 return J else if x==true then local w=a.MetaData.ForceLoadSession local A=false if w~=nil then A=t(w)end if A==true then if p==false then H=H+1 if H==X.ForceLoadMaxSteps then o=true end end Y.HeartbeatWait()else F=F-1 return nil end p=false elseif b==true then Y.HeartbeatWait()else local X=J(P[1],P[2])if X=="\082\101\112\101\097\116"then Y.HeartbeatWait()elseif X=="\067\097\110\099\101\108"then F=F-1 return nil elseif X=="\070\111\114\099\101\076\111\097\100"then x=true p=true Y.HeartbeatWait()elseif X=="\083\116\101\097\108"then b=true Y.HeartbeatWait()else error("\091\080\114\111\102\105\108\101\083\101\114\118\105\099\101\093\058 \073\110\118\097\108\105\100 \114\101\116\117\114\110 \102\114\111\109 \110\111\116\095\114\101\108\101\097\115\101\100\095\104\097\110\100\108\101\114 \040\""..(tostring(X)..("\"\041\040"..(type(X)..("\041\059"..("\n"..(c(i._profile_store_name,i._profile_store_scope,e)..(" \084\114\097\099\101\098\097\099\107\058\n"..debug.traceback()))))))))end end end else F=F-1 return nil end else Y.HeartbeatWait()end end F=F-1 return nil end function g.GlobalUpdateProfileAsync(J,X,A,e)if type(X)~="\115\116\114\105\110\103"or string.len(X)==0 then error("\091\080\114\111\102\105\108\101\083\101\114\118\105\099\101\093\058 \073\110\118\097\108\105\100 \112\114\111\102\105\108\101\095\107\101\121")end if type(A)~="\102\117\110\099\116\105\111\110"then error("\091\080\114\111\102\105\108\101\083\101\114\118\105\099\101\093\058 \073\110\118\097\108\105\100 \117\112\100\097\116\101\095\104\097\110\100\108\101\114")end if w.ServiceLocked==true then return nil end y(J)while w.ServiceLocked==false do local w=T(J,X,{ExistingProfileHandle=nil,MissingProfileHandle=nil;EditProfile=function(X)local Y={_updates_latest=X.GlobalUpdates,_update_handler_mode=true}setmetatable(Y,Z)A(Y)end},e==z)m(J._profile_store_lookup,X)if w~=nil then local X={_updates_latest=w.GlobalUpdates}setmetatable(X,Z)return X else Y.HeartbeatWait()end end return nil end function g.ViewProfileAsync(e,X,A)if type(X)~="\115\116\114\105\110\103"or string.len(X)==0 then error("\091\080\114\111\102\105\108\101\083\101\114\118\105\099\101\093\058 \073\110\118\097\108\105\100 \112\114\111\102\105\108\101\095\107\101\121")end if w.ServiceLocked==true then return nil end y(e)while w.ServiceLocked==false do local w=T(e,X,{ExistingProfileHandle=nil;MissingProfileHandle=nil;EditProfile=nil},A==z)m(e._profile_store_lookup,X)if w~=nil then local A={_updates_latest=w.GlobalUpdates;_profile=nil}setmetatable(A,Z)local J={Data=w.Data;MetaData=w.MetaData;MetaTagsUpdated=Y.NewScriptSignal(),GlobalUpdates=A,_profile_store=e;_profile_key=X;_view_mode=true,_load_timestamp=os.clock()}setmetatable(J,h)A._profile=J return J else Y.HeartbeatWait()end end return nil end function g.WipeProfileAsync(A,X,Y)if type(X)~="\115\116\114\105\110\103"or string.len(X)==0 then error("\091\080\114\111\102\105\108\101\083\101\114\118\105\099\101\093\058 \073\110\118\097\108\105\100 \112\114\111\102\105\108\101\095\107\101\121")end if w.ServiceLocked==true then return false end y(A)local e=T(A,X,{WipeProfile=true},Y==z)m(A._profile_store_lookup,X)return e end function w.GetProfileStore(X,Y)local w local A=nil if type(X)=="\115\116\114\105\110\103"then w=X elseif type(X)=="\116\097\098\108\101"then w=X.Name A=X.Scope else error("\091\080\114\111\102\105\108\101\083\101\114\118\105\099\101\093\058 \073\110\118\097\108\105\100 \111\114 \109\105\115\115\105\110\103 \112\114\111\102\105\108\101\095\115\116\111\114\101\095\105\110\100\101\120")end if w==nil or type(w)~="\115\116\114\105\110\103"then error("\091\080\114\111\102\105\108\101\083\101\114\118\105\099\101\093\058 \077\105\115\115\105\110\103 \111\114 \105\110\118\097\108\105\100 \"\078\097\109\101\" \112\097\114\097\109\101\116\101\114")elseif string.len(w)==0 then error("\091\080\114\111\102\105\108\101\083\101\114\118\105\099\101\093\058 \080\114\111\102\105\108\101\083\116\111\114\101 \110\097\109\101 \099\097\110\110\111\116 \098\101 \097\110 \101\109\112\116\121 \115\116\114\105\110\103")end if A~=nil and(type(A)~="\115\116\114\105\110\103"or string.len(A)==0)then error("\091\080\114\111\102\105\108\101\083\101\114\118\105\099\101\093\058 \073\110\118\097\108\105\100 \"\083\099\111\112\101\" \112\097\114\097\109\101\116\101\114")end if type(Y)~="\116\097\098\108\101"then error("\091\080\114\111\102\105\108\101\083\101\114\118\105\099\101\093\058 \073\110\118\097\108\105\100 \112\114\111\102\105\108\101\095\116\101\109\112\108\097\116\101")end local e e={Mock={LoadProfileAsync=function(X,Y,w)return e:LoadProfileAsync(Y,w,z)end;GlobalUpdateProfileAsync=function(X,Y,w)return e:GlobalUpdateProfileAsync(Y,w,z)end;ViewProfileAsync=function(X,Y)return e:ViewProfileAsync(Y,z)end,WipeProfileAsync=function(X,Y)return e:WipeProfileAsync(Y,z)end};_profile_store_name=w,_profile_store_scope=A;_profile_store_lookup=w..("\000"..(A or"")),_profile_template=Y;_global_data_store=nil;_loaded_profiles={},_profile_load_jobs={};_mock_loaded_profiles={},_mock_profile_load_jobs={},_is_pending=false}setmetatable(e,g)if b==true then e._is_pending=true;(coroutine.wrap(function()v()if a==false then e._global_data_store=P:GetDataStore(w,A)end e._is_pending=false end))()else if a==false then e._global_data_store=P:GetDataStore(w,A)end end return e end if o==true then b=true;(coroutine.wrap(function()local X,Y=pcall(function()(P:GetDataStore("\095\095\095\095\080\083")):SetAsync("\095\095\095\095\080\083",os.time())end)local A=X==false and string.find(Y,"\067\111\110\110\101\099\116\070\097\105\108",1,true)~=nil if A==true then warn("\091\080\114\111\102\105\108\101\083\101\114\118\105\099\101\093\058 \078\111 \105\110\116\101\114\110\101\116 \097\099\099\101\115\115 - \099\104\101\099\107 \121\111\117\114 \110\101\116\119\111\114\107 \099\111\110\110\101\099\116\105\111\110")end if X==false and(string.find(Y,"\052\048\051",1,true)~=nil or string.find(Y,"\109\117\115\116 \112\117\098\108\105\115\104",1,true)~=nil or A==true)then a=true w._use_mock_data_store=true print("\091\080\114\111\102\105\108\101\083\101\114\118\105\099\101\093\058 \082\111\098\108\111\120 \065\080\073 \115\101\114\118\105\099\101\115 \117\110\097\118\097\105\108\097\098\108\101 - \100\097\116\097 \119\105\108\108 \110\111\116 \098\101 \115\097\118\101\100")else print("\091\080\114\111\102\105\108\101\083\101\114\118\105\099\101\093\058 \082\111\098\108\111\120 \065\080\073 \115\101\114\118\105\099\101\115 \097\118\097\105\108\097\098\108\101 - \100\097\116\097 \119\105\108\108 \098\101 \115\097\118\101\100")end b=false end))()end i.Heartbeat:Connect(function()local Y=#e if Y>0 then local w=X.AutoSaveProfiles/Y local A=os.clock()while A-x>w do x=x+w local J=e[I]if A-J._load_timestamp<X.AutoSaveProfiles then J=nil for w=1,Y-1,1 do I=I+1 if I>Y then I=1 end J=e[I]if A-J._load_timestamp>=X.AutoSaveProfiles then break else J=nil end end end I=I+1 if I>Y then I=1 end if J~=nil then(coroutine.wrap(j))(J)end end end if w.CriticalState==false then if#J>=X.IssueCountForCriticalState then w.CriticalState=true w.CriticalStateSignal:Fire(true)p=os.clock()warn("\091\080\114\111\102\105\108\101\083\101\114\118\105\099\101\093\058 \069\110\116\101\114\101\100 \099\114\105\116\105\099\097\108 \115\116\097\116\101")end else if#J>=X.IssueCountForCriticalState then p=os.clock()elseif os.clock()-p>X.CriticalStateLast then w.CriticalState=false w.CriticalStateSignal:Fire(false)warn("\091\080\114\111\102\105\108\101\083\101\114\118\105\099\101\093\058 \067\114\105\116\105\099\097\108 \115\116\097\116\101 \101\110\100\101\100")end end while true do local Y=J[1]if Y==nil then break elseif os.clock()-Y>X.IssueLast then table.remove(J,1)else break end end end);(coroutine.wrap(function()v()Y.ConnectToOnClose(function()w.ServiceLocked=true local X=0 local A={}for X,Y in ipairs(e)do A[X]=Y end for Y,w in ipairs(A)do if w:IsActive()==true then X=X+1;(coroutine.wrap(function()j(w,true)X=X-1 end))()end end while X>0 or F>0 or H>0 do Y.HeartbeatWait()end return end,a==false)end))()return w
-- local Madwork = _G.Madwork
--[[
{Madwork}

-[ProfileService]---------------------------------------
	(STANDALONE VERSION)
	DataStore profiles - universal session-locked savable table API
	
	Official documentation:
		https://madstudioroblox.github.io/ProfileService/

	DevForum discussion:
		https://devforum.roblox.com/t/ProfileService/667805
	
	WARNINGS FOR "Profile.Data" VALUES:
	 	! Do not create numeric tables with gaps - attempting to replicate such tables will result in an error;
		! Do not create mixed tables (some values indexed by number and others by string key), as only
		     the data indexed by number will be replicated.
		! Do not index tables by anything other than numbers and strings.
		! Do not reference Roblox Instances
		! Do not reference userdata (Vector3, Color3, CFrame...) - Serialize userdata before referencing
		! Do not reference functions
		
	WARNING: Calling ProfileStore:LoadProfileAsync() with a "profile_key" which wasn't released in the SAME SESSION will result
		in an error! If you want to "ProfileStore:LoadProfileAsync()" instead of using the already loaded profile, :Release()
		the old Profile object.
		
	Members:
	
		ProfileService.ServiceLocked         [bool]
		
		ProfileService.IssueSignal           [ScriptSignal] (error_message, profile_store_name, profile_key)
		ProfileService.CorruptionSignal      [ScriptSignal] (profile_store_name, profile_key)
		ProfileService.CriticalStateSignal   [ScriptSignal] (is_critical_state)
	
	Functions:
	
		ProfileService.GetProfileStore(profile_store_index, profile_template) --> [ProfileStore]
			profile_store_index   [string] -- DataStore name
			OR
			profile_store_index   [table]: -- Allows the developer to define more GlobalDataStore variables
				{
					Name = "StoreName", -- [string] -- DataStore name
					-- Optional arguments:
					Scope = "StoreScope", -- [string] -- DataStore scope
				}
			profile_template      [table] -- Profiles will default to given table (hard-copy) when no data was saved previously
				
	Members [ProfileStore]:
	
		ProfileStore.Mock   [ProfileStore] -- Reflection of ProfileStore methods, but the methods will use a mock DataStore
		
	Methods [ProfileStore]:
	
		ProfileStore:LoadProfileAsync(profile_key, not_released_handler) --> [Profile / nil] not_released_handler(place_id, game_job_id)
		ProfileStore:GlobalUpdateProfileAsync(profile_key, update_handler) --> [GlobalUpdates / nil] (update_handler(GlobalUpdates))
			-- Returns GlobalUpdates object if update was successful, otherwise returns nil
		
		ProfileStore:ViewProfileAsync(profile_key) --> [Profile / nil] -- Notice #1: Profile object methods will not be available;
			Notice #2: Profile object members will be nil (Profile.Data = nil, Profile.MetaData = nil) if the profile hasn't
			been created, with the exception of Profile.GlobalUpdates which could be empty or populated by
			ProfileStore:GlobalUpdateProfileAsync()
			
		ProfileStore:WipeProfileAsync(profile_key) --> is_wipe_successful [bool] -- Completely wipes out profile data from the
			DataStore / mock DataStore with no way to recover it.
		
		* Parameter description for "ProfileStore:LoadProfileAsync()":
		
			profile_key            [string] -- DataStore key
			not_released_handler = "ForceLoad" -- Force loads profile on first call
			OR
			not_released_handler = "Steal" -- Steals the profile ignoring it's session lock
			OR
			not_released_handler   [function] (place_id, game_job_id) --> [string] ("Repeat" / "Cancel" / "ForceLoad")
				-- "not_released_handler" will be triggered in cases where the profile is not released by a session. This
				function may yield for as long as desirable and must return one of three string values:
					["Repeat"] - ProfileService will repeat the profile loading proccess and may trigger the release handler again
					["Cancel"] - ProfileStore:LoadProfileAsync() will immediately return nil
					["ForceLoad"] - ProfileService will repeat the profile loading call, but will return Profile object afterwards
						and release the profile for another session that has loaded the profile
					["Steal"] - The profile will usually be loaded immediately, ignoring an existing remote session lock and applying
						a session lock for this session.
						
		* Parameter description for "ProfileStore:GlobalUpdateProfileAsync()":
		
			profile_key      [string] -- DataStore key
			update_handler   [function] (GlobalUpdates) -- This function gains access to GlobalUpdates object methods
				(update_handler can't yield)
		
	Members [Profile]:
	
		Profile.Data            [table] -- Writable table that gets saved automatically and once the profile is released
		Profile.MetaData        [table] (Read-only) -- Information about this profile
		
			Profile.MetaData.ProfileCreateTime   [number] (Read-only) -- os.time() timestamp of profile creation
			Profile.MetaData.SessionLoadCount    [number] (Read-only) -- Amount of times the profile was loaded
			Profile.MetaData.ActiveSession       [table] (Read-only) {place_id, game_job_id} / nil -- Set to a session link if a
				game session is currently having this profile loaded; nil if released
			Profile.MetaData.MetaTags            [table] {["tag_name"] = tag_value, ...} -- Saved and auto-saved just like Profile.Data
			Profile.MetaData.MetaTagsLatest      [table] (Read-only) -- Latest version of MetaData.MetaTags that was definetly saved to DataStore
				(You can use Profile.MetaData.MetaTagsLatest for product purchase save confirmation, but create a system to clear old tags after
				they pile up)

		Profile.MetaTagsUpdated [ScriptSignal] (meta_tags_latest) -- Fires after every auto-save, after
			--	Profile.MetaData.MetaTagsLatest has been updated with the version that's guaranteed to be saved;
			--  .MetaTagsUpdated will fire regardless of whether .MetaTagsLatest changed after update;
			--	.MetaTagsUpdated may fire after the Profile is released - changes to Profile.Data are not saved
			--	after release.
		
		Profile.GlobalUpdates   [GlobalUpdates]
		
	Methods [Profile]:
	
		-- SAFE METHODS - Will not error after profile expires:
		Profile:IsActive() --> [bool] -- Returns true while the profile is active and can be written to
			
		Profile:GetMetaTag(tag_name) --> value
		
		Profile:Reconcile() -- Fills in missing (nil) [string_key] = [value] pairs to the Profile.Data structure
		
		Profile:ListenToRelease(listener) --> [ScriptConnection] (place_id / nil, game_job_id / nil) -- WARNING: Profiles can be released externally if another session
			force-loads this profile - use :ListenToRelease() to handle player leaving cleanup.
			
		Profile:Release() -- Call after the session has finished working with this profile
			e.g., after the player leaves (Profile object will become expired) (Does not yield)

		Profile:ListenToHopReady(listener) --> [ScriptConnection] () -- Passed listener will be executed after the releasing UpdateAsync call finishes;
			--	Wrap universe teleport requests with this method AFTER releasing the profile to improve session lock sharing between universe places;
			--  :ListenToHopReady() will usually call the listener in around a second, but may ocassionally take up to 7 seconds when a release happens
			--	next to an auto-update in regular usage scenarios.

		Profile:Identify() --> [string] -- Returns a string containing DataStore name, scope and key; Used for debug;
			-- Example return: "[Store:"GameData";Scope:"Live";Key:"Player_2312310"]"
		
		-- DANGEROUS METHODS - Will error if the profile is expired:
		-- MetaTags - Save and read values stored in Profile.MetaData for storing info about the
			profile itself like "Profile:SetMetaTag("FirstTimeLoad", true)"
		Profile:SetMetaTag(tag_name, value)
		
		Profile:Save() -- Call to quickly progress global update state or to speed up save validation processes (Does not yield)

		
	Methods [GlobalUpdates]:
	
	-- ALWAYS PUBLIC:
		GlobalUpdates:GetActiveUpdates() --> [table] {{update_id, update_data}, ...}
		GlobalUpdates:GetLockedUpdates() --> [table] {{update_id, update_data}, ...}
		
	-- ONLY WHEN FROM "Profile.GlobalUpdates":
		GlobalUpdates:ListenToNewActiveUpdate(listener) --> [ScriptConnection] listener(update_id, update_data)
		GlobalUpdates:ListenToNewLockedUpdate(listener) --> [ScriptConnection] listener(update_id, update_data)
		-- WARNING: GlobalUpdates:LockUpdate() and GlobalUpdates:ClearLockedUpdate() will error after profile expires
		GlobalUpdates:LockActiveUpdate(update_id)
		GlobalUpdates:ClearLockedUpdate(update_id)
		
	-- EXPOSED TO "update_handler" DURING ProfileStore:GlobalUpdateProfileAsync() CALL
		GlobalUpdates:AddActiveUpdate(update_data)
		GlobalUpdates:ChangeActiveUpdate(update_id, update_data)
		GlobalUpdates:ClearActiveUpdate(update_id)
		
--]]

local SETTINGS = {

	AutoSaveProfiles = 30, -- Seconds (This value may vary - ProfileService will split the auto save load evenly in the given time)
	RobloxWriteCooldown = 7, -- Seconds between successive DataStore calls for the same key
	ForceLoadMaxSteps = 8, -- Steps taken before ForceLoad request steals the active session for a profile
	AssumeDeadSessionLock = 30 * 60, -- (seconds) If a profile hasn't been updated for 30 minutes, assume the session lock is dead
		-- As of writing, os.time() is not completely reliable, so we can only assume session locks are dead after a significant amount of time.
	
	IssueCountForCriticalState = 5, -- Issues to collect to announce critical state
	IssueLast = 120, -- Seconds
	CriticalStateLast = 120, -- Seconds
	
	MetaTagsUpdatedValues = { -- Technical stuff - do not alter
		ProfileCreateTime = true,
		SessionLoadCount = true,
		ActiveSession = true,
		ForceLoadSession = true,
		LastUpdate = true,
	}
	
}

local Madwork -- Standalone Madwork reference for portable version of ProfileService
do

	local MadworkScriptSignal = {}
	
	local ScriptConnection = {}
	
	function ScriptConnection:Disconnect()
		local listener = self._listener
		if listener ~= nil then
			local script_signal = self._script_signal
			local fire_pointer_stack = script_signal._fire_pointer_stack
			local listeners_next = script_signal._listeners_next
			local listeners_back = script_signal._listeners_back
			-- Check fire pointers:
			for i = 1, script_signal._stack_count do
				if fire_pointer_stack[i] == listener then
					fire_pointer_stack[i] = listeners_next[listener]
				end
			end
			-- Remove listener:
			if script_signal._tail_listener == listener then
				local new_tail = listeners_back[listener]
				if new_tail ~= nil then
					listeners_next[new_tail] = nil
					listeners_back[listener] = nil
				else
					script_signal._head_listener = nil -- tail was also head
				end
				script_signal._tail_listener = new_tail
			elseif script_signal._head_listener == listener then
				-- If this listener is not the tail, assume another listener is the tail:
				local new_head = listeners_next[listener]
				listeners_back[new_head] = nil
				listeners_next[listener] = nil
				script_signal._head_listener = new_head
			else
				local next_listener = listeners_next[listener]
				local back_listener = listeners_back[listener]
				if next_listener ~= nil or back_listener ~= nil then -- Catch cases when duplicate listeners are disconnected
					listeners_next[back_listener] = next_listener
					listeners_back[next_listener] = back_listener
					listeners_next[listener] = nil
					listeners_back[listener] = nil
				end
			end
			self._listener = nil
			script_signal._listener_count -= 1
		end
		if self._disconnect_listener ~= nil then
			self._disconnect_listener(self._disconnect_param)
			self._disconnect_listener = nil
		end
	end
	
	local ScriptSignal = {}
	
	function ScriptSignal:Connect(listener, disconnect_listener, disconnect_param) --> [ScriptConnection]
		if type(listener) ~= "function" then
			error("[MadworkScriptSignal]: Only functions can be passed to ScriptSignal:Connect()")
		end
		local tail_listener = self._tail_listener
		if tail_listener == nil then
			self._head_listener = listener
			self._tail_listener = listener
			self._listener_count += 1
		elseif tail_listener ~= listener and self._listeners_next[listener] == nil then -- Prevent connecting the same listener more than once
			self._listeners_next[tail_listener] = listener
			self._listeners_back[listener] = tail_listener
			self._tail_listener = listener
			self._listener_count += 1
		end
		return {
			_listener = listener,
			_script_signal = self,
			_disconnect_listener = disconnect_listener,
			_disconnect_param = disconnect_param,
			Disconnect = ScriptConnection.Disconnect
		}
	end
	
	function ScriptSignal:GetListenerCount()
		return self._listener_count
	end
	
	function ScriptSignal:Fire(...)
		local fire_pointer_stack = self._fire_pointer_stack
		local stack_id = self._stack_count + 1
		self._stack_count = stack_id
		
		local listeners_next = self._listeners_next
		fire_pointer_stack[stack_id] = self._head_listener
		while true do
			local pointer = fire_pointer_stack[stack_id]
			fire_pointer_stack[stack_id] = listeners_next[pointer]
			if pointer ~= nil then
				coroutine.wrap(pointer)(...)
			else
				break
			end
		end
		self._stack_count -= 1
	end

	function ScriptSignal:FireUntil(continue_callback, ...)
		local fire_pointer_stack = self._fire_pointer_stack
		local stack_id = self._stack_count + 1
		self._stack_count = stack_id
	
		local listeners_next = self._listeners_next
		fire_pointer_stack[stack_id] = self._head_listener
		while true do
			local pointer = fire_pointer_stack[stack_id]
			fire_pointer_stack[stack_id] = listeners_next[pointer]
			if pointer ~= nil then
				coroutine.wrap(pointer)(...)
				if continue_callback() ~= true then
					fire_pointer_stack[stack_id] = nil
					break
				end
			else
				break
			end
		end
		self._stack_count -= 1
	end
	
	function MadworkScriptSignal.NewScriptSignal() --> [ScriptSignal]
		return {
			_fire_pointer_stack = {},
			_stack_count = 0,
			_listener_count = 0,
			_listeners_next = {},
			_listeners_back = {},
			_head_listener = nil,
			_tail_listener = nil,
			Connect = ScriptSignal.Connect,
			GetListenerCount = ScriptSignal.GetListenerCount,
			Fire = ScriptSignal.Fire,
			FireUntil = ScriptSignal.FireUntil,
		}
	end
	
	local RunService = game:GetService("RunService")
	local Heartbeat = RunService.Heartbeat
	
	Madwork = {
		NewScriptSignal = MadworkScriptSignal.NewScriptSignal,
		HeartbeatWait = function(wait_time) --> time_elapsed
			if wait_time == nil or wait_time == 0 then
				return Heartbeat:Wait()
			else
				local time_elapsed = 0
				while time_elapsed <= wait_time do
					local time_waited = Heartbeat:Wait()
					time_elapsed = time_elapsed + time_waited
				end
				return time_elapsed
			end
		end,
		ConnectToOnClose = function(task, run_in_studio_mode)
			if game:GetService("RunService"):IsStudio() == false or run_in_studio_mode == true then
				game:BindToClose(task)
			end
		end,
	}
end

----- Service Table -----

local ProfileService = {

	ServiceLocked = false, -- Set to true once the server is shutting down

	IssueSignal = Madwork.NewScriptSignal(), -- (error_message, profile_store_name, profile_key) -- Fired when a DataStore API call throws an error
	CorruptionSignal = Madwork.NewScriptSignal(), -- (profile_store_name, profile_key) -- Fired when DataStore key returns a value that has
	-- all or some of it's profile components set to invalid data types. E.g., accidentally setting Profile.Data to a noon table value

	CriticalState = false, -- Set to true while DataStore service is throwing too many errors
	CriticalStateSignal = Madwork.NewScriptSignal(), -- (is_critical_state) -- Fired when CriticalState is set to true
	-- (You may alert players with this, or set up analytics)

	ServiceIssueCount = 0,

	_active_profile_stores = {}, -- {profile_store, ...}

	_auto_save_list = {}, -- {profile, ...} -- loaded profile table which will be circularly auto-saved

	_issue_queue = {}, -- [table] {issue_time, ...}
	_critical_state_start = 0, -- [number] 0 = no critical state / os.clock() = critical state start

	-- Debug:
	_mock_data_store = {},
	_user_mock_data_store = {},

	_use_mock_data_store = false,

}

--[[
	Saved profile structure:
	
	DataStoreProfile = {
		Data = {},
		MetaData = {
			ProfileCreateTime = 0,
			SessionLoadCount = 0,
			ActiveSession = {place_id, game_job_id} / nil,
			ForceLoadSession = {place_id, game_job_id} / nil,
			MetaTags = {},
			LastUpdate = 0, -- os.time()
		},
		GlobalUpdates = {
			update_index,
			{
				{update_id, version_id, update_locked, update_data},
				...
			}
		},
	}
	
	OR
	
	DataStoreProfile = {
		GlobalUpdates = {
			update_index,
			{
				{update_id, version_id, update_locked, update_data},
				...
			}
		},
	}
--]]

----- Private Variables -----

local ActiveProfileStores = ProfileService._active_profile_stores
local AutoSaveList = ProfileService._auto_save_list
local IssueQueue = ProfileService._issue_queue

local DataStoreService = game:GetService("DataStoreService")
local RunService = game:GetService("RunService")

local PlaceId = game.PlaceId
local JobId = game.JobId

local AutoSaveIndex = 1 -- Next profile to auto save
local LastAutoSave = os.clock()

local LoadIndex = 0

local ActiveProfileLoadJobs = 0 -- Number of active threads that are loading in profiles
local ActiveProfileSaveJobs = 0 -- Number of active threads that are saving profiles

local CriticalStateStart = 0 -- os.clock()

local IsStudio = RunService:IsStudio()
local IsLiveCheckActive = false

local UseMockDataStore = false
local MockDataStore = ProfileService._mock_data_store -- Mock data store used when API access is disabled

local UserMockDataStore = ProfileService._user_mock_data_store -- Separate mock data store accessed via ProfileStore.Mock
local UseMockTag = {}

local CustomWriteQueue = {
	--[[
		[store] = {
			[key] = {
				LastWrite = os.clock(),
				Queue = {callback, ...},
				CleanupJob = nil,
			},
			...
		},
		...
	--]]
}

----- Utils -----

local function DeepCopyTable(t)
	local copy = {}
	for key, value in pairs(t) do
		if type(value) == "table" then
			copy[key] = DeepCopyTable(value)
		else
			copy[key] = value
		end
	end
	return copy
end

local function ReconcileTable(target, template)
	for k, v in pairs(template) do
		if type(k) == "string" then -- Only string keys will be reconciled
			if target[k] == nil then
				if type(v) == "table" then
					target[k] = DeepCopyTable(v)
				else
					target[k] = v
				end
			elseif type(target[k]) == "table" and type(v) == "table" then
				ReconcileTable(target[k], v)
			end
		end
	end
end

----- Private functions -----

local function IdentifyProfile(store_name, store_scope, key)
	return string.format(
		"[Store:\"%s\";%sKey:\"%s\"]",
		store_name,
		store_scope ~= nil and string.format("Scope:\"%s\";", store_scope) or "",
		key
	)
end

local function CustomWriteQueueCleanup(store, key)
	if CustomWriteQueue[store] ~= nil then
		CustomWriteQueue[store][key] = nil
		if next(CustomWriteQueue[store]) == nil then
			CustomWriteQueue[store] = nil
		end
	end
end

local function CustomWriteQueueMarkForCleanup(store, key)
	if CustomWriteQueue[store] ~= nil then
		if CustomWriteQueue[store][key] ~= nil then

			local queue_data = CustomWriteQueue[store][key]
			local queue = queue_data.Queue

			if queue_data.CleanupJob == nil then

				queue_data.CleanupJob = RunService.Heartbeat:Connect(function()
					if os.clock() - queue_data.LastWrite > SETTINGS.RobloxWriteCooldown and #queue == 0 then
						queue_data.CleanupJob:Disconnect()
						CustomWriteQueueCleanup(store, key)
					end
				end)

			end

		elseif next(CustomWriteQueue[store]) == nil then
			CustomWriteQueue[store] = nil
		end
	end
end

local function CustomWriteQueueAsync(callback, store, key) --> ... -- Passed return from callback

	if CustomWriteQueue[store] == nil then
		CustomWriteQueue[store] = {}
	end
	if CustomWriteQueue[store][key] == nil then
		CustomWriteQueue[store][key] = {LastWrite = 0, Queue = {}, CleanupJob = nil}
	end

	local queue_data = CustomWriteQueue[store][key]
	local queue = queue_data.Queue

	-- Cleanup job:

	if queue_data.CleanupJob ~= nil then
		queue_data.CleanupJob:Disconnect()
		queue_data.CleanupJob = nil
	end

	-- Queue logic:

	if os.clock() - queue_data.LastWrite > SETTINGS.RobloxWriteCooldown and #queue == 0 then
		queue_data.LastWrite = os.clock()
		return callback()
	else
		table.insert(queue, callback)
		while true do
			if os.clock() - queue_data.LastWrite > SETTINGS.RobloxWriteCooldown and queue[1] == callback then
				table.remove(queue, 1)
				queue_data.LastWrite = os.clock()
				return callback()
			end
			Madwork.HeartbeatWait()
		end
	end

end

local function IsCustomWriteQueueEmptyFor(store, key) --> is_empty [bool]
	local lookup = CustomWriteQueue[store]
	if lookup ~= nil then
		lookup = lookup[key]
		return lookup == nil or #lookup.Queue == 0
	end
	return true
end

local function WaitForLiveAccessCheck() -- This function was created to prevent the ProfileService module yielding execution when required
	while IsLiveCheckActive == true do
		Madwork.HeartbeatWait()
	end
end

local function WaitForPendingProfileStore(profile_store)
	while profile_store._is_pending == true do
		Madwork.HeartbeatWait()
	end
end

local function RegisterIssue(error_message, store_name, store_scope, profile_key) -- Called when a DataStore API call errors
	warn("[ProfileService]: DataStore API error " .. IdentifyProfile(store_name, store_scope, profile_key) .. " - \"" .. tostring(error_message) .. "\"")
	table.insert(IssueQueue, os.clock()) -- Adding issue time to queue
	ProfileService.IssueSignal:Fire(tostring(error_message), store_name, profile_key)
end

local function RegisterCorruption(store_name, store_scope, profile_key) -- Called when a corrupted profile is loaded
	warn("[ProfileService]: Resolved profile corruption " .. IdentifyProfile(store_name, store_scope, profile_key))
	ProfileService.CorruptionSignal:Fire(store_name, profile_key)
end

local function MockUpdateAsync(mock_data_store, profile_store_name, key, transform_function)
	local profile_store = mock_data_store[profile_store_name]
	if profile_store == nil then
		profile_store = {}
		mock_data_store[profile_store_name] = profile_store
	end
	local transform = transform_function(profile_store[key])
	if transform == nil then
		return nil
	else
		profile_store[key] = DeepCopyTable(transform)
		return DeepCopyTable(profile_store[key])
	end
end

local function IsThisSession(session_tag)
	return session_tag[1] == PlaceId and session_tag[2] == JobId
end

--[[
update_settings = {
	ExistingProfileHandle = function(latest_data),
	MissingProfileHandle = function(latest_data),
	EditProfile = function(lastest_data),
	
	WipeProfile = nil / true,
}
--]]
local function StandardProfileUpdateAsyncDataStore(profile_store, profile_key, update_settings, is_user_mock)
	local loaded_data
	local wipe_status = false
	local success, error_message = pcall(function()
		if update_settings.WipeProfile ~= true then
			local transform_function = function(latest_data)
				if latest_data == "PROFILE_WIPED" then
					latest_data = nil -- Profile was previously wiped - ProfileService will act like it was empty
				end

				local missing_profile = false
				local data_corrupted = false
				local global_updates_data = {0, {}}

				if latest_data == nil then
					missing_profile = true
				elseif type(latest_data) ~= "table" then
					missing_profile = true
					data_corrupted = true
				end

				if type(latest_data) == "table" then
					-- Case #1: Profile was loaded
					if type(latest_data.Data) == "table" and
						type(latest_data.MetaData) == "table" and
						type(latest_data.GlobalUpdates) == "table" then

						latest_data.WasCorrupted = false -- Must be set to false if set previously
						global_updates_data = latest_data.GlobalUpdates
						if update_settings.ExistingProfileHandle ~= nil then
							update_settings.ExistingProfileHandle(latest_data)
						end
						-- Case #2: Profile was not loaded but GlobalUpdate data exists
					elseif latest_data.Data == nil and
						latest_data.MetaData == nil and
						type(latest_data.GlobalUpdates) == "table" then

						latest_data.WasCorrupted = false -- Must be set to false if set previously
						global_updates_data = latest_data.GlobalUpdates
						missing_profile = true
					else
						missing_profile = true
						data_corrupted = true
					end
				end

				-- Case #3: Profile was not created or corrupted and no GlobalUpdate data exists
				if missing_profile == true then
					latest_data = {
						-- Data = nil,
						-- MetaData = nil,
						GlobalUpdates = global_updates_data,
					}
					if update_settings.MissingProfileHandle ~= nil then
						update_settings.MissingProfileHandle(latest_data)
					end
				end

				-- Editing profile:
				if update_settings.EditProfile ~= nil then
					update_settings.EditProfile(latest_data)
				end

				-- Data corruption handling (Silently override with empty profile) (Also run Case #1)
				if data_corrupted == true then
					latest_data.WasCorrupted = true -- Temporary tag that will be removed on first save
				end

				return latest_data
			end
			if is_user_mock == true then -- Used when the profile is accessed through ProfileStore.Mock
				loaded_data = MockUpdateAsync(UserMockDataStore, profile_store._profile_store_lookup, profile_key, transform_function)
				Madwork.HeartbeatWait() -- Simulate API call yield
			elseif UseMockDataStore == true then -- Used when API access is disabled
				loaded_data = MockUpdateAsync(MockDataStore, profile_store._profile_store_lookup, profile_key, transform_function)
				Madwork.HeartbeatWait() -- Simulate API call yield
			else
				loaded_data = CustomWriteQueueAsync(
					function() -- Callback
						return profile_store._global_data_store:UpdateAsync(profile_key, transform_function)
					end,
					profile_store._profile_store_lookup, -- Store
					profile_key -- Key
				)
			end
		else
			if is_user_mock == true then -- Used when the profile is accessed through ProfileStore.Mock
				local mock_data_store = UserMockDataStore[profile_store._profile_store_lookup]
				if mock_data_store ~= nil then
					mock_data_store[profile_key] = nil
				end
				wipe_status = true
				Madwork.HeartbeatWait() -- Simulate API call yield
			elseif UseMockDataStore == true then -- Used when API access is disabled
				local mock_data_store = MockDataStore[profile_store._profile_store_lookup]
				if mock_data_store ~= nil then
					mock_data_store[profile_key] = nil
				end
				wipe_status = true
				Madwork.HeartbeatWait() -- Simulate API call yield
			else
				loaded_data = profile_store._global_data_store:UpdateAsync(profile_key, function()
					return "PROFILE_WIPED" -- It's impossible to set DataStore keys to nil after they have been set
				end)
				if loaded_data == "PROFILE_WIPED" then
					wipe_status = true
				end
			end
		end
	end)
	if update_settings.WipeProfile == true then
		return wipe_status
	elseif success == true and type(loaded_data) == "table" then
		-- Corruption handling:
		if loaded_data.WasCorrupted == true then
			RegisterCorruption(
				profile_store._profile_store_name,
				profile_store._profile_store_scope,
				profile_key
			)
		end
		-- Return loaded_data:
		return loaded_data
	else
		RegisterIssue(
			(error_message ~= nil) and error_message or "Undefined error",
			profile_store._profile_store_name,
			profile_store._profile_store_scope,
			profile_key
		)
		-- Return nothing:
		return nil
	end
end

local function RemoveProfileFromAutoSave(profile)
	local auto_save_index = table.find(AutoSaveList, profile)
	if auto_save_index ~= nil then
		table.remove(AutoSaveList, auto_save_index)
		if auto_save_index < AutoSaveIndex then
			AutoSaveIndex = AutoSaveIndex - 1 -- Table contents were moved left before AutoSaveIndex so move AutoSaveIndex left as well
		end
		if AutoSaveList[AutoSaveIndex] == nil then -- AutoSaveIndex was at the end of the AutoSaveList - reset to 1
			AutoSaveIndex = 1
		end
	end
end

local function AddProfileToAutoSave(profile) -- Notice: Makes sure this profile isn't auto-saved too soon
	-- Add at AutoSaveIndex and move AutoSaveIndex right:
	table.insert(AutoSaveList, AutoSaveIndex, profile)
	if #AutoSaveList > 1 then
		AutoSaveIndex = AutoSaveIndex + 1
	elseif #AutoSaveList == 1 then
		-- First profile created - make sure it doesn't get immediately auto saved:
		LastAutoSave = os.clock()
	end
end

local function ReleaseProfileInternally(profile)
	-- 1) Remove profile object from ProfileService references: --
	-- Clear reference in ProfileStore:
	local profile_store = profile._profile_store
	local loaded_profiles = profile._is_user_mock == true and profile_store._mock_loaded_profiles or profile_store._loaded_profiles
	loaded_profiles[profile._profile_key] = nil
	if next(profile_store._loaded_profiles) == nil and next(profile_store._mock_loaded_profiles) == nil then -- ProfileStore has turned inactive
		local index = table.find(ActiveProfileStores, profile_store)
		if index ~= nil then
			table.remove(ActiveProfileStores, index)
		end
	end
	-- Clear auto update reference:
	RemoveProfileFromAutoSave(profile)
	-- 2) Trigger release listeners: --
	local place_id
	local game_job_id
	local active_session = profile.MetaData.ActiveSession
	if active_session ~= nil then
		place_id = active_session[1]
		game_job_id = active_session[2]
	end
	profile._release_listeners:Fire(place_id, game_job_id)
end

local function CheckForNewGlobalUpdates(profile, old_global_updates_data, new_global_updates_data)
	local global_updates_object = profile.GlobalUpdates -- [GlobalUpdates]
	local pending_update_lock = global_updates_object._pending_update_lock -- {update_id, ...}
	local pending_update_clear = global_updates_object._pending_update_clear -- {update_id, ...}
	-- "old_" or "new_" global_updates_data = {update_index, {{update_id, version_id, update_locked, update_data}, ...}}
	for _, new_global_update in ipairs(new_global_updates_data[2]) do
		-- Find old global update with the same update_id:
		local old_global_update
		for _, global_update in ipairs(old_global_updates_data[2]) do
			if global_update[1] == new_global_update[1] then
				old_global_update = global_update
				break
			end
		end
		-- A global update is new when it didn't exist before or its version_id or update_locked state changed:
		local is_new = false
		if old_global_update == nil or new_global_update[2] > old_global_update[2] or new_global_update[3] ~= old_global_update[3] then
			is_new = true
		end
		if is_new == true then
			-- Active global updates:
			if new_global_update[3] == false then
				-- Check if update is not pending to be locked: (Preventing firing new active update listeners more than necessary)
				local is_pending_lock = false
				for _, update_id in ipairs(pending_update_lock) do
					if new_global_update[1] == update_id then
						is_pending_lock = true
						break
					end
				end
				if is_pending_lock == false then
					-- Trigger new active update listeners:
					global_updates_object._new_active_update_listeners:Fire(new_global_update[1], new_global_update[4])
				end
			end
			-- Locked global updates:
			if new_global_update[3] == true then
				-- Check if update is not pending to be cleared: (Preventing firing new locked update listeners after marking a locked update for clearing)
				local is_pending_clear = false
				for _, update_id in ipairs(pending_update_clear) do
					if new_global_update[1] == update_id then
						is_pending_clear = true
						break
					end
				end
				if is_pending_clear == false then
					-- Trigger new locked update listeners:

					global_updates_object._new_locked_update_listeners:FireUntil(
						function()
							-- Check if listener marked the update to be cleared:
							-- Normally there should be only one listener per profile for new locked global updates, but
							-- in case several listeners are connected we will not trigger more listeners after one listener
							-- marks the locked global update to be cleared.
							return table.find(pending_update_clear, new_global_update[1]) == nil
						end,
						new_global_update[1], new_global_update[4]
					)

				end
			end
		end
	end
end

local function SaveProfileAsync(profile, release_from_session)
	if type(profile.Data) ~= "table" then
		RegisterCorruption(
			profile._profile_store._profile_store_name,
			profile._profile_store._profile_store_scope,
			profile._profile_key
		)
		error("[ProfileService]: PROFILE DATA CORRUPTED DURING RUNTIME! Profile: " .. profile:Identify())
	end
	if release_from_session == true then
		ReleaseProfileInternally(profile)
	end
	ActiveProfileSaveJobs = ActiveProfileSaveJobs + 1
	local last_session_load_count = profile.MetaData.SessionLoadCount
	-- Compare "SessionLoadCount" when writing to profile to prevent a rare case of repeat last save when the profile is loaded on the same server again
	local repeat_save_flag = true -- Released Profile save calls have to repeat until they succeed
	while repeat_save_flag == true do
		if release_from_session ~= true then
			repeat_save_flag = false
		end
		local loaded_data = StandardProfileUpdateAsyncDataStore(
			profile._profile_store,
			profile._profile_key,
			{
				ExistingProfileHandle = nil,
				MissingProfileHandle = nil,
				EditProfile = function(latest_data)
					-- 1) Check if this session still owns the profile: --
					local active_session = latest_data.MetaData.ActiveSession
					local force_load_session = latest_data.MetaData.ForceLoadSession
					local session_load_count = latest_data.MetaData.SessionLoadCount
					local session_owns_profile = false
					local force_load_pending = false
					if type(active_session) == "table" then
						session_owns_profile = IsThisSession(active_session) and session_load_count == last_session_load_count
					end
					if type(force_load_session) == "table" then
						force_load_pending = not IsThisSession(force_load_session)
					end

					if session_owns_profile == true then -- We may only edit the profile if this session has ownership of the profile
						-- 2) Manage global updates: --
						local latest_global_updates_data = latest_data.GlobalUpdates -- {update_index, {{update_id, version_id, update_locked, update_data}, ...}}
						local latest_global_updates_list = latest_global_updates_data[2]

						local global_updates_object = profile.GlobalUpdates -- [GlobalUpdates]
						local pending_update_lock = global_updates_object._pending_update_lock -- {update_id, ...}
						local pending_update_clear = global_updates_object._pending_update_clear -- {update_id, ...}
						-- Active update locking:
						for i = 1, #latest_global_updates_list do
							for _, lock_id in ipairs(pending_update_lock) do
								if latest_global_updates_list[i][1] == lock_id then
									latest_global_updates_list[i][3] = true
									break
								end
							end
						end
						-- Locked update clearing:
						for _, clear_id in ipairs(pending_update_clear) do
							for i = 1, #latest_global_updates_list do
								if latest_global_updates_list[i][1] == clear_id and latest_global_updates_list[i][3] == true then
									table.remove(latest_global_updates_list, i)
									break
								end
							end
						end
						-- 3) Save profile data: --
						latest_data.Data = profile.Data
						latest_data.MetaData.MetaTags = profile.MetaData.MetaTags -- MetaData.MetaTags is the only actively savable component of MetaData
						latest_data.MetaData.LastUpdate = os.time()
						if release_from_session == true or force_load_pending == true then
							latest_data.MetaData.ActiveSession = nil
						end
					end
				end,
			},
			profile._is_user_mock
		)
		if loaded_data ~= nil then
			repeat_save_flag = false
			-- 4) Set latest data in profile: --
			-- Setting global updates:
			local global_updates_object = profile.GlobalUpdates -- [GlobalUpdates]
			local old_global_updates_data = global_updates_object._updates_latest
			local new_global_updates_data = loaded_data.GlobalUpdates
			global_updates_object._updates_latest = new_global_updates_data
			-- Setting MetaData:
			local session_meta_data = profile.MetaData
			local latest_meta_data = loaded_data.MetaData
			for key in pairs(SETTINGS.MetaTagsUpdatedValues) do
				session_meta_data[key] = latest_meta_data[key]
			end
			session_meta_data.MetaTagsLatest = latest_meta_data.MetaTags
			-- 5) Check if session still owns the profile: --
			local active_session = loaded_data.MetaData.ActiveSession
			local session_load_count = loaded_data.MetaData.SessionLoadCount
			local session_owns_profile = false
			if type(active_session) == "table" then
				session_owns_profile = IsThisSession(active_session) and session_load_count == last_session_load_count
			end
			local is_active = profile:IsActive()
			if session_owns_profile == true then
				-- 6) Check for new global updates: --
				if is_active == true then -- Profile could've been released before the saving thread finished
					CheckForNewGlobalUpdates(profile, old_global_updates_data, new_global_updates_data)
				end
			else
				-- Session no longer owns the profile:
				-- 7) Release profile if it hasn't been released yet: --
				if is_active == true then
					ReleaseProfileInternally(profile)
				end
				-- Cleanup reference in custom write queue:
				CustomWriteQueueMarkForCleanup(profile._profile_store._profile_store_lookup, profile._profile_key)
				-- Hop ready listeners:
				if profile._hop_ready == false then
					profile._hop_ready = true
					profile._hop_ready_listeners:Fire()
				end
			end
			-- Signaling MetaTagsUpdated listeners after a possible external profile release was handled:
			profile.MetaTagsUpdated:Fire(profile.MetaData.MetaTagsLatest)
		elseif repeat_save_flag == true then
			Madwork.HeartbeatWait() -- Prevent infinite loop in case DataStore API does not yield
		end
	end
	ActiveProfileSaveJobs = ActiveProfileSaveJobs - 1
end

----- Public functions -----

-- GlobalUpdates object:

local GlobalUpdates = {
	--[[
		_updates_latest = {}, -- [table] {update_index, {{update_id, version_id, update_locked, update_data}, ...}}
		_pending_update_lock = {update_id, ...} / nil, -- [table / nil]
		_pending_update_clear = {update_id, ...} / nil, -- [table / nil]
		
		_new_active_update_listeners = [ScriptSignal] / nil, -- [table / nil]
		_new_locked_update_listeners = [ScriptSignal] / nil, -- [table / nil]
		
		_profile = Profile / nil, -- [Profile / nil]
		
		_update_handler_mode = true / nil, -- [bool / nil]
	--]]
}
GlobalUpdates.__index = GlobalUpdates

-- ALWAYS PUBLIC:
function GlobalUpdates:GetActiveUpdates() --> [table] {{update_id, update_data}, ...}
	local query_list = {}
	for _, global_update in ipairs(self._updates_latest[2]) do
		if global_update[3] == false then
			local is_pending_lock = false
			if self._pending_update_lock ~= nil then
				for _, update_id in ipairs(self._pending_update_lock) do
					if global_update[1] == update_id then
						is_pending_lock = true -- Exclude global updates pending to be locked
						break
					end
				end
			end
			if is_pending_lock == false then
				table.insert(query_list, {global_update[1], global_update[4]})
			end
		end
	end
	return query_list
end

function GlobalUpdates:GetLockedUpdates() --> [table] {{update_id, update_data}, ...}
	local query_list = {}
	for _, global_update in ipairs(self._updates_latest[2]) do
		if global_update[3] == true then
			local is_pending_clear = false
			if self._pending_update_clear ~= nil then
				for _, update_id in ipairs(self._pending_update_clear) do
					if global_update[1] == update_id then
						is_pending_clear = true -- Exclude global updates pending to be cleared
						break
					end
				end
			end
			if is_pending_clear == false then
				table.insert(query_list, {global_update[1], global_update[4]})
			end
		end
	end
	return query_list
end

-- ONLY WHEN FROM "Profile.GlobalUpdates":
function GlobalUpdates:ListenToNewActiveUpdate(listener) --> [ScriptConnection] listener(update_id, update_data)
	if type(listener) ~= "function" then
		error("[ProfileService]: Only a function can be set as listener in GlobalUpdates:ListenToNewActiveUpdate()")
	end
	local profile = self._profile
	if self._update_handler_mode == true then
		error("[ProfileService]: Can't listen to new global updates in ProfileStore:GlobalUpdateProfileAsync()")
	elseif self._new_active_update_listeners == nil then
		error("[ProfileService]: Can't listen to new global updates in view mode")
	elseif profile:IsActive() == false then -- Check if profile is expired
		return { -- Do not connect listener if the profile is expired
			Disconnect = function() end,
		}
	end
	-- Connect listener:
	return self._new_active_update_listeners:Connect(listener)
end

function GlobalUpdates:ListenToNewLockedUpdate(listener) --> [ScriptConnection] listener(update_id, update_data)
	if type(listener) ~= "function" then
		error("[ProfileService]: Only a function can be set as listener in GlobalUpdates:ListenToNewLockedUpdate()")
	end
	local profile = self._profile
	if self._update_handler_mode == true then
		error("[ProfileService]: Can't listen to new global updates in ProfileStore:GlobalUpdateProfileAsync()")
	elseif self._new_locked_update_listeners == nil then
		error("[ProfileService]: Can't listen to new global updates in view mode")
	elseif profile:IsActive() == false then -- Check if profile is expired
		return { -- Do not connect listener if the profile is expired
			Disconnect = function() end,
		}
	end
	-- Connect listener:
	return self._new_locked_update_listeners:Connect(listener)
end

function GlobalUpdates:LockActiveUpdate(update_id)
	if type(update_id) ~= "number" then
		error("[ProfileService]: Invalid update_id")
	end
	local profile = self._profile
	if self._update_handler_mode == true then
		error("[ProfileService]: Can't lock active global updates in ProfileStore:GlobalUpdateProfileAsync()")
	elseif self._pending_update_lock == nil then
		error("[ProfileService]: Can't lock active global updates in view mode")
	elseif profile:IsActive() == false then -- Check if profile is expired
		error("[ProfileService]: PROFILE EXPIRED - Can't lock active global updates")
	end
	-- Check if global update exists with given update_id
	local global_update_exists = nil
	for _, global_update in ipairs(self._updates_latest[2]) do
		if global_update[1] == update_id then
			global_update_exists = global_update
			break
		end
	end
	if global_update_exists ~= nil then
		local is_pending_lock = false
		for _, lock_update_id in ipairs(self._pending_update_lock) do
			if update_id == lock_update_id then
				is_pending_lock = true -- Exclude global updates pending to be locked
				break
			end
		end
		if is_pending_lock == false and global_update_exists[3] == false then -- Avoid id duplicates in _pending_update_lock
			table.insert(self._pending_update_lock, update_id)
		end
	else
		error("[ProfileService]: Passed non-existant update_id")
	end
end

function GlobalUpdates:ClearLockedUpdate(update_id)
	if type(update_id) ~= "number" then
		error("[ProfileService]: Invalid update_id")
	end
	local profile = self._profile
	if self._update_handler_mode == true then
		error("[ProfileService]: Can't clear locked global updates in ProfileStore:GlobalUpdateProfileAsync()")
	elseif self._pending_update_clear == nil then
		error("[ProfileService]: Can't clear locked global updates in view mode")
	elseif profile:IsActive() == false then -- Check if profile is expired
		error("[ProfileService]: PROFILE EXPIRED - Can't clear locked global updates")
	end
	-- Check if global update exists with given update_id
	local global_update_exists = nil
	for _, global_update in ipairs(self._updates_latest[2]) do
		if global_update[1] == update_id then
			global_update_exists = global_update
			break
		end
	end
	if global_update_exists ~= nil then
		local is_pending_clear = false
		for _, clear_update_id in ipairs(self._pending_update_clear) do
			if update_id == clear_update_id then
				is_pending_clear = true -- Exclude global updates pending to be cleared
				break
			end
		end
		if is_pending_clear == false and global_update_exists[3] == true then -- Avoid id duplicates in _pending_update_clear
			table.insert(self._pending_update_clear, update_id)
		end
	else
		error("[ProfileService]: Passed non-existant update_id")
	end
end

-- EXPOSED TO "update_handler" DURING ProfileStore:GlobalUpdateProfileAsync() CALL
function GlobalUpdates:AddActiveUpdate(update_data)
	if type(update_data) ~= "table" then
		error("[ProfileService]: Invalid update_data")
	end
	if self._new_active_update_listeners ~= nil then
		error("[ProfileService]: Can't add active global updates in loaded Profile; Use ProfileStore:GlobalUpdateProfileAsync()")
	elseif self._update_handler_mode ~= true then
		error("[ProfileService]: Can't add active global updates in view mode; Use ProfileStore:GlobalUpdateProfileAsync()")
	end
	-- self._updates_latest = {}, -- [table] {update_index, {{update_id, version_id, update_locked, update_data}, ...}}
	local updates_latest = self._updates_latest
	local update_index = updates_latest[1] + 1 -- Incrementing global update index
	updates_latest[1] = update_index
	-- Add new active global update:
	table.insert(updates_latest[2], {update_index, 1, false, update_data})
end

function GlobalUpdates:ChangeActiveUpdate(update_id, update_data)
	if type(update_id) ~= "number" then
		error("[ProfileService]: Invalid update_id")
	end
	if type(update_data) ~= "table" then
		error("[ProfileService]: Invalid update_data")
	end
	if self._new_active_update_listeners ~= nil then
		error("[ProfileService]: Can't change active global updates in loaded Profile; Use ProfileStore:GlobalUpdateProfileAsync()")
	elseif self._update_handler_mode ~= true then
		error("[ProfileService]: Can't change active global updates in view mode; Use ProfileStore:GlobalUpdateProfileAsync()")
	end
	-- self._updates_latest = {}, -- [table] {update_index, {{update_id, version_id, update_locked, update_data}, ...}}
	local updates_latest = self._updates_latest
	local get_global_update = nil
	for _, global_update in ipairs(updates_latest[2]) do
		if update_id == global_update[1] then
			get_global_update = global_update
			break
		end
	end
	if get_global_update ~= nil then
		if get_global_update[3] == true then
			error("[ProfileService]: Can't change locked global update")
		end
		get_global_update[2] = get_global_update[2] + 1 -- Increment version id
		get_global_update[4] = update_data -- Set new global update data
	else
		error("[ProfileService]: Passed non-existant update_id")
	end
end

function GlobalUpdates:ClearActiveUpdate(update_id)
	if type(update_id) ~= "number" then
		error("[ProfileService]: Invalid update_id argument")
	end
	if self._new_active_update_listeners ~= nil then
		error("[ProfileService]: Can't clear active global updates in loaded Profile; Use ProfileStore:GlobalUpdateProfileAsync()")
	elseif self._update_handler_mode ~= true then
		error("[ProfileService]: Can't clear active global updates in view mode; Use ProfileStore:GlobalUpdateProfileAsync()")
	end
	-- self._updates_latest = {}, -- [table] {update_index, {{update_id, version_id, update_locked, update_data}, ...}}
	local updates_latest = self._updates_latest
	local get_global_update_index = nil
	local get_global_update = nil
	for index, global_update in ipairs(updates_latest[2]) do
		if update_id == global_update[1] then
			get_global_update_index = index
			get_global_update = global_update
			break
		end
	end
	if get_global_update ~= nil then
		if get_global_update[3] == true then
			error("[ProfileService]: Can't clear locked global update")
		end
		table.remove(updates_latest[2], get_global_update_index) -- Remove active global update
	else
		error("[ProfileService]: Passed non-existant update_id")
	end
end

-- Profile object:

local Profile = {
	--[[
		Data = {}, -- [table] -- Loaded once after ProfileStore:LoadProfileAsync() finishes
		MetaData = {}, -- [table] -- Updated with every auto-save
		GlobalUpdates = GlobalUpdates, -- [GlobalUpdates]
		
		_profile_store = ProfileStore, -- [ProfileStore]
		_profile_key = "", -- [string]
		
		_release_listeners = [ScriptSignal] / nil, -- [table / nil]
		_hop_ready_listeners = [ScriptSignal] / nil, -- [table / nil]
		_hop_ready = false,
		
		_view_mode = true / nil, -- [bool / nil]
		
		_load_timestamp = os.clock(),
		
		_is_user_mock = false, -- ProfileStore.Mock
	--]]
}
Profile.__index = Profile

function Profile:IsActive() --> [bool]
	local loaded_profiles = self._is_user_mock == true and self._profile_store._mock_loaded_profiles or self._profile_store._loaded_profiles
	return loaded_profiles[self._profile_key] == self
end

function Profile:GetMetaTag(tag_name) --> value
	local meta_data = self.MetaData
	if meta_data == nil then
		return nil
		-- error("[ProfileService]: This Profile hasn't been loaded before - MetaData not available")
	end
	return self.MetaData.MetaTags[tag_name]
end

function Profile:SetMetaTag(tag_name, value)
	if type(tag_name) ~= "string" then
		error("[ProfileService]: tag_name must be a string")
	elseif string.len(tag_name) == 0 then
		error("[ProfileService]: Invalid tag_name")
	end
	if self._view_mode == true then
		error("[ProfileService]: Can't set meta tag in view mode")
	end
	if self:IsActive() == false then
		error("[ProfileService]: PROFILE EXPIRED - Meta tags can't be set")
	end
	self.MetaData.MetaTags[tag_name] = value
end

function Profile:Reconcile()
	ReconcileTable(self.Data, self._profile_store._profile_template)
end

function Profile:ListenToRelease(listener) --> [ScriptConnection] (place_id / nil, game_job_id / nil)
	if type(listener) ~= "function" then
		error("[ProfileService]: Only a function can be set as listener in Profile:ListenToRelease()")
	end
	if self._view_mode == true then
		error("[ProfileService]: Can't listen to Profile release in view mode")
	end
	if self:IsActive() == false then
		-- Call release listener immediately if profile is expired
		local place_id
		local game_job_id
		local active_session = self.MetaData.ActiveSession
		if active_session ~= nil then
			place_id = active_session[1]
			game_job_id = active_session[2]
		end
		listener(place_id, game_job_id)
		return {
			Disconnect = function() end,
		}
	else
		return self._release_listeners:Connect(listener)
	end
end

function Profile:Save()
	if self._view_mode == true then
		error("[ProfileService]: Can't save Profile in view mode")
	end
	if self:IsActive() == false then
		error("[ProfileService]: PROFILE EXPIRED - Can't save Profile")
	end
	-- Reject save request if a save is already pending in the queue - this will prevent the user from
	--	unecessary API request spam which we could not meaningfully execute anyways!
	if IsCustomWriteQueueEmptyFor(self._profile_store._profile_store_lookup, self._profile_key) == true then
		-- We don't want auto save to trigger too soon after manual saving - this will reset the auto save timer:
		RemoveProfileFromAutoSave(self)
		AddProfileToAutoSave(self)
		-- Call save function in a new thread:
		coroutine.wrap(SaveProfileAsync)(self)
	end
end

function Profile:Release()
	if self._view_mode == true then
		error("[ProfileService]: Can't release Profile in view mode")
	end
	if self:IsActive() == true then
		coroutine.wrap(SaveProfileAsync)(self, true) -- Call save function in a new thread with release_from_session = true
	end
end

function Profile:ListenToHopReady(listener) --> [ScriptConnection] ()
	if type(listener) ~= "function" then
		error("[ProfileService]: Only a function can be set as listener in Profile:ListenToHopReady()")
	end
	if self._view_mode == true then
		error("[ProfileService]: Can't listen to HopReady in view mode")
	end
	if self._hop_ready == true then
		coroutine.wrap(listener)()
		return {
			Disconnect = function() end,
		}
	else
		return self._hop_ready_listeners:Connect(listener)
	end
end

function Profile:Identify() --> [string]
	return IdentifyProfile(
		self._profile_store._profile_store_name,
		self._profile_store._profile_store_scope,
		self._profile_key
	)
end

-- ProfileStore object:

local ProfileStore = {
	--[[
		Mock = {},
	
		_profile_store_name = "", -- [string] -- DataStore name
		_profile_store_scope = nil, -- [string] or [nil] -- DataStore scope
		_profile_store_lookup = "", -- [string] -- _profile_store_name .. "\0" .. (_profile_store_scope or "")
		
		_profile_template = {}, -- [table]
		_global_data_store = global_data_store, -- [GlobalDataStore] -- Object returned by DataStoreService:GetDataStore(_profile_store_name)
		
		_loaded_profiles = {[profile_key] = Profile, ...},
		_profile_load_jobs = {[profile_key] = {load_id, loaded_data}, ...},
		
		_mock_loaded_profiles = {[profile_key] = Profile, ...},
		_mock_profile_load_jobs = {[profile_key] = {load_id, loaded_data}, ...},
	--]]
}
ProfileStore.__index = ProfileStore

function ProfileStore:LoadProfileAsync(profile_key, not_released_handler, _use_mock) --> [Profile / nil] not_released_handler(place_id, game_job_id)
	if self._profile_template == nil then
		error("[ProfileService]: Profile template not set - ProfileStore:LoadProfileAsync() locked for this ProfileStore")
	end
	if type(profile_key) ~= "string" then
		error("[ProfileService]: profile_key must be a string")
	elseif string.len(profile_key) == 0 then
		error("[ProfileService]: Invalid profile_key")
	end
	if type(not_released_handler) ~= "function" and not_released_handler ~= "ForceLoad" and not_released_handler ~= "Steal" then
		error("[ProfileService]: Invalid not_released_handler")
	end

	if ProfileService.ServiceLocked == true then
		return nil
	end

	WaitForPendingProfileStore(self)

	local is_user_mock = _use_mock == UseMockTag

	-- Check if profile with profile_key isn't already loaded in this session:
	for _, profile_store in ipairs(ActiveProfileStores) do
		if profile_store._profile_store_lookup == self._profile_store_lookup then
			local loaded_profiles = is_user_mock == true and profile_store._mock_loaded_profiles or profile_store._loaded_profiles
			if loaded_profiles[profile_key] ~= nil then
				error("[ProfileService]: Profile " .. IdentifyProfile(self._profile_store_name, self._profile_store_scope, profile_key) .. " is already loaded in this session")
				-- Are you using Profile:Release() properly?
			end
		end
	end

	ActiveProfileLoadJobs = ActiveProfileLoadJobs + 1
	local force_load = not_released_handler == "ForceLoad"
	local force_load_steps = 0
	local request_force_load = force_load -- First step of ForceLoad
	local steal_session = false -- Second step of ForceLoad
	local aggressive_steal = not_released_handler == "Steal" -- Developer invoked steal
	while ProfileService.ServiceLocked == false do
		-- Load profile:
		-- SPECIAL CASE - If LoadProfileAsync is called for the same key before another LoadProfileAsync finishes,
		-- yoink the DataStore return for the new call. The older call will return nil. This would prevent very rare
		-- game breaking errors where a player rejoins the server super fast.
		local profile_load_jobs = is_user_mock == true and self._mock_profile_load_jobs or self._profile_load_jobs
		local loaded_data
		local load_id = LoadIndex + 1
		LoadIndex = load_id
		local profile_load_job = profile_load_jobs[profile_key] -- {load_id, loaded_data}
		if profile_load_job ~= nil then
			profile_load_job[1] = load_id -- Yoink load job
			while profile_load_job[2] == nil do -- Wait for job to finish
				Madwork.HeartbeatWait()
			end
			if profile_load_job[1] == load_id then -- Load job hasn't been double-yoinked
				loaded_data = profile_load_job[2]
				profile_load_jobs[profile_key] = nil
			else
				ActiveProfileLoadJobs = ActiveProfileLoadJobs - 1
				return nil
			end
		else
			profile_load_job = {load_id, nil}
			profile_load_jobs[profile_key] = profile_load_job
			profile_load_job[2] = StandardProfileUpdateAsyncDataStore(
				self,
				profile_key,
				{
					ExistingProfileHandle = function(latest_data)
						if ProfileService.ServiceLocked == false then
							local active_session = latest_data.MetaData.ActiveSession
							local force_load_session = latest_data.MetaData.ForceLoadSession
							-- IsThisSession(active_session)
							if active_session == nil then
								latest_data.MetaData.ActiveSession = {PlaceId, JobId}
								latest_data.MetaData.ForceLoadSession = nil
							elseif type(active_session) == "table" then
								if IsThisSession(active_session) == false then
									local last_update = latest_data.MetaData.LastUpdate
									if last_update ~= nil then
										if os.time() - last_update > SETTINGS.AssumeDeadSessionLock then
											latest_data.MetaData.ActiveSession = {PlaceId, JobId}
											latest_data.MetaData.ForceLoadSession = nil
											return
										end
									end
									if steal_session == true or aggressive_steal == true then
										local force_load_uninterrupted = false
										if force_load_session ~= nil then
											force_load_uninterrupted = IsThisSession(force_load_session)
										end
										if force_load_uninterrupted == true or aggressive_steal == true then
											latest_data.MetaData.ActiveSession = {PlaceId, JobId}
											latest_data.MetaData.ForceLoadSession = nil
										end
									elseif request_force_load == true then
										latest_data.MetaData.ForceLoadSession = {PlaceId, JobId}
									end
								else
									latest_data.MetaData.ForceLoadSession = nil
								end
							end
						end
					end,
					MissingProfileHandle = function(latest_data)
						latest_data.Data = DeepCopyTable(self._profile_template)
						latest_data.MetaData = {
							ProfileCreateTime = os.time(),
							SessionLoadCount = 0,
							ActiveSession = {PlaceId, JobId},
							ForceLoadSession = nil,
							MetaTags = {},

						}
					end,
					EditProfile = function(latest_data)
						if ProfileService.ServiceLocked == false then
							local active_session = latest_data.MetaData.ActiveSession
							if active_session ~= nil and IsThisSession(active_session) == true then
								latest_data.MetaData.SessionLoadCount = latest_data.MetaData.SessionLoadCount + 1
								latest_data.MetaData.LastUpdate = os.time()
							end
						end
					end,
				},
				is_user_mock
			)
			if profile_load_job[1] == load_id then -- Load job hasn't been yoinked
				loaded_data = profile_load_job[2]
				profile_load_jobs[profile_key] = nil
			else
				ActiveProfileLoadJobs = ActiveProfileLoadJobs - 1
				return nil -- Load job yoinked
			end
		end
		-- Handle load_data:
		if loaded_data ~= nil then
			local active_session = loaded_data.MetaData.ActiveSession
			if type(active_session) == "table" then
				if IsThisSession(active_session) == true then
					-- Special component in MetaTags:
					loaded_data.MetaData.MetaTagsLatest = DeepCopyTable(loaded_data.MetaData.MetaTags)
					-- Case #1: Profile is now taken by this session:
					-- Create Profile object:
					local global_updates_object = {
						_updates_latest = loaded_data.GlobalUpdates,
						_pending_update_lock = {},
						_pending_update_clear = {},

						_new_active_update_listeners = Madwork.NewScriptSignal(),
						_new_locked_update_listeners = Madwork.NewScriptSignal(),

						_profile = nil,
					}
					setmetatable(global_updates_object, GlobalUpdates)
					local profile = {
						Data = loaded_data.Data,
						MetaData = loaded_data.MetaData,
						MetaTagsUpdated = Madwork.NewScriptSignal(),
						GlobalUpdates = global_updates_object,

						_profile_store = self,
						_profile_key = profile_key,

						_release_listeners = Madwork.NewScriptSignal(),
						_hop_ready_listeners = Madwork.NewScriptSignal(),
						_hop_ready = false,

						_load_timestamp = os.clock(),

						_is_user_mock = is_user_mock,
					}
					setmetatable(profile, Profile)
					global_updates_object._profile = profile
					-- Referencing Profile object in ProfileStore:
					if next(self._loaded_profiles) == nil and next(self._mock_loaded_profiles) == nil then -- ProfileStore object was inactive
						table.insert(ActiveProfileStores, self)
					end
					if is_user_mock == true then
						self._mock_loaded_profiles[profile_key] = profile
					else
						self._loaded_profiles[profile_key] = profile
					end
					-- Adding profile to AutoSaveList;
					AddProfileToAutoSave(profile)
					-- Special case - finished loading profile, but session is shutting down:
					if ProfileService.ServiceLocked == true then
						SaveProfileAsync(profile, true) -- Release profile and yield until the DataStore call is finished
						profile = nil -- nil will be returned by this call
					end
					-- Return Profile object:
					ActiveProfileLoadJobs = ActiveProfileLoadJobs - 1
					return profile
				else
					-- Case #2: Profile is taken by some other session:
					if force_load == true then
						local force_load_session = loaded_data.MetaData.ForceLoadSession
						local force_load_uninterrupted = false
						if force_load_session ~= nil then
							force_load_uninterrupted = IsThisSession(force_load_session)
						end
						if force_load_uninterrupted == true then
							if request_force_load == false then
								force_load_steps = force_load_steps + 1
								if force_load_steps == SETTINGS.ForceLoadMaxSteps then
									steal_session = true
								end
							end
							Madwork.HeartbeatWait() -- Overload prevention
						else
							-- Another session tried to force load this profile:
							ActiveProfileLoadJobs = ActiveProfileLoadJobs - 1
							return nil
						end
						request_force_load = false -- Only request a force load once
					elseif aggressive_steal == true then
						Madwork.HeartbeatWait() -- Overload prevention
					else
						local handler_result = not_released_handler(active_session[1], active_session[2])
						if handler_result == "Repeat" then
							Madwork.HeartbeatWait() -- Overload prevention
						elseif handler_result == "Cancel" then
							ActiveProfileLoadJobs = ActiveProfileLoadJobs - 1
							return nil
						elseif handler_result == "ForceLoad" then
							force_load = true
							request_force_load = true
							Madwork.HeartbeatWait() -- Overload prevention
						elseif handler_result == "Steal" then
							aggressive_steal = true
							Madwork.HeartbeatWait() -- Overload prevention
						else
							error(
								"[ProfileService]: Invalid return from not_released_handler (\"" .. tostring(handler_result) .. "\")(" .. type(handler_result) .. ");" ..
									"\n" .. IdentifyProfile(self._profile_store_name, self._profile_store_scope, profile_key) ..
								" Traceback:\n" .. debug.traceback()
							)
						end
					end
				end
			else
				ActiveProfileLoadJobs = ActiveProfileLoadJobs - 1
				return nil -- In this scenario it is likely the ProfileService.ServiceLocked flag was raised
			end
		else
			Madwork.HeartbeatWait() -- Overload prevention
		end
	end
	ActiveProfileLoadJobs = ActiveProfileLoadJobs - 1
	return nil -- If loop breaks return nothing
end

function ProfileStore:GlobalUpdateProfileAsync(profile_key, update_handler, _use_mock) --> [GlobalUpdates / nil] (update_handler(GlobalUpdates))
	if type(profile_key) ~= "string" or string.len(profile_key) == 0 then
		error("[ProfileService]: Invalid profile_key")
	end
	if type(update_handler) ~= "function" then
		error("[ProfileService]: Invalid update_handler")
	end

	if ProfileService.ServiceLocked == true then
		return nil
	end

	WaitForPendingProfileStore(self)

	while ProfileService.ServiceLocked == false do
		-- Updating profile:
		local loaded_data = StandardProfileUpdateAsyncDataStore(
			self,
			profile_key,
			{
				ExistingProfileHandle = nil,
				MissingProfileHandle = nil,
				EditProfile = function(latest_data)
					-- Running update_handler:
					local global_updates_object = {
						_updates_latest = latest_data.GlobalUpdates,
						_update_handler_mode = true,
					}
					setmetatable(global_updates_object, GlobalUpdates)
					update_handler(global_updates_object)
				end,
			},
			_use_mock == UseMockTag
		)
		CustomWriteQueueMarkForCleanup(self._profile_store_lookup, profile_key)
		-- Handling loaded_data:
		if loaded_data ~= nil then
			-- Return GlobalUpdates object (Update successful):
			local global_updates_object = {
				_updates_latest = loaded_data.GlobalUpdates,
			}
			setmetatable(global_updates_object, GlobalUpdates)
			return global_updates_object
		else
			Madwork.HeartbeatWait() -- Overload prevention
		end
	end
	return nil -- Return nothing (Update unsuccessful)
end

function ProfileStore:ViewProfileAsync(profile_key, _use_mock) --> [Profile / nil]
	if type(profile_key) ~= "string" or string.len(profile_key) == 0 then
		error("[ProfileService]: Invalid profile_key")
	end

	if ProfileService.ServiceLocked == true then
		return nil
	end

	WaitForPendingProfileStore(self)

	while ProfileService.ServiceLocked == false do
		-- Load profile:
		local loaded_data = StandardProfileUpdateAsyncDataStore(
			self,
			profile_key,
			{
				ExistingProfileHandle = nil,
				MissingProfileHandle = nil,
				EditProfile = nil,
			},
			_use_mock == UseMockTag
		)
		CustomWriteQueueMarkForCleanup(self._profile_store_lookup, profile_key)
		-- Handle load_data:
		if loaded_data ~= nil then
			-- Create Profile object:
			local global_updates_object = {
				_updates_latest = loaded_data.GlobalUpdates,
				_profile = nil,
			}
			setmetatable(global_updates_object, GlobalUpdates)
			local profile = {
				Data = loaded_data.Data,
				MetaData = loaded_data.MetaData,
				MetaTagsUpdated = Madwork.NewScriptSignal(),
				GlobalUpdates = global_updates_object,

				_profile_store = self,
				_profile_key = profile_key,

				_view_mode = true,

				_load_timestamp = os.clock(),
			}
			setmetatable(profile, Profile)
			global_updates_object._profile = profile
			-- Returning Profile object:
			return profile
		else
			Madwork.HeartbeatWait() -- Overload prevention
		end
	end
	return nil -- If loop breaks return nothing
end

function ProfileStore:WipeProfileAsync(profile_key, _use_mock) --> is_wipe_successful [bool]
	if type(profile_key) ~= "string" or string.len(profile_key) == 0 then
		error("[ProfileService]: Invalid profile_key")
	end

	if ProfileService.ServiceLocked == true then
		return false
	end

	WaitForPendingProfileStore(self)

	local wipe_status = StandardProfileUpdateAsyncDataStore(
		self,
		profile_key,
		{
			WipeProfile = true
		},
		_use_mock == UseMockTag
	)

	CustomWriteQueueMarkForCleanup(self._profile_store_lookup, profile_key)

	return wipe_status
end

-- New ProfileStore:

function ProfileService.GetProfileStore(profile_store_index, profile_template) --> [ProfileStore]
	
	local profile_store_name
	local profile_store_scope = nil
	
	-- Parsing profile_store_index:
	if type(profile_store_index) == "string" then
		-- profile_store_index as string:
		profile_store_name = profile_store_index
	elseif type(profile_store_index) == "table" then
		-- profile_store_index as table:
		profile_store_name = profile_store_index.Name
		profile_store_scope = profile_store_index.Scope
	else
		error("[ProfileService]: Invalid or missing profile_store_index")
	end
	
	-- Type checking:
	if profile_store_name == nil or type(profile_store_name) ~= "string" then
		error("[ProfileService]: Missing or invalid \"Name\" parameter")
	elseif string.len(profile_store_name) == 0 then
		error("[ProfileService]: ProfileStore name cannot be an empty string")
	end
	
	if profile_store_scope ~= nil and (type(profile_store_scope) ~= "string" or string.len(profile_store_scope) == 0) then
		error("[ProfileService]: Invalid \"Scope\" parameter")
	end

	if type(profile_template) ~= "table" then
		error("[ProfileService]: Invalid profile_template")
	end

	local profile_store
	profile_store = {
		Mock = {
			LoadProfileAsync = function(_, profile_key, not_released_handler)
				return profile_store:LoadProfileAsync(profile_key, not_released_handler, UseMockTag)
			end,
			GlobalUpdateProfileAsync = function(_, profile_key, update_handler)
				return profile_store:GlobalUpdateProfileAsync(profile_key, update_handler, UseMockTag)
			end,
			ViewProfileAsync = function(_, profile_key)
				return profile_store:ViewProfileAsync(profile_key, UseMockTag)
			end,
			WipeProfileAsync = function(_, profile_key)
				return profile_store:WipeProfileAsync(profile_key, UseMockTag)
			end
		},

		_profile_store_name = profile_store_name,
		_profile_store_scope = profile_store_scope,
		_profile_store_lookup = profile_store_name .. "\0" .. (profile_store_scope or ""),
		
		_profile_template = profile_template,
		_global_data_store = nil,
		_loaded_profiles = {},
		_profile_load_jobs = {},
		_mock_loaded_profiles = {},
		_mock_profile_load_jobs = {},
		_is_pending = false,
	}
	setmetatable(profile_store, ProfileStore)
	
	if IsLiveCheckActive == true then
		profile_store._is_pending = true
		coroutine.wrap(function()
			WaitForLiveAccessCheck()
			if UseMockDataStore == false then
				profile_store._global_data_store = DataStoreService:GetDataStore(profile_store_name, profile_store_scope)
			end
			profile_store._is_pending = false
		end)()
	else
		if UseMockDataStore == false then
			profile_store._global_data_store = DataStoreService:GetDataStore(profile_store_name, profile_store_scope)
		end
	end
	
	return profile_store
end

----- Initialize -----

if IsStudio == true then
	IsLiveCheckActive = true
	coroutine.wrap(function()
		local status, message = pcall(function()
			-- This will error if current instance has no Studio API access:
			DataStoreService:GetDataStore("____PS"):SetAsync("____PS", os.time())
		end)
		local no_internet_access = status == false and string.find(message, "ConnectFail", 1, true) ~= nil
		if no_internet_access == true then
			warn("[ProfileService]: No internet access - check your network connection")
		end
		if status == false and
			(string.find(message, "403", 1, true) ~= nil or -- Cannot write to DataStore from studio if API access is not enabled
				string.find(message, "must publish", 1, true) ~= nil or -- Game must be published to access live keys
				no_internet_access == true) then -- No internet access

			UseMockDataStore = true
			ProfileService._use_mock_data_store = true
			print("[ProfileService]: Roblox API services unavailable - data will not be saved")
		else
			print("[ProfileService]: Roblox API services available - data will be saved")
		end
		IsLiveCheckActive = false
	end)()
end

----- Connections -----

-- Auto saving and issue queue managing:
RunService.Heartbeat:Connect(function()
	-- 1) Auto saving: --
	local auto_save_list_length = #AutoSaveList
	if auto_save_list_length > 0 then
		local auto_save_index_speed = SETTINGS.AutoSaveProfiles / auto_save_list_length
		local os_clock = os.clock()
		while os_clock - LastAutoSave > auto_save_index_speed do
			LastAutoSave = LastAutoSave + auto_save_index_speed
			local profile = AutoSaveList[AutoSaveIndex]
			if os_clock - profile._load_timestamp < SETTINGS.AutoSaveProfiles then
				-- This profile is freshly loaded - auto-saving immediately after loading will cause a warning in the log:
				profile = nil
				for _ = 1, auto_save_list_length - 1 do
					-- Move auto save index to the right:
					AutoSaveIndex = AutoSaveIndex + 1
					if AutoSaveIndex > auto_save_list_length then
						AutoSaveIndex = 1
					end
					profile = AutoSaveList[AutoSaveIndex]
					if os_clock - profile._load_timestamp >= SETTINGS.AutoSaveProfiles then
						break
					else
						profile = nil
					end
				end
			end
			-- Move auto save index to the right:
			AutoSaveIndex = AutoSaveIndex + 1
			if AutoSaveIndex > auto_save_list_length then
				AutoSaveIndex = 1
			end
			-- Perform save call:
			if profile ~= nil then
				coroutine.wrap(SaveProfileAsync)(profile) -- Auto save profile in new thread
			end
		end
	end
	-- 2) Issue queue: --
	-- Critical state handling:
	if ProfileService.CriticalState == false then
		if #IssueQueue >= SETTINGS.IssueCountForCriticalState then
			ProfileService.CriticalState = true
			ProfileService.CriticalStateSignal:Fire(true)
			CriticalStateStart = os.clock()
			warn("[ProfileService]: Entered critical state")
		end
	else
		if #IssueQueue >= SETTINGS.IssueCountForCriticalState then
			CriticalStateStart = os.clock()
		elseif os.clock() - CriticalStateStart > SETTINGS.CriticalStateLast then
			ProfileService.CriticalState = false
			ProfileService.CriticalStateSignal:Fire(false)
			warn("[ProfileService]: Critical state ended")
		end
	end
	-- Issue queue:
	while true do
		local issue_time = IssueQueue[1]
		if issue_time == nil then
			break
		elseif os.clock() - issue_time > SETTINGS.IssueLast then
			table.remove(IssueQueue, 1)
		else
			break
		end
	end
end)

-- Release all loaded profiles when the server is shutting down:
coroutine.wrap(function()
	WaitForLiveAccessCheck()
	Madwork.ConnectToOnClose(
		function()
			ProfileService.ServiceLocked = true
			-- 1) Release all active profiles: --
			-- Clone AutoSaveList to a new table because AutoSaveList changes when profiles are released:
			local on_close_save_job_count = 0
			local active_profiles = {}
			for index, profile in ipairs(AutoSaveList) do
				active_profiles[index] = profile
			end
			-- Release the profiles; Releasing profiles can trigger listeners that release other profiles, so check active state:
			for _, profile in ipairs(active_profiles) do
				if profile:IsActive() == true then
					on_close_save_job_count = on_close_save_job_count + 1
					coroutine.wrap(function() -- Save profile on new thread
						SaveProfileAsync(profile, true)
						on_close_save_job_count = on_close_save_job_count - 1
					end)()
				end
			end
			-- 2) Yield until all active profile jobs are finished: --
			while on_close_save_job_count > 0 or ActiveProfileLoadJobs > 0 or ActiveProfileSaveJobs > 0 do
				Madwork.HeartbeatWait()
			end
			return -- We're done!
		end,
		UseMockDataStore == false -- Always run this OnClose task if using Roblox API services
	)
end)()

return ProfileService