@1vgot_terminated @JuniorMikezz
I decided to write up a Luau code you guys can copy and paste into the console. Change the settings at the bottom of the script before running and follow any instructions it has given you. Of course, feel free to edit as this code doesn’t currently include internal failures and automatic retries.
This should really only be used once or during rollbacks that requires specific timeframes, and any future rollbacks should be saved and uploaded via disk/cloud instead for fewer, safer, and battle-tested API calls.
--!strict
local RunService = game:GetService("RunService")
-- Only allow the script to run in the console
if RunService:IsEdit() == false or RunService:IsRunMode() == true then
return
end
local DataStoreService = game:GetService("DataStoreService")
local function waitForRequestTypeBudget(dataStoreRequestType: Enum.DataStoreRequestType, budget: number, interval: number, verbose: boolean?): ()
local hasWarned: boolean = false
while DataStoreService:GetRequestBudgetForRequestType(dataStoreRequestType) < budget do
if verbose == true and hasWarned == false then
warn(`Waiting for budget '{dataStoreRequestType.Name}' ({DataStoreService:GetRequestBudgetForRequestType(dataStoreRequestType)} / {budget})`)
hasWarned = true
end
task.wait(interval)
end
end
local function fetchDataStoreKeysAsync(name: string, prefix: string?, pageSize: number?, cursor: string?, excludeDeleted: boolean?, verbose: boolean?): {string}
if verbose == true then
print(`Fetching keys from '{name}'`)
end
waitForRequestTypeBudget(Enum.DataStoreRequestType.ListAsync, 1, 1)
local dataStore: DataStore = DataStoreService:GetDataStore(name)
local dataStoreKeyPages: DataStoreKeyPages = dataStore:ListKeysAsync(prefix, pageSize, cursor, excludeDeleted)
local dataStoreKeys: {string} = {}
while true do
for _: number, dataStoreKey: DataStoreKey in dataStoreKeyPages:GetCurrentPage() do
table.insert(dataStoreKeys, dataStoreKey.KeyName)
if verbose == true then
print(`Fetched key path '{name}/{dataStoreKey.KeyName}'`)
end
end
if dataStoreKeyPages.IsFinished == true then
break
end
dataStoreKeyPages:AdvanceToNextPageAsync()
end
if verbose == true then
print(`Successfully fetched keys from '{name}'`, dataStoreKeys)
end
return dataStoreKeys
end
local function fetchDataStoreVersionsAsync(name: string, key: string, sortDirection: Enum.SortDirection?, minDate: number?, maxDate: number?, pageSize: number?, verbose: boolean?): {DataStoreObjectVersionInfo}
if verbose == true then
print(`Fetching versions from path '{name}/{key}'`)
end
waitForRequestTypeBudget(Enum.DataStoreRequestType.ListAsync, 1, 1)
local dataStore: DataStore = DataStoreService:GetDataStore(name)
local dataStoreKeyVersionPages: DataStoreVersionPages = dataStore:ListVersionsAsync(key, sortDirection, minDate, maxDate, pageSize)
local dataStoreVersions: {DataStoreObjectVersionInfo} = {}
while true do
for _: number, dataStoreObjectVersionInfo: DataStoreObjectVersionInfo in dataStoreKeyVersionPages:GetCurrentPage() do
table.insert(dataStoreVersions, dataStoreObjectVersionInfo)
if verbose == true then
print(`Fetched version '{dataStoreObjectVersionInfo.Version}' from path '{name}/{key}'`)
end
end
if dataStoreKeyVersionPages.IsFinished == true then
break
end
dataStoreKeyVersionPages:AdvanceToNextPageAsync()
end
if verbose == true then
print(`Successfully fetched versions from path '{name}/{key}'`, dataStoreVersions)
end
return dataStoreVersions
end
local function fetchVersionAsync(name: string, key: string, version: string, verbose: boolean?): (any, DataStoreKeyInfo)
if verbose == true then
print(`Fetching version from path '{name}/{key}'`)
end
waitForRequestTypeBudget(Enum.DataStoreRequestType.GetVersionAsync, 1, 1)
local dataStore: DataStore = DataStoreService:GetDataStore(name)
local value: any, dataStoreKeyInfo: DataStoreKeyInfo = dataStore:GetVersionAsync(key, version)
if verbose == true then
print(`Successfully fetched version from path '{name}'/{key}`, value, dataStoreKeyInfo)
end
return value, dataStoreKeyInfo
end
local rollbackThread: thread? = nil
local function tryRollback(name: string, beforeMillis: number, prefix: string?, editFunction: (data: any, keyInfo: DataStoreKeyInfo) -> DataStoreSetOptions?): ()
if game:GetAttribute("OPERATION_ROLLBACK") == true then
warn(
[[A rollback has already been requested, is already running, or has previously crashed.
To forcefully cleanup the previous operation, run this in the console and try again:
game:SetAttribute("OPERATION_ROLLBACK", nil)
game:SetAttribute("OPERATION_CONTINUE", nil)
game:SetAttribute("OPERATION_CANCELLED_MESSAGE", nil)]]
)
return
end
rollbackThread = coroutine.running()
game:SetAttribute("OPERATION_ROLLBACK", true)
game:SetAttribute("OPERATION_CONTINUE", nil)
game:SetAttribute("OPERATION_CANCELLED_MESSAGE", nil)
local cancelConnection: RBXScriptConnection; cancelConnection = game:GetAttributeChangedSignal("OPERATION_ROLLBACK"):Connect(function(): ()
if cancelConnection.Connected == true then
if game:GetAttribute("OPERATION_ROLLBACK") ~= true and rollbackThread ~= nil then
cancelConnection:Disconnect()
pcall(task.cancel, rollbackThread)
warn(`Rollback has been cancelled: {game:GetAttribute("OPERATION_CANCELLED_MESSAGE") or "user cancelled"}`)
game:SetAttribute("OPERATION_ROLLBACK", nil)
game:SetAttribute("OPERATION_CONTINUE", nil)
game:SetAttribute("OPERATION_CANCELLED_MESSAGE", nil)
end
end
end)
warn(string.format(
[[Requested rollback. Please confirm the following arguments before proceeding:
DataStore Name: %s
Before Time: %s
Prefix: %s
To cancel at anytime, run this in the console:
game:SetAttribute("OPERATION_ROLLBACK", nil)
To start the process, run this in the console:
game:SetAttribute("OPERATION_CONTINUE", true)]],
name, DateTime.fromUnixTimestampMillis(beforeMillis):ToIsoDate(), prefix or "nil")
)
while game:GetAttribute("OPERATION_CONTINUE") ~= true do
warn('--------------------------------------------------')
warn(`Waiting for user input...`)
game:GetAttributeChangedSignal("OPERATION_CONTINUE"):Wait()
end
game:SetAttribute("OPERATION_CONTINUE", nil)
warn('--------------------------------------------------')
local dataStoreKeys: {string} = nil
do
local success: boolean, response: string? = pcall(function(): ()
dataStoreKeys = fetchDataStoreKeysAsync(name)
end)
if success == false then
game:SetAttribute("OPERATION_CANCELLED_MESSAGE", `failed to get DataStore keys ({response})`)
game:SetAttribute("OPERATION_ROLLBACK", nil)
return
end
end
if #dataStoreKeys == 0 then
game:SetAttribute("OPERATION_CANCELLED_MESSAGE", "no keys found")
game:SetAttribute("OPERATION_ROLLBACK", nil)
return
end
local latestSafeDataStoreVersions: {[string]: string} = {}
local latestSafeDataStoreData: {
[string]: {
Value: any,
DataStoreKeyInfo: DataStoreKeyInfo
}
} = {}
local rollbackCount: number = 0
for _: number, key: string in dataStoreKeys do
for _: number, dataStoreObjectVersionInfo: DataStoreObjectVersionInfo in fetchDataStoreVersionsAsync(name, key, Enum.SortDirection.Descending, nil, beforeMillis, 1) do
latestSafeDataStoreVersions[key] = dataStoreObjectVersionInfo.Version
end
end
if next(latestSafeDataStoreVersions) == nil then
game:SetAttribute("OPERATION_CANCELLED_MESSAGE", "no suitable versions found")
game:SetAttribute("OPERATION_ROLLBACK", nil)
return
end
for key: string, version: string in latestSafeDataStoreVersions do
local value: any, dataStoreKeyInfo: DataStoreKeyInfo = nil, nil
local success: boolean, response: string? = pcall(function(): ()
value, dataStoreKeyInfo = fetchVersionAsync(name, key, version)
end)
if success == true then
latestSafeDataStoreData[key] = {
Value = value,
DataStoreKeyInfo = dataStoreKeyInfo
}
rollbackCount += 1
else
warn(`ERROR: Failed to get version '{version}' for key '{key}' ({response})`)
end
end
if next(latestSafeDataStoreData) == nil then
game:SetAttribute("OPERATION_CANCELLED_MESSAGE", "no data found")
game:SetAttribute("OPERATION_ROLLBACK", nil)
return
end
for key: string, data: {Value: any, DataStoreKeyInfo: DataStoreKeyInfo} in latestSafeDataStoreData do
print('--------------------------------------------------')
print(`> Key: {key} | Last Updated: {DateTime.fromUnixTimestampMillis(data.DataStoreKeyInfo.UpdatedTime):ToIsoDate()} ({data.DataStoreKeyInfo.UpdatedTime}) | Data:`)
print(data)
print('--------------------------------------------------')
end
warn(string.format(
[[You are about to rollback %d key(s) listed above.
To cancel at anytime, run this in the console:
game:SetAttribute("OPERATION_ROLLBACK", nil)
To continue the process, run this in the console:
game:SetAttribute("OPERATION_CONTINUE", true)]],
rollbackCount
))
while game:GetAttribute("OPERATION_CONTINUE") ~= true do
warn('--------------------------------------------------')
warn(`Waiting for user input...`)
game:GetAttributeChangedSignal("OPERATION_CONTINUE"):Wait()
end
game:SetAttribute("OPERATION_CONTINUE", nil)
warn('--------------------------------------------------')
local dataStore: DataStore = DataStoreService:GetDataStore(name)
local successCount: number = 0
local failedCount: number = 0
for key: string, data: {Value: any, DataStoreKeyInfo: DataStoreKeyInfo} in latestSafeDataStoreData do
local dataStoreSetOptions: DataStoreSetOptions? = if editFunction ~= nil then editFunction(data.Value, data.DataStoreKeyInfo) else nil
waitForRequestTypeBudget(Enum.DataStoreRequestType.SetIncrementAsync, 1, 5)
local success: boolean, response: string? = pcall(function(): ()
dataStore:SetAsync(key, data.Value, data.DataStoreKeyInfo:GetUserIds(), dataStoreSetOptions)
end)
if success == true then
print(`🟢 Successfully rolled back data for key '{key}'`)
successCount += 1
else
warn(`🔴 Failed to rollback data for key '{key}' ({response})`)
failedCount += 1
end
end
warn('--------------------------------------------------')
warn(`Rollback complete.`)
warn(`Successful rollbacks: {successCount} / {rollbackCount}`)
warn(`Failed rollbacks: {failedCount} / {rollbackCount}`)
rollbackThread = nil
game:SetAttribute("OPERATION_ROLLBACK", nil)
game:SetAttribute("OPERATION_CONTINUE", nil)
game:SetAttribute("OPERATION_CANCELLED_MESSAGE", nil)
end
-- The DataStore name you want to rollback
local DATA_STORE_NAME: string = "v0.0.0-dev1"
-- The before date you wish to revert to, if any.
-- Year, Month, Day, Hour, Minute, Second, Millisecond
local BEFORE_DATE: DateTime = DateTime.fromUniversalTime(2024, 11, 10, 0, 0, 0, 0)
local BEFORE_DATE_MILLISECONDS: number = BEFORE_DATE.UnixTimestampMillis
-- The prefix, if any, you want to use to grab keys.
-- Leave as an empty string to get all keys.
-- If you want to test it out using your or a test account,
-- you can put your specific key here to make sure
-- everything works before proceeding with the mass rollback.
local PREFIX: string = ""
-- The function to run to edit any data. Use it to manually
-- edit things such as session locks or reapplying metadata
local EDIT_FUNCTION: (data: any, keyInfo: DataStoreKeyInfo) -> DataStoreSetOptions? = function(data: any, keyInfo: DataStoreKeyInfo): DataStoreSetOptions?
-- Unlock ProfileService session
if data.Data ~= nil and typeof(data.MetaData) == "table" then
--data.MetaData.ActiveSession = nil
print(`Unlocked session for '{keyInfo.Version}'`)
end
-- Reconstruct metadata. UserIds are automatically applied again.
local setOptions: DataStoreSetOptions = Instance.new("DataStoreSetOptions")
setOptions:SetMetadata(keyInfo:GetMetadata())
-- Pass the set options back for saving
return setOptions
end
tryRollback(DATA_STORE_NAME, BEFORE_DATE_MILLISECONDS, PREFIX, EDIT_FUNCTION)