ProfileService Lua
ProfileService Lua
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
Members:
ProfileService.ServiceLocked [bool]
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]:
Profile.GlobalUpdates [GlobalUpdates]
Methods [Profile]:
Methods [GlobalUpdates]:
-- ALWAYS PUBLIC:
GlobalUpdates:GetActiveUpdates() --> [table] {{update_id, update_data},
...}
GlobalUpdates:GetLockedUpdates() --> [table] {{update_id, update_data},
...}
--]]
local SETTINGS = {
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: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
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
local ProfileService = {
ServiceIssueCount = 0,
-- 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},
...
}
},
}
--]]
local LoadIndex = 0
local CustomWriteQueue = {
--[[
[store] = {
[key] = {
LastWrite = os.clock(),
Queue = {callback, ...},
CleanupJob = nil,
},
...
},
...
--]]
}
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
-- Cleanup job:
-- 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
--[[
update_settings = {
ExistingProfileHandle = function(latest_data),
MissingProfileHandle = function(latest_data),
EditProfile = function(lastest_data),
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
update_settings.MissingProfileHandle(latest_data)
end
end
-- Editing profile:
if update_settings.EditProfile ~= nil then
update_settings.EditProfile(latest_data)
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
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 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
-- 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]
-- 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
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
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]
_load_timestamp = os.clock(),
function Profile:Reconcile()
ReconcileTable(self.Data, self._profile_store._profile_template)
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
-- ProfileStore object:
local ProfileStore = {
--[[
Mock = {},
WaitForPendingProfileStore(self)
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.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.ForceLoadSession = nil
end
elseif request_force_load ==
true then
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
WaitForPendingProfileStore(self)
WaitForPendingProfileStore(self)
_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
WaitForPendingProfileStore(self)
CustomWriteQueueMarkForCleanup(self._profile_store_lookup, profile_key)
return wipe_status
end
-- New 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
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)
return profile_store
end
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
return ProfileService