diff --git a/saveinstance.lua b/saveinstance.lua index d0a8e88..45d9a9f 100644 --- a/saveinstance.lua +++ b/saveinstance.lua @@ -670,8 +670,8 @@ XML_Descriptors = { .. " 0 " end) end, - Content = function(raw) - return raw == "" and "" or "" .. XML_Descriptors.string(raw, true) .. "" + ContentId = function(raw) + return raw == "" and "" or "" .. XML_Descriptors.string(raw) .. "", "Content" end, CoordinateFrame = function(raw) return "" .. XML_Descriptors.CFrame(raw) .. "" @@ -690,12 +690,12 @@ XML_Descriptors = { local EmptyStyle = string_find(FontString, "Style = }") return "" - .. XML_Descriptors.Content(raw.Family) + .. XML_Descriptors.ContentId(raw.Family) .. "" .. (EmptyWeight and "" or XML_Descriptors.__ENUM(raw.Weight)) .. "" --TODO (OPTIONAL ELEMENT): Figure out how to determine (Content) rbxasset://fonts/GothamSSm-Medium.otf + .. "" --TODO (OPTIONAL ELEMENT): Figure out how to determine (ContentId) rbxasset://fonts/GothamSSm-Medium.otf end, NumberRange = function(raw) -- tostring(raw) also works -- The value is the text content, formatted as a space-separated list of floating point numbers. @@ -831,8 +831,8 @@ XML_Descriptors = { float = nil, -- Float32 int = nil, -- Int32 int64 = nil, -- Int64 (long) - string = function(raw, skipEmptyCheck) - return not skipEmptyCheck and (raw == "" and raw or raw == nil and "") + string = function(raw) + return (raw == nil or raw == "") and "" or string_find(raw, "]]>") and string.gsub(raw, ESCAPES_PATTERN, ESCAPES) or XML_Descriptors.__CDATA(string.gsub(raw, "\0", "")) end, @@ -840,7 +840,7 @@ XML_Descriptors = { for descriptorName, redirectName in next, { - ContentId = "Content", + Content = "ContentId", -- For sake of compatibility with older clients NumberSequence = "__SEQUENCE", Vector2int16 = "Vector2", Vector3int16 = "Vector3", @@ -1116,9 +1116,9 @@ do local API_Dump - local ok, err = pcall(function() - local CLIENT_VERSION = string.split(version(), ".")[2] + local CLIENT_VERSION = string.split(version(), ".")[2] + local ok, err = pcall(function() local ok, result = pcall(readfile, CLIENT_VERSION) if ok and result and result ~= "" then API_Dump = result @@ -1229,6 +1229,10 @@ do local ValueType = Member.ValueType local ValueType_Name = ValueType.Name + if 649 <= CLIENT_VERSION and ValueType_Name == "Content" then -- TODO: Remove after Roblox adds a descriptor for it + continue + end + local Special if MemberTags then @@ -1311,7 +1315,7 @@ local GLOBAL_ENV = getgenv and getgenv() or _G or shared --- @field SafeMode boolean -- Kicks you before Saving, which prevents you from being detected in any game. ___Default:___ false --- @field ShutdownWhenDone boolean -- Shuts the game down after saveinstance is finished. ___Default:___ false --- @field AntiIdle boolean -- Prevents the 20-minute-Idle Kick. ___Default:___ true ---- @field Anonymous boolean -- * **RISKY:** Cleans the file of any info related to your account like: Name, UserId. This is useful for some games that might store that info in GUIs or other Instances. Might potentially mess up parts of strings that contain characters that match your Name or parts of numbers that match your UserId. ___Default:___ false +--- Anonymous {boolean|table{UserId = string, Name = string}} -- * **RISKY:** Cleans the file of any info related to your account like: Name, UserId. This is useful for some games that might store that info in GUIs or other Instances. Might potentially mess up parts of strings that contain characters that match your Name or parts of numbers that match your UserId. Can also be a table with UserId & Name keys. ___Default:___ false --- @field ShowStatus boolean -- ___Default:___ true --- @field Callback boolean -- If set, the serialized data will be sent to the callback function instead of to file. ___Default:___ nil --- @field mode string -- Change this to invalid mode like "invalid" if you only want ExtraInstances. "optimized" mode is **NOT** supported with *@Object* option. ___Default:___ `"optimized"` @@ -1382,12 +1386,6 @@ local GLOBAL_ENV = getgenv and getgenv() or _G or shared ]=] local function synsaveinstance(CustomOptions, CustomOptions2) - -- if GLOBAL_ENV.__USSI then - -- warn("UniversalSynSaveInstance is already running") - -- return - -- end - -- GLOBAL_ENV.__USSI = true - do local setthreadidentity = global_container.setthreadidentity if setthreadidentity then @@ -1396,7 +1394,10 @@ local function synsaveinstance(CustomOptions, CustomOptions2) end local currentstr, currentsize, totalsize, chunks = "", 0, 0, table.create(1) - local savebuffer, savebuffer_size = { '' }, 2 + local savebuffer, savebuffer_size = + { + '', + }, 2 local StatusText @@ -1488,7 +1489,11 @@ local function synsaveinstance(CustomOptions, CustomOptions2) do -- * Load Settings local function construct_NilinstanceFix(Name, ClassName, Separate) return function(instance, instancePropertyOverrides) - local Exists = not Separate and OPTIONS.NilInstancesFixes[Name] or nil + local Exists + + if not Separate then + Exists = OPTIONS.NilInstancesFixes[Name] + end local Fix @@ -1500,13 +1505,14 @@ local function synsaveinstance(CustomOptions, CustomOptions2) end -- Fix.Name = Name - instancePropertyOverrides[Fix] = { __Children = { instance }, Properties = { Name = Name } } + instancePropertyOverrides[Fix] = + { __SaveSpecific = true, __Children = { instance }, Properties = { Name = Name } } else Fix = Exists end table.insert(instancePropertyOverrides[Fix].__Children, instance) - -- InstancePropertyOverrides[instance].Parent = AnimationController + -- InstancesOverrides[instance].Parent = AnimationController if DoesntExist then return Fix end @@ -1601,41 +1607,7 @@ local function synsaveinstance(CustomOptions, CustomOptions2) end end - local function construct_TimeoutHandler(timeout, f, timeout_ret) - return function(script) -- TODO Ideally use ... (vararg) instead of `script` in case this is reused for something other than `decompile` & `getscriptbytecode` - if timeout < 0 then - return pcall(f, script) - end - - local thread = coroutine.running() - local timeoutThread, isCancelled - - timeoutThread = task.delay(timeout, function() - isCancelled = true -- TODO task.cancel - coroutine.resume(thread, nil, timeout_ret) - end) - - task.spawn(function() - local ok, result = pcall(f, script) - - if isCancelled then - return - end - - task.cancel(timeoutThread) - - while coroutine.status(thread) ~= "suspended" do - task.wait() - end - - coroutine.resume(thread, ok, result) - end) - - return coroutine.yield() - end - end - - local InstancePropertyOverrides = {} + local InstancesOverrides = {} local DecompileIgnore, IgnoreList, IgnoreProperties, NotCreatableFixes = ArrayToDictionary(OPTIONS.DecompileIgnore, true), @@ -1663,16 +1635,11 @@ local function synsaveinstance(CustomOptions, CustomOptions2) local IgnorePropertiesOfNotScriptsOnScriptsMode = OPTIONS.IgnorePropertiesOfNotScriptsOnScriptsMode local old_gethiddenproperty - if OPTIONS.IgnoreSpecialProperties and gethiddenproperty then + if OPTIONS and gethiddenproperty then old_gethiddenproperty = gethiddenproperty gethiddenproperty = nil end - local IsolateLocalPlayer = OPTIONS.IsolateLocalPlayer - local IsolateLocalPlayerCharacter = OPTIONS.IsolateLocalPlayerCharacter - local IsolateStarterPlayer = OPTIONS.IsolateStarterPlayer - local IsolatePlayers = OPTIONS.IsolatePlayers - local SaveNonCreatable = OPTIONS.SaveNonCreatable local TreatUnionsAsParts = OPTIONS.TreatUnionsAsParts @@ -1683,29 +1650,6 @@ local function synsaveinstance(CustomOptions, CustomOptions2) local IgnoreSharedStrings = OPTIONS.IgnoreSharedStrings local SharedStringOverwrite = OPTIONS.SharedStringOverwrite - local NilInstances = OPTIONS.NilInstances - - if NilInstances and enablenilinstances then -- ? Solara fix - enablenilinstances() - end - - local getbytecode - if getscriptbytecode then - getbytecode = construct_TimeoutHandler(3, getscriptbytecode) -- ? Solara fix - end - - local SaveBytecode - if OPTIONS.SaveBytecode and getscriptbytecode then - SaveBytecode = function(script) - local s, bytecode = getbytecode(script) - - if s then - if bytecode and bytecode ~= "" then - return "-- Bytecode (Base64):\n-- " .. base64encode(bytecode) .. "\n\n" - end - end - end - end local ldeccache = GLOBAL_ENV.scriptcache @@ -1761,6 +1705,10 @@ local function synsaveinstance(CustomOptions, CustomOptions2) end end + if FilePath then + FilePath = sanitizeFileName(FilePath) + end + if IsModel then placename = ( FilePath @@ -1770,6 +1718,13 @@ local function synsaveinstance(CustomOptions, CustomOptions2) placename = (FilePath or sanitizeFileName("place " .. PlaceName)) .. ".rbxlx" end + if GLOBAL_ENV[placename] then + -- warn("UniversalSynSaveInstance is already saving to this file") + return + end + + GLOBAL_ENV[placename] = true + if mode ~= "scripts" then IgnorePropertiesOfNotScriptsOnScriptsMode = nil end @@ -1851,6 +1806,15 @@ local function synsaveinstance(CustomOptions, CustomOptions2) end end + local IsolateLocalPlayer = OPTIONS.IsolateLocalPlayer + local IsolateLocalPlayerCharacter = OPTIONS.IsolateLocalPlayerCharacter + local IsolatePlayers = OPTIONS.IsolatePlayers + local IsolateStarterPlayer = OPTIONS.IsolateStarterPlayer + local NilInstances = OPTIONS.NilInstances + + if NilInstances and enablenilinstances then -- ? Solara fix + enablenilinstances() + end local function get_size_format() local Size @@ -1928,10 +1892,60 @@ local function synsaveinstance(CustomOptions, CustomOptions2) return unpack(result) end + local function construct_TimeoutHandler(timeout, f, timeout_ret) + return function(script) -- TODO Ideally use ... (vararg) instead of `script` in case this is reused for something other than `decompile` & `getscriptbytecode` + if timeout < 0 then + return pcall(f, script) + end + + local thread = coroutine.running() + local timeoutThread, isCancelled + + timeoutThread = task.delay(timeout, function() + isCancelled = true -- TODO task.cancel + coroutine.resume(thread, nil, timeout_ret) + end) + + task.spawn(function() + local ok, result = pcall(f, script) + + if isCancelled then + return + end + + task.cancel(timeoutThread) + + while coroutine.status(thread) ~= "suspended" do + task.wait() + end + + coroutine.resume(thread, ok, result) + end) + + return coroutine.yield() + end + end + local get_proxied_linkedsource = construct_TimeoutHandler(30, function(asset) return game:HttpGet("https://linkedsource.glitch.me/asset/" .. asset) end) + local getbytecode + if getscriptbytecode then + getbytecode = construct_TimeoutHandler(3, getscriptbytecode) -- ? Solara fix + end + + local SaveBytecode + if OPTIONS.SaveBytecode and getscriptbytecode then + SaveBytecode = function(script) + local s, bytecode = getbytecode(script) + + if s and bytecode and bytecode ~= "" then + return "-- Bytecode (Base64):\n-- " .. base64encode(bytecode) .. "\n\n" + end + end + end + do local Decompiler = OPTIONS.decomptype == "custom" and custom_decompiler or decompile or custom_decompiler @@ -2028,11 +2042,14 @@ local function synsaveinstance(CustomOptions, CustomOptions2) local function ReadProperty(instance, property, propertyName, special, category, optional) local raw = __BREAK - local InstanceOverride = InstancePropertyOverrides[instance] + local InstanceOverride = InstancesOverrides[instance] if InstanceOverride then - local PropertyOverride = InstanceOverride.Properties[propertyName] - if PropertyOverride then - return PropertyOverride + local PropertiesOverride = InstanceOverride.Properties + if PropertiesOverride then + local PropertyOverride = PropertiesOverride[propertyName] + if PropertyOverride ~= nil then + return PropertyOverride + end end end @@ -2121,7 +2138,7 @@ local function synsaveinstance(CustomOptions, CustomOptions2) local function ReturnValueAndTag(raw, valueType, descriptor) local value, tag = (descriptor or XML_Descriptors[valueType])(raw) - return value, tag == nil and valueType or tag + return value, tag or valueType end local function InheritsFix(fixes, className, instance) @@ -2252,10 +2269,10 @@ local function synsaveinstance(CustomOptions, CustomOptions2) do local function replaceClassName(newClassName) if InstanceName ~= ClassName then -- TODO Compare against default instance instead (TouchTransmitter is called TouchInterest by default) - InstanceOverride = InstancePropertyOverrides[instance] + InstanceOverride = InstancesOverrides[instance] if not InstanceOverride then InstanceOverride = { Properties = { Name = "[" .. ClassName .. "] " .. InstanceName } } - InstancePropertyOverrides[instance] = InstanceOverride + InstancesOverrides[instance] = InstanceOverride end end ClassName = newClassName @@ -2267,7 +2284,7 @@ local function synsaveinstance(CustomOptions, CustomOptions2) if SaveNonCreatable then replaceClassName(Fix) else - break -- They won't show up in Studio anyway (Enable IsolatePlayers if you wish to bypass this) + break -- They won't show up in Studio anyway (Enable SaveNonCreatable if you wish to bypass this) end else -- ! Assuming nothing that is a PartOperation or inherits from it is in NotCreatableFixes if TreatUnionsAsParts and instance:IsA("PartOperation") then @@ -2285,10 +2302,8 @@ local function synsaveinstance(CustomOptions, CustomOptions2) end if not InstanceOverride then - InstanceOverride = InstancePropertyOverrides[instance] + InstanceOverride = InstancesOverrides[instance] end - local ChildrenOverride = InstanceOverride and InstanceOverride.__Children - if ClassName == "" then -- * FilteredSelection ClassName = "Folder" end @@ -2296,8 +2311,8 @@ local function synsaveinstance(CustomOptions, CustomOptions2) -- ? The reason we only save .Name (and few other props in save_specific) is because -- ? we can be sure this is a custom container (ex. NilInstancesFixes) -- ? However, in case of NotCreatableFixes, the Instance might have Tags, Attributes etc. that can potentially be saved (even though it's a Folder) - if ChildrenOverride then - savebuffer[savebuffer_size] = save_specific(ClassName, InstanceOverride.Properties) -- ! Assuming anything that has __Children will have .Properties + if InstanceOverride and InstanceOverride.__SaveSpecific then + savebuffer[savebuffer_size] = save_specific(ClassName, InstanceOverride.Properties) -- ! Assuming anything that has __SaveSpecific will have .Properties savebuffer_size = savebuffer_size + 1 else -- local Properties = @@ -2547,7 +2562,7 @@ local function synsaveinstance(CustomOptions, CustomOptions2) end end - value = "-- Saved by UniversalSynSaveInstance https://discord.gg/wx4ThpAsmw\n\n" + value = "-- Saved by UniversalSynSaveInstance (Join to Copy Games) https://discord.gg/wx4ThpAsmw\n\n" .. (hasLinkedSource and "-- Original Source: https://assetdelivery.roblox.com/v1/asset/?" .. (LinkedSource_type or "id") .. "=" .. (LinkedSource or LinkedSource_Url) .. "\n\n" or "") .. value end @@ -2591,7 +2606,7 @@ local function synsaveinstance(CustomOptions, CustomOptions2) end if SkipEntirely ~= false then -- ? We save instance without it's descendants in this case (== false) - local Children = ChildrenOverride or instance:GetChildren() + local Children = InstanceOverride and InstanceOverride.__Children or instance:GetChildren() if #Children ~= 0 then save_hierarchy(Children) @@ -2680,7 +2695,8 @@ local function synsaveinstance(CustomOptions, CustomOptions2) local Fix = InheritsFix(NilInstancesFixes, ClassName, instance) if Fix then - instance = Fix(instance, InstancePropertyOverrides) + instance = Fix(instance, InstancesOverrides) + -- continue end local Class = ClassList[ClassName] @@ -2689,6 +2705,7 @@ local function synsaveinstance(CustomOptions, CustomOptions2) if ClassTags and ClassTags.Service then -- For CSGDictionaryService, NonReplicatedCSGDictionaryService, LogService, ProximityPromptService, TestService & more -- instance.Parent = game instance = nil + -- continue end end end @@ -2711,7 +2728,7 @@ local function synsaveinstance(CustomOptions, CustomOptions2) RecoveredScripts ) .. "\n" or "") .. [[ - Thank you for using UniversalSynSaveInstance https://discord.gg/wx4ThpAsmw. + Thank you for using UniversalSynSaveInstance (Join to Copy Games) https://discord.gg/wx4ThpAsmw. If you didn't save in Binary (rbxl) - it's recommended to save the game right away to take advantage of the binary format & to preserve values of certain properties if you used IgnoreDefaultProperties setting (as they might change in the future). You can do that by going to FILE -> Save to File As -> Make sure File Name ends with .rbxl -> Save @@ -2769,7 +2786,8 @@ local function synsaveinstance(CustomOptions, CustomOptions2) end end - savebuffer[savebuffer_size] = "" + savebuffer[savebuffer_size] = + "" savebuffer_size = savebuffer_size + 1 save_cache(true) do @@ -2779,9 +2797,49 @@ local function synsaveinstance(CustomOptions, CustomOptions2) -- TODO It's also not smart to filter entire file string at the end as this might also affect decompiled scripts content, which has no way of containing any user-related information. It would be better to use gsub in string Descriptor and such if OPTIONS.Anonymous then local LocalPlayer = service.Players.LocalPlayer + if LocalPlayer then + local function gsubCaseInsensitive(input, search, replacement) -- * Credits to friends + local inputLower = string.lower(input) - for _, chunk in next, chunks do - chunk.str = string.gsub(string.gsub(chunk.str, LocalPlayer.UserId, "1"), LocalPlayer.Name, "Roblox") + search = string.lower(search) + + local lastFinish = 0 + local subStrings = {} + local search_len = #search + local input_len = #input + while search_len <= input_len - lastFinish do + local init = lastFinish + 1 + + local start, finish = string.find(inputLower, search, init, true) + + if start == nil then + break + end + + table.insert(subStrings, string.sub(input, init, start - 1)) + + lastFinish = finish + end + + if lastFinish == 0 then + return input + end + + table.insert(subStrings, string.sub(input, lastFinish + 1)) + + return table.concat(subStrings, replacement) + end + + local Anonymous = type(OPTIONS.Anonymous) == "table" and OPTIONS.Anonymous + or { UserId = "1", Name = "Roblox" } + + for _, chunk in next, chunks do + chunk.str = gsubCaseInsensitive( + string.gsub(chunk.str, LocalPlayer.UserId, Anonymous.UserId), + LocalPlayer.Name, + Anonymous.Name + ) + end end end @@ -2791,7 +2849,7 @@ local function synsaveinstance(CustomOptions, CustomOptions2) for _, chunk in next, chunks do totalstr = totalstr .. chunk.str end - Callback(totalstr) + Callback(totalstr, chunks, totalsize) elseif OPTIONS.AlternativeWritefile and appendfile then local SEGMENT_SIZE = 4145728 -- Celery has an arbitrary savefile/appendfile size limit of ~4MB for reasons unknown. This is a workaround to save the file in segments. @@ -2957,7 +3015,28 @@ local function synsaveinstance(CustomOptions, CustomOptions2) local SafeMode = OPTIONS.SafeMode if SafeMode then task.spawn(function() - GetLocalPlayer():Kick("\n[SAFEMODE] Saving in Progress..\nPlease do NOT leave") + local LocalPlayer = GetLocalPlayer() + + local PlayerScripts = LocalPlayer:FindFirstChild("PlayerScripts") + if PlayerScripts then + local function construct_InstanceOverride(instance) + local children = instance:GetChildren() + InstancesOverrides[instance] = { + __Children = children, + } + for _, child in next, children do + construct_InstanceOverride(child) + end + end + construct_InstanceOverride(PlayerScripts) + + InstancesOverrides[LocalPlayer] = { + __Children = LocalPlayer:GetChildren(), + Properties = { Name = "[" .. LocalPlayer.ClassName .. "] " .. LocalPlayer.Name }, + } + end + + LocalPlayer:Kick("\n[SAFEMODE] Saving in Progress..\nPlease do NOT leave") wait_for_render() task.delay(10, service.GuiService.ClearError, service.GuiService) end) @@ -3001,7 +3080,7 @@ local function synsaveinstance(CustomOptions, CustomOptions2) connection:Disconnect() end end - -- GLOBAL_ENV.__USSI = nil + GLOBAL_ENV[placename] = nil if StatusText then task.spawn(function() elapse_t = os.clock() - elapse_t @@ -3026,7 +3105,7 @@ local function synsaveinstance(CustomOptions, CustomOptions2) end) end - if OPTIONS.ShutdownWhenDone then + if OPTIONS.ShutdownWhenDone and ok then game:Shutdown() end end diff --git a/saveinstance.luau b/saveinstance.luau index 41a0e5e..7584f75 100644 --- a/saveinstance.luau +++ b/saveinstance.luau @@ -666,7 +666,7 @@ XML_Descriptors = { end) end, ContentId = function(raw) - return raw == "" and "" or "" .. XML_Descriptors.string(raw, true) .. "", "Content" + return raw == "" and "" or "" .. XML_Descriptors.string(raw) .. "", "Content" end, CoordinateFrame = function(raw) return "" .. XML_Descriptors.CFrame(raw) .. "" @@ -1483,7 +1483,11 @@ local function synsaveinstance(CustomOptions, CustomOptions2) do -- * Load Settings local function construct_NilinstanceFix(Name, ClassName, Separate) return function(instance, instancePropertyOverrides) - local Exists = if Separate then nil else OPTIONS.NilInstancesFixes[Name] + local Exists + + if not Separate then + Exists = OPTIONS.NilInstancesFixes[Name] + end local Fix @@ -1927,10 +1931,8 @@ local function synsaveinstance(CustomOptions, CustomOptions2) SaveBytecode = function(script) local s, bytecode = getbytecode(script) - if s then - if bytecode and bytecode ~= "" then - return "-- Bytecode (Base64):\n-- " .. base64encode(bytecode) .. "\n\n" - end + if s and bytecode and bytecode ~= "" then + return "-- Bytecode (Base64):\n-- " .. base64encode(bytecode) .. "\n\n" end end end @@ -2776,7 +2778,7 @@ local function synsaveinstance(CustomOptions, CustomOptions2) if OPTIONS.Anonymous then local LocalPlayer = service.Players.LocalPlayer if LocalPlayer then - local function gsubCaseInsensitive(input, search, replacement) + local function gsubCaseInsensitive(input, search, replacement) -- * Credits to friends local inputLower = string.lower(input) search = string.lower(search)