diff --git a/README.md b/README.md index dd6b308..f34fc09 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ ```lua local Params = { - RepoURL = "https://raw.githubusercontent.com/luau/SynSaveInstance/main/", + RepoURL = "https://raw.githubusercontent.com/luau/UniversalSynSaveInstance/main/", SSI = "saveinstance", } local synsaveinstance = loadstring(game:HttpGet(Params.RepoURL .. Params.SSI .. ".luau", true), Params.SSI)() @@ -56,58 +56,20 @@ Reason: Many Executors fail miserably at providing good user experience when it - ! Check out [Rojo Rbx Dom Binary] & [Roblox Format Specifications Binary] for more documentation about the Binary File Format! - ! Also see [buffer], [bit32] libraries as well as [pack]/[unpack] from the [string] library for more information on how you can implement something like this! - ! [Rbx-Binary-Format] -- [x] Add custom decompiler in case executor doesn't have one but has getscriptbytecode (UMF) - - -- [x] Add custom timeout logic for decompiler instead of relying on executor to have one - - Using threads & coroutines. -- [x] Add `continue` where needed -- [x] Add Documentation similar to [KRNL Docs] or [Synapse X Docs] / [Synapse X Docs Old] -- [ ] Merge SharedStrings and sharedstrings tables -- [x] ~~Add fallback function for appendfile (whether through storing current xml as string or with use of readfile)~~ Removed Appendfile entirely -- [x] Add getproperties as fallback for specialinfo -- [x] ~~Add Redirects to some special (in a bad way 😡) values, more info @ [PropertyPatches v1],[PropertyPatches v2]+[PropertyPatches v3], otherwise they will fallback to default when file is opened~~ Relying on CanSave instead - - Not all though, test each & see if it carries over or not (when file is opened).. - - All current redirects: [Here](https://github.com/luau/SynSaveInstance/blob/main/TODO/PropertyPatches) -- [x] Add more Fixes for Errors that **_can_** pop up during opening process -- [x] Add Optional tags support -- [ ] Add readbinarystring or readbinarystringpropertyvalue/readbspval/getbspval (elysian) as fallback for gethiddenproperty - [ ] Add table.clone instead {} in some cases if possible -- [ ] Avoid scanning for default values of properties if those properties won't get serialized anyway (e.g. don't have a Descriptor) -- [x] Add --!native tag just in case -- [x] ~~Auto-Detect DataTypes/ValueType Categories of Properties (CFrame, UDim2 so on)~~ Full API Dump Solves this ? -- [x] Bring said DataType serializer into an outside function -- [x] ~~Bypass NotCreatable by hardcoding links/references/indexes to said Classes~~ Should be Solved by IsPropertyModified - - Example: Terrain class can be indexed by doing workspace.Terrain but is NotCreatable -- [x] Check if table.concat is actually the fastest way as compared to other alternatives (IT'S NOT) -- [x] Do ~~clean-up in inheritor &~~ (API Dumps solve this, illogical) automatically assume the top-most class that owns the property, while also cleaning up said property from classes that inherit from it - - This will be only be needed if we try to implement our own scanning for hidden properties in which case a lot of duplicates might arise that need to be tracked down to instance they all inherit from & cleaned up respectively -- [x] Fix indexes being mixed up after table.remove shifting -- [x] Hidden properties - - [x] ~~Scan for them~~ Full API Dump Solves this - - [x] ~~Scan game & map instances in format {ClassName = {Instance1, Instance2} }, if none found then attempt to create proper Replica for them~~ Full API Dump Solves this - * This will help with getting many ValueTypes accurately, especially BinaryStrings vs strings - - [x] ~~Inherit them properly & do the clean-up~~ Full API Dump Solves this - - [x] ~~Tell whether ValueType is string or BinaryString~~ Full API Dump Solves this - [ ] Support for Model files: - [x] rbxmx (xml) - [ ] rbxm (binary) -- [x] Possibly convert to non-Name tables & use instance references instead (Perhaps make a config Bool Toggle for this, false by default), ex. DecompileIgnore = {game.CoreGui} - - This will allow for more flexibility of saveinstancing -- [x] ~~Remove Useless tables & functions of specialinfo~~ Repurposed - [x] Implement [Luau Syntax] (important for performance!): - - [x] Compound Operators - [x] Avoid using `next`, `ipairs` & `pairs` - [x] ~~Interpolated strings instead of concat~~ Slower - [ ] Type-checking (😩🙀) - - [ ] `if-then-else` expressions + - [ ] ~~`if-then-else` expressions~~ No reason to - [ ] Floor division - [ ] Speed things up as much as possible - Requires benchmarks - - Requires looking at other scripts of ours that are aimed at speed & performance -- [x] Support for NotScriptable Properties - - Requires gethiddenproperty support - [ ] Support for as many [KRNL-like saveinstance Options] & [UNC]: - Change mode to invalid mode like "custom" if you only want to save ExtraInstances * [x] Decompile (! This takes priority over OPTIONS.noscripts if set !) @@ -123,39 +85,16 @@ Reason: Many Executors fail miserably at providing good user experience when it * [x] SavePlayers * [x] ShowStatus - [x] ~~Add Drawing Library support for ShowStatus~~ Can't reliably test if it's working on an executor - * [x] ~~IsolatePlayerGui~~ Use IsolateLocalPlayer instead + * [x] IsolatePlayerGui (same as IsolateLocalPlayer) * [x] Callback * [x] ~~CopyToClipboard/Clipboard~~ Use Callback instead * [ ] Binary (rbxl/rbxm) -- [x] Support for as many Executors as possible -- [x] ~~Use getspecialinfo fallback function carefully as it's hardcoded~~ Useless because there's no way to tell if the Property Values of those instances are default or not - - LOOK INTO Instance:IsPropertyModified & Instance:ResetPropertyToDefault -- [x] Isolators must clear -- [x] ~~Store all functions outside that are used during saveinstancing for sake of performance~~ Arguable - [x] ~~Remove buffersize, savebuffer & so on for sake of performance by concatenating strings to total string then writing it to file (no extra steps like table.concat)~~ table.concat proved faster in the case of huge amount of concatenations - Test table.concat vs string ..= with a full buffer (this benchmark differs per usecase) -- [ ] Make sure BinaryStrings are compared to Defaults properly (aka in same format) - - Find default values of BinaryStrings properties (MaximumADHD might have a clue) - [ ] Add Option to restart saveinstance from the point that it crashed on (perhaps by skipping) -- [ ] Check out [DataType Exceptions] -- [x] Add README Similar to current Synapse -- [x] ~~Ignore all properties of instances that aren't Local or Module Scripts except Name if mode is set to "scripts"~~ IgnorePropertiesOfNotScriptsOnScriptsMode - [ ] Maybe modes should do more than just determining the list of instances to save, like changing IgnoreDefaultProperties to false if mode is "full" for example -- [x] Add Support for [SharedStrings] - - Fun fact: SharedStrings can also be used for ValueTypes that aren't `SharedString`, this behavior is not documented anywhere but makes sense (Could create issues though, due to _potential_ ValueType mix-up). By replacing `Base64EncodedValue` with `UniqueIdentifierForSharedString` & putting `Base64EncodedValue` into SharedStrings container you can achieve this amazing behaviour. This should be only enabled using an optional setting
Only known to work with (probably because both are base64 encoded): - * BinaryString -- [x] Add Lua & Luau versions instead of merged (WARNING: LUAU WILL ALWAYS BE MORE UPDATED THAN LUA VERSION, lua version exists just for the sake of old & bad executors, ask devs of your executors to support luau as its latest & greatest) +- [x] Add Lua & Luau versions instead of merged (WARNING: LUAU MIGHT BE MORE UPDATED THAN LUA VERSION, lua version exists just for the sake of old & bad executors, ask devs of your executors to support luau as its latest & greatest) - [x] ~~Add Support for multiple Instances to be saved as a model~~ IsModel = true & ExtraInstances -- [x] Do something about devs renaming Services therefore bypassing Ignore lists (CoreGui/CorePackages are not affected) - - LOOK INTO Instance:IsPropertyModified & Instance:ResetPropertyToDefault -- [ ] Custom fallback Decompiler for ModuleScripts using require and then iterating through it, gathering all info about functions using [getupvals/getprotos/getconsts][debug], converting all DataTypes using tostring or Descriptors, and then perhaps converting to JSON. (Make use of op-codes from Dex?) !!! -- [ ] Check out varios Leaked Executors (Especially their Init / Lua scripts) to expand knowledge on the whole subject of saveinstance -- [x] ~~Fix Player's Characters not being visible (must Refresh MeshId)~~ Fixed? - - "" Could cause issues too (needs testing) - - Perhaps add a possible FIX script to README -- [x] ~~Be able to exclude / blacklist any mentions of certain string in other strings~~ Use Anonymous Option - - ~~Example: You wish to blacklist your player's name from appearing in any property value~~ - - Default options like IsolateSomething might also use / influence this - [ ] Force disable ParticleEmitters in case something like IgnorePropertiesOfNotScriptsOnScriptsMode is enabled (they stack in one place and create huge lag) - [ ] Be able to specify which special properties you want saved (to avoid saving all) @@ -182,8 +121,6 @@ resources include: [pack]: https://create.roblox.com/docs/reference/engine/libraries/string#pack [unpack]: https://create.roblox.com/docs/reference/engine/libraries/string#unpack [string]: https://create.roblox.com/docs/reference/engine/libraries/string -[DataType Exceptions]: https://github.com/rojo-rbx/rbx-dom/blob/8ca9250fa5a5ad3756c89e1e111e1aabaf698b27/rbx_reflector/src/cli/generate.rs#L196 -[KRNL Docs]: https://app.archbee.com/public/PREVIEW-2Jp4SDaAD4P1COFfx1p_t/PREVIEW-EtjA4sQe5zYUxIHwA6CqJ#mDB9D [KRNL-like saveinstance Options]: https://app.archbee.com/public/PREVIEW-2Jp4SDaAD4P1COFfx1p_t/PREVIEW-EtjA4sQe5zYUxIHwA6CqJ#mDB9D [Rojo Rbx Dom Xml]: https://github.com/rojo-rbx/rbx-dom/blob/master/docs/xml.md [Rojo Rbx Dom Binary]: https://github.com/rojo-rbx/rbx-dom/blob/master/docs/binary.md @@ -193,12 +130,5 @@ resources include: [Roblox File Format]: https://github.com/MaximumADHD/Roblox-File-Format [Roblox Format Specifications]: https://github.com/RobloxAPI/spec/ [Roblox Format Specifications Binary]: https://github.com/RobloxAPI/spec/blob/master/formats/rbxl.md -[SharedStrings]: https://github.com/RobloxAPI/spec/blob/master/formats/rbxlx.md#sharedstring -[Synapse X Docs Old]: https://synapsexdocs.github.io/custom-lua-functions/misc-functions/#save-instance -[debug]: https://web.archive.org/web/20221021015553/https://docs.synapse.to/reference/debug_lib.html -[Synapse X Docs]: https://web.archive.org/web/20230318113846/https://docs.synapse.to/reference/misc.html [Synapse X Source 2019]: https://github.com/Acrillis/SynapseX -[PropertyPatches v1]: https://github.com/MaximumADHD/Roblox-File-Format/blob/main/Plugins/GenerateApiDump/PropertyPatches.lua#L72 -[PropertyPatches v2]: https://github.com/rojo-rbx/rbx-dom/tree/master/patches -[PropertyPatches v3]: https://github.com/rojo-rbx/rbx-dom/blob/master/rbx_dom_lua/src/customProperties.lua [UNC]: https://github.com/unified-naming-convention/NamingStandard/commit/613c1956b801ace54ba141dfc60842a16608b54f diff --git a/saveinstance.lua b/saveinstance.lua index 3699a2f..e44528d 100644 --- a/saveinstance.lua +++ b/saveinstance.lua @@ -67,6 +67,7 @@ local gethiddenproperty = global_container.gethiddenproperty -- These should be universal enough local appendfile = appendfile +local isfile = isfile local readfile = readfile local writefile = writefile @@ -114,9 +115,9 @@ do -- * Load Region of Déjà Vu end end end - local function benchmark(f1, f2, ...) + local function benchmark(funcs, ...) local ranking = table.create(2) - for i, f in next, { f1, f2 } do + for i, f in next, funcs do local start = os.clock() for _ = 1, 50 do f(...) @@ -181,10 +182,16 @@ do -- * Load Region of Déjà Vu end end) + local EncodingService = game:GetService("EncodingService") + local EncodingService_base64encode = function(raw) + return buffer.tostring(EncodingService:Base64Encode(buffer.fromstring(raw))) + end + -- * Tests if base64encode exists and works properly then benchmark it if base64encode and base64encode("\1\0\0\0\1") == "AQAAAAE=" then if rbxcrypt_base64encode then - base64encode = benchmark(base64encode, rbxcrypt_base64encode, test_str) + base64encode = + benchmark({ base64encode, rbxcrypt_base64encode, EncodingService_base64encode }, test_str) end else base64encode = rbxcrypt_base64encode @@ -194,18 +201,14 @@ do -- * Load Region of Déjà Vu end end -local SharedStrings = {} -local SharedString_identifiers = setmetatable({ - identifier = 1e15, -- 1 quadrillion, up to 9.(9) quadrillion, in theory this shouldn't ever run out and be enough for all sharedstrings ever imaginable - -- TODO: worst case, add fallback to str randomizer once numbers run out : ) -}, { +local SharedString_identifier = 1e15 -- 1 quadrillion, up to 9.(9) quadrillion, in theory this shouldn't ever run out and be enough for all sharedstrings ever imaginable -- TODO: worst case, add fallback to str randomizer once numbers run out : ) +local SharedStrings = setmetatable({}, { __index = function(self, str) - local identifier = self.identifier - local Identifier = base64encode(tostring(identifier)) -- tostring is only needed for built-in base64encode, Luau base64 implementations don't need it as buffers autoconvert - self.identifier = identifier + 1 + local identifier = base64encode(tostring(SharedString_identifier)) -- tostring is only needed for built-in base64encode, Luau base64 implementations don't need it as buffers autoconvert + SharedString_identifier = SharedString_identifier + 1 - self[str] = Identifier -- ? The value of the md5 attribute is a Base64-encoded key. type elements use this key to refer to the value of the string. The value is the text content, which is Base64-encoded. Historically, the key was the MD5 hash of the string value. However, this is not required; the key can be any value that will uniquely identify the shared string. Roblox currently uses BLAKE2b truncated to 16 bytes.. - return Identifier + self[str] = identifier -- ? The value of the md5 attribute is a Base64-encoded key. type elements use this key to refer to the value of the string. The value is the text content, which is Base64-encoded. Historically, the key was the MD5 hash of the string value. However, this is not required; the key can be any value that will uniquely identify the shared string. Roblox currently uses BLAKE2b truncated to 16 bytes.. + return identifier end, }) @@ -213,18 +216,6 @@ local inherited_properties = {} local default_instances = {} local referents, ref_size = {}, 0 -- ? Roblox encodes all elements with a referent attribute. Each value is generated by starting with the prefix RBX, followed by a UUID version 4, with - characters removed, and all characters converted to uppercase. -local function __BIT(...) -- * Credits to Friend (you know yourself) - local Value = 0 - - for i, bit in next, { ... } do - if bit then - Value = Value + 2 ^ (i - 1) - end - end - - return Value -end - local function GetRef(instance) local ref = referents[instance] if not ref then @@ -251,9 +242,9 @@ local CLIENT_VERSION = tonumber(string.split(version(), ".")[2]) or 9e9 -- Veloc local attr_Type_IDs = { string = 0x02, boolean = 0x03, - -- int32 = 0x04, - -- float = 0x05, - number = 0x06, + int32 = 0x04, + -- float = 0x05, -- float32 + number = 0x06, -- float64 (double) -- Array = 0x07, -- Dictionary = 0x08, UDim = 0x09, @@ -279,6 +270,8 @@ local attr_Type_IDs = { Region3 = 0x1F, Region3int16 = 0x20, Font = 0x21, + SecurityCapabilities = 0x22, + Path2DControlPoint = 0x23, } local CFrame_Rotation_IDs = { ["\0\0\128\63\0\0\0\0\0\0\0\0\0\0\0\0\0\0\128\63\0\0\0\0\0\0\0\0\0\0\0\0\0\0\128\63"] = 0x02, @@ -306,36 +299,147 @@ local CFrame_Rotation_IDs = { ["\0\0\0\0\0\0\128\191\0\0\0\0\0\0\0\0\0\0\0\0\0\0\128\63\0\0\128\191\0\0\0\0\0\0\0\0"] = 0x22, ["\0\0\0\0\0\0\0\0\0\0\128\191\0\0\0\0\0\0\128\191\0\0\0\128\0\0\128\191\0\0\0\0\0\0\0\128"] = 0x23, } +local BASE_CAPABILITIES +pcall(function() + BASE_CAPABILITIES = SecurityCapabilities.new() +end) +local CAPABILITY_BITS = { + Plugin = 2 ^ 0, ---------------- 0 + LocalUser = 2 ^ 1, ------------- 1 + WritePlayer = 2 ^ 2, ----------- 2 + RobloxScript = 2 ^ 3, ---------- 3 + RobloxEngine = 2 ^ 4, ---------- 4 + NotAccessible = 2 ^ 5, --------- 5 + RunClientScript = 2 ^ 8, ------- 8 + RunServerScript = 2 ^ 9, ------- 9 + Unknown = 2 ^ 10, -------------- 10 (0xa) + AccessOutsideWrite = 2 ^ 11, --- 11 (0xb) + Unassigned = 2 ^ 15, ----------- 15 (0xf) + AssetRequire = 2 ^ 16, --------- 16 (0x10) + LoadString = 2 ^ 17, ----------- 17 (0x11) + ScriptGlobals = 2 ^ 18, -------- 18 (0x12) + CreateInstances = 2 ^ 19, ------ 19 (0x13) + Basic = 2 ^ 20, ---------------- 20 (0x14) + Audio = 2 ^ 21, ---------------- 21 (0x15) + DataStore = 2 ^ 22, ------------ 22 (0x16) + Network = 2 ^ 23, -------------- 23 (0x17) + Physics = 2 ^ 24, -------------- 24 (0x18) + UI = 2 ^ 25, ------------------- 25 (0x19) + CSG = 2 ^ 26, ------------------ 26 (0x1a) + Chat = 2 ^ 27, ----------------- 27 (0x1b) + Animation = 2 ^ 28, ------------ 28 (0x1c) + Avatar = 2 ^ 29, --------------- 29 (0x1d) + Input = 2 ^ 30, ---------------- 30 (0x1e) + Environment = 2 ^ 31, ---------- 31 (0x1f) + RemoteEvent = 2 ^ 32, ---------- 32 (0x20) + LegacySound = 2 ^ 33, ---------- 33 (0x21) + Players = 2 ^ 34, -------------- 34 (0x22) + CapabilityControl = 2 ^ 35, ---- 35 (0x23) + RemoteCommand = 2 ^ 59, -------- 59 (0x3b) + InternalTest = 2 ^ 60, --------- 60 (0x3c), Related to TestingGameScript + PluginOrOpenCloud = 2 ^ 61, ---- 61 (0x3d) + Assistant = 2 ^ 62, ------------ 62 (0x3e) + -- Restricted = 2 ^ 63, -------- for negative (highest bit for i64) +} + +local function __COUNT_CAPABILITY_BITS(raw) + -- TODO tostring & string.split aren't ideal but this is the only way until the feature is out of the experimental phase (SecurityCapabilities.Contains exists but the Enums that it accepts lacks some hidden bits) - NotAccessible, Unknown, Restricted + -- ! Seems like both tostring & .Contains ignore high / internal bits (anything above CapabilityControl): RemoteCommand, InternalTest, PluginOrOpenCloud, Assistant. They're present when created & saved by Studio but can't be read through current means + + local result = 0 + for _, flag in next, string.split(tostring(raw), " | ") do + local bit = CAPABILITY_BITS[flag] + if bit then + result = result + bit + end + end + return result +end + +local function __COUNT_BITS(...) -- * Credits to Friend (you know yourself) + local Value = 0 + + for i, bit in next, { ... } do + if bit then + Value = Value + 2 ^ (i - 1) + end + end + + return Value +end + local Binary_Descriptors Binary_Descriptors = { - __SEQUENCE = function(raw, valueFormatter, keypointSize, Envelope) - local Keypoints = raw.Keypoints - local Keypoints_n = #Keypoints + __PACK_MULTIPLE = function(descriptor, value1, value2, value3) + local buf1, size1 = descriptor(value1) + local buf2, size2 = descriptor(value2) + + local len = size1 + size2 + local buf3, size3 + + if value3 ~= nil then + buf3, size3 = descriptor(value3) + len = len + size3 + end - local len = 4 + (keypointSize or 12) * Keypoints_n local b = buffer.create(len) - local offset = 0 - buffer.writeu32(b, offset, Keypoints_n) - offset = offset + 4 + buffer.copy(b, 0, buf1) + buffer.copy(b, size1, buf2) - for _, keypoint in next, Keypoints do - buffer.writef32(b, offset, Envelope or keypoint.Envelope) - offset = offset + 4 - buffer.writef32(b, offset, keypoint.Time) - offset = offset + 4 - - local Value = keypoint.Value - if valueFormatter then - offset = offset + valueFormatter(Value, b, offset) - else - buffer.writef32(b, offset, Value) - offset = offset + 4 - end + if value3 ~= nil then + buffer.copy(b, size1 + size2, buf3) end return b, len end, + __construct_Sequence = function(keypoint_handler, keypointSize) + return function(raw) + local Keypoints = raw.Keypoints + local Keypoints_n = #Keypoints + + local len = 4 + keypointSize * Keypoints_n + local b = buffer.create(len) + + buffer.writeu32(b, 0, Keypoints_n) + + local offset = 4 + for _, keypoint in next, Keypoints do + keypoint_handler(keypoint, b, offset) + offset = offset + keypointSize + end + + return b, len + end + end, + __writei64 = function(b, offset, raw) + local low = bit32.band(raw, 0xFFFFFFFF) + local high = (raw - low) / 0x100000000 + + buffer.writei32(b, offset, low) + buffer.writei32(b, offset + 4, high) + end, + __PACK_F32 = nil, + __PACK_I16 = nil, + __construct__PACKER = function(float) + local writeFunc = float and buffer.writef32 or buffer.writei16 + local elementSize = float and 4 or 2 + + -- local zbuf, nozbuf = buffer.create(elementSize * 3), buffer.create(elementSize * 2) + + return function(X, Y, Z) + local len = Z and (elementSize * 3) or (elementSize * 2) + local b = buffer.create(len) + + writeFunc(b, 0, X) + writeFunc(b, elementSize, Y) + if Z then + writeFunc(b, elementSize * 2, Z) + end + + return b, len + end + end, -------------------------------------------------------------- -------------------------------------------------------------- -------------------------------------------------------------- @@ -373,38 +477,22 @@ Binary_Descriptors = { return b, 8 end, ["UDim2"] = function(raw) - local b = buffer.create(16) - - local UDim__descriptor = Binary_Descriptors.UDim - local X = UDim__descriptor(raw.X) - buffer.copy(b, 0, X) - local Y = UDim__descriptor(raw.Y) - buffer.copy(b, 8, Y) - - return b, 16 + return Binary_Descriptors.__PACK_MULTIPLE(Binary_Descriptors["UDim"], raw.X, raw.Y) end, ["Ray"] = function(raw) - local b = buffer.create(24) - - local Vector3__descriptor = Binary_Descriptors.Vector3 - local Origin = Vector3__descriptor(raw.Origin) - buffer.copy(b, 0, Origin) - local Direction = Vector3__descriptor(raw.Direction) - buffer.copy(b, 12, Direction) - - return b, 24 + return Binary_Descriptors.__PACK_MULTIPLE(Binary_Descriptors["Vector3"], raw.Origin, raw.Direction) end, ["Faces"] = function(raw) local b = buffer.create(4) - buffer.writeu32(b, 0, __BIT(raw.Right, raw.Top, raw.Back, raw.Left, raw.Bottom, raw.Front)) + buffer.writeu32(b, 0, __COUNT_BITS(raw.Right, raw.Top, raw.Back, raw.Left, raw.Bottom, raw.Front)) return b, 4 end, ["Axes"] = function(raw) local b = buffer.create(4) - buffer.writeu32(b, 0, __BIT(raw.X, raw.Y, raw.Z)) + buffer.writeu32(b, 0, __COUNT_BITS(raw.X, raw.Y, raw.Z)) return b, 4 end, @@ -416,92 +504,78 @@ Binary_Descriptors = { return b, 4 end, ["Color3"] = function(raw) - local b = buffer.create(12) - - buffer.writef32(b, 0, raw.R) - buffer.writef32(b, 4, raw.G) - buffer.writef32(b, 8, raw.B) - - return b, 12 + return Binary_Descriptors.__PACK_F32(raw.R, raw.G, raw.B) end, ["Vector2"] = function(raw) - local b = buffer.create(8) - - buffer.writef32(b, 0, raw.X) - buffer.writef32(b, 4, raw.Y) - - return b, 8 + return Binary_Descriptors.__PACK_F32(raw.X, raw.Y) end, ["Vector3"] = function(raw) - local b = buffer.create(12) - - buffer.writef32(b, 0, raw.X) - buffer.writef32(b, 4, raw.Y) - buffer.writef32(b, 8, raw.Z) - - return b, 12 + return Binary_Descriptors.__PACK_F32(raw.X, raw.Y, raw.Z) end, ["Vector2int16"] = function(raw) - local b = buffer.create(4) - - buffer.writei16(b, 0, raw.X) - buffer.writei16(b, 2, raw.Y) - - return b, 4 + return Binary_Descriptors.__PACK_I16(raw.X, raw.Y) end, ["Vector3int16"] = function(raw) - local b = buffer.create(6) - - buffer.writei16(b, 0, raw.X) - buffer.writei16(b, 2, raw.Y) - buffer.writei16(b, 4, raw.Z) - - return b, 6 + return Binary_Descriptors.__PACK_I16(raw.X, raw.Y, raw.Z) end, ["CFrame"] = function(raw) local X, Y, Z, R00, R01, R02, R10, R11, R12, R20, R21, R22 = raw:GetComponents() + local rotation_ID + do + local b = buffer.create(36) - local rotation_ID = CFrame_Rotation_IDs[string.pack(" needn't be escaped in text @@ -653,9 +755,6 @@ XML_Descriptors = { __CDATA = function(raw) -- ? Normally Roblox doesn't use CDATA unless the string has newline characters (\n); We rather CDATA everything for sake of speed return "" end, - __ENUM = function(raw) - return raw.Value, "token" - end, __NORMALIZE_NUMBER = function(raw) if raw ~= raw then return "NAN" @@ -676,26 +775,19 @@ XML_Descriptors = { __PROTECTEDSTRING = function(raw) -- ? its purpose is to "protect" data from being treated as ordinary character data during processing; return string_find(raw, "]]>") and string.gsub(raw, ESCAPES_PATTERN, ESCAPES) or XML_Descriptors.__CDATA(raw) end, - __SEQUENCE = function(raw, valueFormatter) + __construct_Sequence = function(keypoint_handler) -- The value is the text content, formatted as a space-separated list of floating point numbers. -- tostring(raw) also works (but way slower rn) - local __NORMALIZE_RANGE = XML_Descriptors.__NORMALIZE_RANGE + -- ? Trailing whitespace after Envelope is needed for lune compatibility + return function(raw) + local sequence = "" - local sequence = "" + for _, keypoint in next, raw.Keypoints do + sequence = sequence .. keypoint_handler(keypoint) + end - for _, keypoint in next, raw.Keypoints do - local Value = keypoint.Value - - sequence = sequence - .. keypoint.Time - .. " " - .. ( - valueFormatter and valueFormatter(Value) - or __NORMALIZE_RANGE(Value) .. " " .. __NORMALIZE_RANGE(keypoint.Envelope) .. " " - ) -- ? Trailing whitespace is only needed for lune compatibility + return sequence end - - return sequence end, __VECTOR = function(X, Y, Z) -- Each element is a local Value = "" .. X .. "" .. Y .. "" -- There is no Vector without at least two Coordinates.. (Vector1, at least on Roblox) @@ -712,13 +804,12 @@ XML_Descriptors = { Axes = function(raw) -- The text of this element is formatted as an integer between 0 and 7 - return "" .. __BIT(raw.X, raw.Y, raw.Z) .. "" + return "" .. __COUNT_BITS(raw.X, raw.Y, raw.Z) .. "" end, - -- ? Roblox uses CDATA only for these (try to prove this wrong): CollisionGroupData, SmoothGrid, MaterialColors, PhysicsGrid -- ! Assuming all base64 encoded strings won't have newlines - BinaryString = function(raw) + BinaryString = function(raw) -- ! only add raw == nil if such edge-case exists (note it) return raw == "" and "" or base64encode(raw) end, @@ -749,6 +840,48 @@ XML_Descriptors = { .. "", "CoordinateFrame" end, + -- CFrameQuat = function(raw) -- ? This will probably never release as it's not even used anywhere naturally, but there are hints it does exist as a DataType + -- local X, Y, Z, R00, R01, R02, R10, R11, R12, R20, R21, R22 = raw:GetComponents() + -- local trace = R00 + R11 + R22 + -- local S, QW, QX, QY, QZ + + -- if trace > 0 then + -- S = math.sqrt(1 + trace) * 2 + -- QW = 0.25 * S + -- QX = (R21 - R12) / S + -- QY = (R02 - R20) / S + -- QZ = (R10 - R01) / S + -- elseif (R00 > R11) and (R00 > R22) then + -- S = math.sqrt(1 + R00 - R11 - R22) * 2 + -- QW = (R21 - R12) / S + -- QX = 0.25 * S + -- QY = (R01 + R10) / S + -- QZ = (R02 + R20) / S + -- elseif R11 > R22 then + -- S = math.sqrt(1 + R11 - R00 - R22) * 2 + -- QW = (R02 - R20) / S + -- QX = (R01 + R10) / S + -- QY = 0.25 * S + -- QZ = (R12 + R21) / S + -- else + -- S = math.sqrt(1 + R22 - R00 - R11) * 2 + -- QW = (R10 - R01) / S + -- QX = (R02 + R20) / S + -- QY = (R12 + R21) / S + -- QZ = 0.25 * S + -- end + + -- return XML_Descriptors.__VECTOR(X, Y, Z) + -- .. "" + -- .. QX + -- .. "" + -- .. QY + -- .. "" + -- .. QZ + -- .. "" + -- .. QW + -- .. "" + -- end, Color3 = function(raw) -- Each element is a return "" .. raw.R .. "" .. raw.G .. "" .. raw.B .. "" -- ? It is recommended that Color3 is encoded with elements instead of text. end, @@ -767,37 +900,41 @@ XML_Descriptors = { -- return tonumber(string.format("0xFF%02X%02X%02X",raw.R*255,raw.G*255,raw.B*255)) end, - ColorSequence = function(raw) - -- The value is the text content, formatted as a space-separated list of FLOATing point numbers. + ColorSequence = nil, + ColorSequenceKeypoint = function(keypoint) + local __NORMALIZE_RANGE = XML_Descriptors.__NORMALIZE_RANGE - return XML_Descriptors.__SEQUENCE(raw, function(color3) - local __NORMALIZE_RANGE = XML_Descriptors.__NORMALIZE_RANGE + local color3 = keypoint.Value - return __NORMALIZE_RANGE(color3.R) - .. " " - .. __NORMALIZE_RANGE(color3.G) - .. " " - .. __NORMALIZE_RANGE(color3.B) - .. " 0 " - end) + return __NORMALIZE_RANGE(keypoint.Time) + .. " " + .. __NORMALIZE_RANGE(color3.R) + .. " " + .. __NORMALIZE_RANGE(color3.G) + .. " " + .. __NORMALIZE_RANGE(color3.B) + .. " 0 " end, - Content = function(raw) + Content = function(raw) -- TODO Not sure about Object & Opaque, run tests when possible local SourceType = raw.SourceType return SourceType == Enum.ContentSourceType.None and "" or SourceType == Enum.ContentSourceType.Uri and "" .. XML_Descriptors.string(raw.Uri) .. "" - or SourceType == Enum.ContentSourceType.Object and "" .. GetRef(raw.Object) - .. "" -- TODO Not sure, run tests + or SourceType == Enum.ContentSourceType.Object and "" .. GetRef(raw.Object) .. "" + or SourceType == Enum.ContentSourceType.Opaque and "" .. GetRef(raw.Opaque) .. "" end, - ContentId = function(raw) + ContentId = function(raw) -- ! only add raw == nil if such edge-case exists (note it) return raw == "" and "" or "" .. XML_Descriptors.string(raw) .. "", "Content" -- TODO Remove "Content" str once Roblox fully releases Content DataType end, CoordinateFrame = function(raw) return "" .. XML_Descriptors.CFrame(raw) .. "" end, -- DateTime = function(raw) return raw.UnixTimestampMillis end, -- ? Not sure + EnumItem = function(raw) + return raw.Value, "token" + end, Faces = function(raw) -- The text of this element is formatted as an integer between 0 and 63 - return "" .. __BIT(raw.Right, raw.Top, raw.Back, raw.Left, raw.Bottom, raw.Front) .. "" + return "" .. __COUNT_BITS(raw.Right, raw.Top, raw.Back, raw.Left, raw.Bottom, raw.Front) .. "" end, Font = 636 < CLIENT_VERSION and function(raw) @@ -813,13 +950,13 @@ XML_Descriptors = { return "" .. XML_Descriptors.ContentId(raw.Family) .. "" - .. (ok_w and XML_Descriptors.__ENUM(weight) or "") + .. (ok_w and XML_Descriptors.EnumItem(weight) or "") .. "" end or function(raw) - local FontString = tostring(raw) -- TODO: Temporary fix + local FontString = tostring(raw) local EmptyWeight = string_find(FontString, "Weight = ,") local EmptyStyle = string_find(FontString, "Style = }") @@ -827,7 +964,7 @@ XML_Descriptors = { return "" .. XML_Descriptors.ContentId(raw.Family) .. "" - .. (EmptyWeight and "" or XML_Descriptors.__ENUM(raw.Weight)) + .. (EmptyWeight and "" or XML_Descriptors.EnumItem(raw.Weight)) .. "" @@ -840,8 +977,16 @@ XML_Descriptors = { return __NORMALIZE_RANGE(raw.Min) .. " " .. __NORMALIZE_RANGE(raw.Max) --[[.. " "]] -- ! This might be required for compatibility; __NORMALIZE_RANGE is not needed here but it fixes the issue where "nan 10" value would reset to "0 0" end, NumberSequence = nil, - -- NumberSequence = Descriptors.__SEQUENCE, + NumberSequenceKeypoint = function(keypoint) + local __NORMALIZE_RANGE = XML_Descriptors.__NORMALIZE_RANGE + return __NORMALIZE_RANGE(keypoint.Time) + .. " " + .. __NORMALIZE_RANGE(keypoint.Value) + .. " " + .. __NORMALIZE_RANGE(keypoint.Envelope) + .. " " + end, -- Path2DControlPoint = function(raw) -- ? Not sure -- local udim2 = XML_Descriptors.UDim2 -- return "" @@ -884,7 +1029,7 @@ XML_Descriptors = { Rect = function(raw) return XML_Descriptors.__MINMAX(raw.Min, raw.Max, XML_Descriptors.Vector2), "Rect2D" end, - Region3 = function(raw) -- ? Not sure yet (/Network/Replicator.cpp#L1306) + Region3 = function(raw) -- ? Not sure about xml format yet, the math is correct though (/Network/Replicator.cpp#L1306) local Translation = raw.CFrame.Position local HalfSize = raw.Size * 0.5 @@ -898,21 +1043,19 @@ XML_Descriptors = { return XML_Descriptors.__MINMAX(raw.Min, raw.Max, XML_Descriptors.Vector3int16) end, SharedString = function(raw) - raw = raw == "" and "" or base64encode(raw) - - local Identifier = SharedString_identifiers[raw] - - if SharedStrings[Identifier] == nil then - SharedStrings[Identifier] = raw + return SharedStrings[XML_Descriptors.BinaryString(raw)] + end, + SecurityCapabilities = function(raw) + if raw == BASE_CAPABILITIES then + return 0 end - return Identifier + return __COUNT_CAPABILITY_BITS(raw) end, - SecurityCapabilities = nil, - -- SystemAddress = function(raw) return raw end, -- PeerId? -- ? Not sure + -- SystemAddress = function(raw) return raw end, -- PeerId? systemAddress as a string in the format "IP|Port", "|" being portDelineator, should not be '.', ':', '%', '-', '/', a number, or a-f -- ? Not sure (binaryAddress) -- TweenInfo = function(raw) -- ? Not sure -- local __NORMALIZE_NUMBER = XML_Descriptors.__NORMALIZE_NUMBER - -- local enum = XML_Descriptors.__ENUM + -- local EnumItem = XML_Descriptors.EnumItem -- return "" savebuffer_size = savebuffer_size + 1 + __DARKLUA_CONTINUE_62 = true until true + if not __DARKLUA_CONTINUE_62 then + break + end end end - local function save_extra(name, hierarchy, customClassName, source) - savebuffer[savebuffer_size] = save_specific((customClassName or "Folder"), { Name = name, Source = source }) - savebuffer_size = savebuffer_size + 1 - if hierarchy then - save_hierarchy(hierarchy) + local function save_extra(name, instanceOrTable, saveProps, customClassName, source) + if not customClassName then + customClassName = "Folder" + end + + local properties = { Name = name, Source = source } + local hierarchy + + if instanceOrTable then + if type(instanceOrTable) == "table" then + hierarchy = instanceOrTable + else + hierarchy = instanceOrTable:GetChildren() + if saveProps then + -- IgnoreList[instanceOrTable] = nil + -- IgnoreNotArchivable = false + + InstancesOverrides[instanceOrTable] = { + __ClassName = customClassName, -- ! Assuming any class that contains ProtectedString is never passed, because it expects bytecode, not normal code + __Children = hierarchy, + Properties = properties, + } + + save_hierarchy({ instanceOrTable }) + end + end + end + + if not saveProps then + savebuffer[savebuffer_size] = save_specific(customClassName, properties) + savebuffer_size = savebuffer_size + 1 + if hierarchy then + save_hierarchy(hierarchy) + end + savebuffer[savebuffer_size] = "
" + savebuffer_size = savebuffer_size + 1 end - savebuffer[savebuffer_size] = "" - savebuffer_size = savebuffer_size + 1 end local function save_game() do if IsModel then --[[ - -- ? Roblox encodes the following additional attributes. These are not required. Moreover, any defined schemas are ignored, and not required for a file to be valid: xmlns:xmime="http://www.w3.org/2005/05/xmlmime" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://www.roblox.com/roblox.xsd" + -- ? Roblox encodes the following additional attributes. These are not required. Moreover, any defined schemas are ignored, and not required for a file to be valid: xmlns:xmime="http://www.w3.org/2005/05/xmlmime" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://www.roblox.com/roblox.xsd" Also http can be converted to https but not sure if Roblox cares -- ? nullnil - is a legacy concept that is no longer used. ]] header = header .. 'true' end - - writefile(placename, header) -- TODO This is sort of useless if writefile will be used at the end (like if AlternativeWritefile and Callback are unused) + if writefile and not OPTIONS.Callback then + writefile(placename, header) -- TODO This is sort of useless if writefile will be used at the end (like if AlternativeWritefile and Callback are unused) + end end -- TODO Find a better solution for this @@ -2965,12 +3590,12 @@ local function synsaveinstance(CustomOptions, CustomOptions2) if LocalPlayer then if IsolateLocalPlayer then SaveNotCreatable = true - save_extra("LocalPlayer", LocalPlayer:GetChildren()) + save_extra("LocalPlayer", LocalPlayer, true) end if IsolateLocalPlayerCharacter then local LocalPlayerCharacter = LocalPlayer.Character if LocalPlayerCharacter then - save_extra("LocalPlayer Character", LocalPlayerCharacter:GetChildren(), "Model") + save_extra("LocalPlayer Character", LocalPlayerCharacter, true, "Model") end end end @@ -2978,12 +3603,12 @@ local function synsaveinstance(CustomOptions, CustomOptions2) if IsolateStarterPlayer then -- SaveNotCreatable = true -- TODO: Enable if StarterPlayerScripts or StarterCharacterScripts stop showing up in isolated folder in Studio - save_extra("StarterPlayer", service.StarterPlayer:GetChildren()) + save_extra("StarterPlayer", service.StarterPlayer) -- no reason to saveprops as you can see the props on the original instance end if IsolatePlayers then SaveNotCreatable = true - save_extra("Players", service.Players:GetChildren()) + save_extra("Players", service.Players) -- no reason to saveprops as you can see the props on the original instance end if NilInstances and global_container.getnilinstances then @@ -3007,8 +3632,7 @@ local function synsaveinstance(CustomOptions, CustomOptions2) local Class = ClassList[ClassName] if Class then - local ClassTags = Class.Tags - if ClassTags and ClassTags.Service then -- For CSGDictionaryService, NonReplicatedCSGDictionaryService, LogService, ProximityPromptService, TestService & more + if Class.Service then -- For CSGDictionaryService, NonReplicatedCSGDictionaryService, LogService, ProximityPromptService, TestService & more -- instance.Parent = game instance = nil -- continue @@ -3028,6 +3652,7 @@ local function synsaveinstance(CustomOptions, CustomOptions2) save_extra( "README", nil, + nil, "Script", "--[[\n" .. (#RecoveredScripts ~= 0 and "\t\tIMPORTANT: Original Source of these Scripts was Recovered: " .. service.HttpService:JSONEncode( @@ -3067,10 +3692,12 @@ local function synsaveinstance(CustomOptions, CustomOptions2) Or Destroy the Camera. This file was generated with the following settings: - ]] + ]] .. service.HttpService:JSONEncode(OPTIONS) .. "\n\n\t\tElapsed time: " .. os.clock() - elapse_t + .. " Date (UTC): " + .. DateTime.now():FormatUniversalTime("LL LTS", "en-gb") .. " PlaceId: " .. game.PlaceId .. " PlaceVersion: " @@ -3084,11 +3711,11 @@ local function synsaveinstance(CustomOptions, CustomOptions2) end do local tmp = { "" } - for identifier, value in next, SharedStrings do + for value, identifier in next, SharedStrings do table.insert(tmp, '' .. value .. "") end - if 1 < #tmp then -- TODO: This sucks so much because we try to iterate a table just to check this (check above) + if 1 < #tmp then -- next(SharedStrings) check also works but seems to be slower savebuffer[savebuffer_size] = table.concat(tmp) savebuffer_size = savebuffer_size + 1 savebuffer[savebuffer_size] = "" @@ -3200,7 +3827,6 @@ local function synsaveinstance(CustomOptions, CustomOptions2) ) end end - table.clear(SharedStrings) end local Connections @@ -3327,7 +3953,7 @@ local function synsaveinstance(CustomOptions, CustomOptions2) task.spawn(function() local LocalPlayer = GetLocalPlayer() - local PlayerScripts = LocalPlayer:FindFirstChild("PlayerScripts") + local PlayerScripts = LocalPlayer:FindFirstChildOfClass("PlayerScripts") if PlayerScripts then local function construct_InstanceOverride(instance) local children = instance:GetChildren() @@ -3404,11 +4030,13 @@ local function synsaveinstance(CustomOptions, CustomOptions2) end end GLOBAL_ENV[placename] = nil + + elapse_t = os.clock() - elapse_t + local Log10 = math.log10(elapse_t) + local ExtraTime = 10 + if StatusText then task.spawn(function() - elapse_t = os.clock() - elapse_t - local Log10 = math.log10(elapse_t) - local ExtraTime = 10 if ok then StatusText.Text = string.format("Saved! Time %.3f seconds; Size %s", elapse_t, get_size_format()) StatusText.TextColor3 = Color3.new(0, 1) @@ -3429,6 +4057,7 @@ local function synsaveinstance(CustomOptions, CustomOptions2) end if OPTIONS.ShutdownWhenDone and ok then + task.wait(Log10 * 2 + ExtraTime) game:Shutdown() end end diff --git a/saveinstance.luau b/saveinstance.luau index ae5fe93..bc37ebf 100644 --- a/saveinstance.luau +++ b/saveinstance.luau @@ -201,17 +201,14 @@ do -- * Load Region of Déjà Vu end end -local SharedStrings = {} -local SharedString_identifiers = setmetatable({ - identifier = 1e15, -- 1 quadrillion, up to 9.(9) quadrillion, in theory this shouldn't ever run out and be enough for all sharedstrings ever imaginable - -- TODO: worst case, add fallback to str randomizer once numbers run out : ) -}, { +local SharedString_identifier = 1e15 -- 1 quadrillion, up to 9.(9) quadrillion, in theory this shouldn't ever run out and be enough for all sharedstrings ever imaginable -- TODO: worst case, add fallback to str randomizer once numbers run out : ) +local SharedStrings = setmetatable({}, { __index = function(self, str) - local Identifier = base64encode(tostring(self.identifier)) -- tostring is only needed for built-in base64encode, Luau base64 implementations don't need it as buffers autoconvert - self.identifier += 1 + local identifier = base64encode(tostring(SharedString_identifier)) -- tostring is only needed for built-in base64encode, Luau base64 implementations don't need it as buffers autoconvert + SharedString_identifier += 1 - self[str] = Identifier -- ? The value of the md5 attribute is a Base64-encoded key. type elements use this key to refer to the value of the string. The value is the text content, which is Base64-encoded. Historically, the key was the MD5 hash of the string value. However, this is not required; the key can be any value that will uniquely identify the shared string. Roblox currently uses BLAKE2b truncated to 16 bytes.. - return Identifier + self[str] = identifier -- ? The value of the md5 attribute is a Base64-encoded key. type elements use this key to refer to the value of the string. The value is the text content, which is Base64-encoded. Historically, the key was the MD5 hash of the string value. However, this is not required; the key can be any value that will uniquely identify the shared string. Roblox currently uses BLAKE2b truncated to 16 bytes.. + return identifier end, }) @@ -812,7 +809,7 @@ XML_Descriptors = { -- ! Assuming all base64 encoded strings won't have newlines - BinaryString = function(raw) + BinaryString = function(raw) -- ! only add raw == nil if such edge-case exists (note it) return raw == "" and "" or base64encode(raw) end, @@ -925,7 +922,7 @@ XML_Descriptors = { or SourceType == Enum.ContentSourceType.Object and "" .. GetRef(raw.Object) .. "" or SourceType == Enum.ContentSourceType.Opaque and "" .. GetRef(raw.Opaque) .. "" end, - ContentId = function(raw) + ContentId = function(raw) -- ! only add raw == nil if such edge-case exists (note it) return raw == "" and "" or "" .. XML_Descriptors.string(raw) .. "", "Content" -- TODO Remove "Content" str once Roblox fully releases Content DataType end, CoordinateFrame = function(raw) @@ -959,7 +956,7 @@ XML_Descriptors = { .. "" end or function(raw) - local FontString = tostring(raw) -- TODO: Temporary fix + local FontString = tostring(raw) local EmptyWeight = string_find(FontString, "Weight = ,") local EmptyStyle = string_find(FontString, "Style = }") @@ -1046,15 +1043,7 @@ XML_Descriptors = { return XML_Descriptors.__MINMAX(raw.Min, raw.Max, XML_Descriptors.Vector3int16) end, SharedString = function(raw) - raw = raw == "" and "" or base64encode(raw) - - local Identifier = SharedString_identifiers[raw] - - if SharedStrings[Identifier] == nil then - SharedStrings[Identifier] = raw - end - - return Identifier + return SharedStrings[XML_Descriptors.BinaryString(raw)] end, SecurityCapabilities = function(raw) if raw == BASE_CAPABILITIES then @@ -1063,7 +1052,7 @@ XML_Descriptors = { return __COUNT_CAPABILITY_BITS(raw) end, - -- SystemAddress = function(raw) return raw end, -- PeerId? systemAddress as a string in the format "IP|Port", "|" being portDelineator, should not be '.', ':', '%', '-', '/', a number, or a-f -- ? Not sure + -- SystemAddress = function(raw) return raw end, -- PeerId? systemAddress as a string in the format "IP|Port", "|" being portDelineator, should not be '.', ':', '%', '-', '/', a number, or a-f -- ? Not sure (binaryAddress) -- TweenInfo = function(raw) -- ? Not sure -- local __NORMALIZE_NUMBER = XML_Descriptors.__NORMALIZE_NUMBER -- local EnumItem = XML_Descriptors.EnumItem @@ -1111,16 +1100,16 @@ XML_Descriptors = { .. "" end, - -- UniqueId = function(raw) -- ? Not sure -- ? No idea if this even needs a Descriptor - -- --[[ - -- UniqueId properties might be random everytime Studio saves a place file - -- and don't have a use right now outside of packages, which SSI doesn't - -- account for anyway. They generate diff noise, so we shouldn't serialize - -- them until we have to. - -- ]] - -- -- https://github.com/MaximumADHD/Roblox-Client-Tracker/blob/master/LuaPackages/Packages/_Index/ApolloClient/ApolloClient/utilities/common/makeUniqueId.lua#L68 - -- return raw -- seems to be string type by default - -- end, + UniqueId = function(raw) + -- --[[ + -- UniqueId properties might be random everytime Studio saves a place file + -- and don't have a use right now outside of packages, which SSI doesn't + -- account for anyway. They generate diff noise, so we shouldn't serialize + -- them until we have to. + -- ]] + -- -- https://github.com/MaximumADHD/Roblox-Client-Tracker/blob/master/LuaPackages/Packages/_Index/ApolloClient/ApolloClient/utilities/common/makeUniqueId.lua#L68 + return string.gsub(raw, "-", "") -- seems to be string type by default + end, Vector2 = function(raw) --[[ @@ -1493,6 +1482,18 @@ do return buffer.tostring(b) end, }, + AnimationClip = { + GuidBinaryString = function(instance) -- RobloxScriptSecurity + local cleanGuid = string.gsub(instance.Guid, "[{}-]", "") + local bytes = table.create(16) + for i = 1, 32, 2 do + local hexByte = string.sub(cleanGuid, i, i + 1) + local byte = tonumber(hexByte, 16) + table.insert(bytes, string.char(byte)) + end + return table.concat(bytes) + end, + }, AnimationRigData = { label = function(instance) local labels = instance:GetLabels() -- RobloxScriptSecurity @@ -1619,7 +1620,7 @@ do return AttenuationSerialize(instance:GetDistanceAttenuation()) end, }, - -- DebuggerBreakpoint = {line="Line"}, -- ? This shouldn't appear in live games (try to prove this wrong) + DebuggerBreakpoint = { line = "Line" }, -- ? This shouldn't appear in live games (try to prove this wrong) BallSocketConstraint = { MaxFrictionTorqueXml = "MaxFrictionTorque" }, BasePart = { Color3uint8 = "Color", @@ -1692,7 +1693,15 @@ do formFactorRaw = "FormFactor", }, Fire = { heat_xml = "Heat", size_xml = "Size" }, - Humanoid = { Health_XML = "Health" }, + Humanoid = { + Health_XML = "Health", + InternalBodyScale = function(instance) -- RobloxScriptSecurity + return instance:GetAccessoryHandleScale(instance.Parent.HumanoidRootPart, Enum.BodyPartR15.RootPart) -- It doesn't matter if it errors due to missing HumanoidRootPart, the function just won't be called ever again in such case + end, + InternalHeadScale = function(instance) -- RobloxScriptSecurity + return instance:GetAccessoryHandleScale(instance.Parent.Head, Enum.BodyPartR15.Head).X -- It doesn't matter if it errors due to missing Head, the function just won't be called ever again in such case; X, Y, Z seem to be always equal + end, + }, HumanoidDescription = { EmotesDataInternal = function(instance) local emotes_data = "" @@ -1715,8 +1724,14 @@ do end, }, MaterialService = { Use2022MaterialsXml = "Use2022Materials" }, -- RobloxScriptSecurity + VideoPlayer = { + PlayingReplicating = "IsPlaying", -- CanSave & CanLoad false + }, Model = { + Scale = function(instance) -- CanSave & CanLoad false + return instance:GetScale() + end, ScaleFactor = function(instance) return instance:GetScale() end, @@ -1728,26 +1743,32 @@ do StarterPlayer = { AvatarJointUpgrade_Serialized = "AvatarJointUpgrade" }, Smoke = { size_xml = "Size", opacity_xml = "Opacity", riseVelocity_xml = "RiseVelocity" }, Sound = { + xmlRead_MinDistance_3 = "RollOffMinDistance", -- * Also MinDistance xmlRead_MaxDistance_3 = "RollOffMaxDistance", -- * Also MaxDistance }, - -- ViewportFrame = { -- * Pointless because these reflect CurrentCamera's properties - -- CameraCFrame = function(instance) -- * - -- local CurrentCamera = instance.CurrentCamera - -- if CurrentCamera then - -- return CurrentCamera.CFrame - -- else - -- error("No CurrentCamera", 2) - -- end - -- end, - -- -- CameraFieldOfView = - -- }, + ViewportFrame = { + CameraCFrame = function(instance) + local CurrentCamera = instance.CurrentCamera + + return CurrentCamera and CurrentCamera.CFrame or CFrame.identity + end, + CameraFieldOfView = function(instance) + local CurrentCamera = instance.CurrentCamera + + return math.rad(CurrentCamera and CurrentCamera.FieldOfView or 70) + end, + }, WeldConstraint = { + CFrame0 = function(instance) -- Assuming CFrame1 is the same just with Part1 -> Part0 + local Part0, Part1 = instance.Part0, instance.Part1 + + return Part0 and Part1 and Part0.CFrame:ToObjectSpace(Part1.CFrame) or CFrame.identity + end, Part0Internal = "Part0", Part1Internal = "Part1", - -- State = function(instance) - -- -- If untouched then default state is 3 (default true) - -- return instance.Enabled and 1 or 0 - -- end, + State = function(instance) + return __COUNT_BITS(instance.Enabled, instance.Active) + end, }, Workspace = { -- SignalBehavior2 = "SignalBehavior", -- * Both are NotScriptable so it doesn't make sense to keep @@ -1796,6 +1817,11 @@ do end, }, } + for _, enum_item in Enum.Material:GetEnumItems() do + NotScriptableFixes.MaterialService[enum_item.Name .. "Name"] = function(instance) + return instance:GetBaseMaterialOverride(enum_item) + end + end local function FetchAPI() -- Credits @MaximumADHD @@ -1875,10 +1901,33 @@ do end local classList = {} + local tmp_classDict = {} local ClassesWhitelist, ClassesBlacklist = ClassPropertyExceptions.Whitelist, ClassPropertyExceptions.Blacklist - for _, API_Class in service.HttpService:JSONDecode(API_Dump) do + local API_Dump_Decoded = service.HttpService:JSONDecode(API_Dump) + + -- First pass (prep) + for _, API_Class in API_Dump_Decoded do + local ClassName = API_Class.Name + local props = {} + + for _, Member in API_Class.Members do + local MemberType = Member.MemberType + if MemberType == "Property" or MemberType == "Function" then + props[Member.Name] = { + ValueType = MemberType == "Property" and Member.ValueType.Name, + MemberType = MemberType, + -- Serialization = Member.Serialization, + } + end + end + + tmp_classDict[ClassName] = props + end + + -- Second pass (actual) + for _, API_Class in API_Dump_Decoded do local ClassProperties, ClassProperties_size = {}, 1 local Class = { Properties = ClassProperties, @@ -1948,6 +1997,21 @@ do end end + local preferredDescriptorProp + if PreferredDescriptorName then + preferredDescriptorProp = tmp_classDict[ClassName][PreferredDescriptorName] + + if -- Prevents type mismatch + preferredDescriptorProp == nil + or ( + preferredDescriptorProp.MemberType == "Property" + and ValueType_Name ~= preferredDescriptorProp.ValueType + ) + then -- For ex. (if they were notscriptable) CollisionGroupId (int) -> CollisionGroup (string) + PreferredDescriptorName = nil + end + end + -- if not Special then local Property = { Name = PropertyName, @@ -1966,26 +2030,32 @@ do Property.Optional = string.sub(ValueType_Name, 9) end - if NotScriptableFixClass then - local NotScriptableFix = NotScriptableFixClass[PropertyName] - if NotScriptableFix then - Property.Fallback = type(NotScriptableFix) == "function" and NotScriptableFix - or PreferredDescriptorName and function(instance) - local o, r = pcall(index, instance, PreferredDescriptorName) - if o then - return r - end - return instance[NotScriptableFix] + local NotScriptableFix = NotScriptableFixClass and NotScriptableFixClass[PropertyName] + local accessFunc = PreferredDescriptorName + and ( + preferredDescriptorProp.MemberType == "Property" + and function(instance) + return instance[PreferredDescriptorName] end - or function(instance) - return instance[NotScriptableFix] + or function(instance) -- Assume MemberType is "Function" + return instance[PreferredDescriptorName](instance) + end + ) + + Property.Fallback = NotScriptableFix + and (type(NotScriptableFix) == "function" and NotScriptableFix or accessFunc and function( + instance + ) + local o, r = pcall(accessFunc, instance) + if o then + return r end - end - elseif PreferredDescriptorName then - Property.Fallback = function(instance) - return instance[PreferredDescriptorName] - end - end + return instance[NotScriptableFix] + end or function(instance) + return instance[NotScriptableFix] + end) + or accessFunc + ClassProperties[ClassProperties_size] = Property ClassProperties_size += 1 @@ -1998,8 +2068,6 @@ do classList[ClassName] = Class end - -- classList.Instance.Properties.Parent = nil -- ? Not sure if this is a better option than filtering through properties to remove this - return classList end @@ -2086,7 +2154,7 @@ local GLOBAL_ENV = getgenv and getgenv() or _G or shared Saves instances with specified options. Example: ```lua local Params = { - RepoURL = "https://raw.githubusercontent.com/luau/SynSaveInstance/main/", + RepoURL = "https://raw.githubusercontent.com/luau/UniversalSynSaveInstance/main/", SSI = "saveinstance", } @@ -2809,7 +2877,7 @@ local function synsaveinstance(CustomOptions, CustomOptions2) return "-- Not found in already decompiled ScriptCache" end - task.wait() -- TODO Maybe remove? + -- task.wait() -- TODO Maybe remove? end local ok, result = run_with_loading("Decompiling " .. script.Name, true, nil, decomp, script) @@ -3251,8 +3319,7 @@ local function synsaveinstance(CustomOptions, CustomOptions2) PropertyName == "PlayerToHideFrom" or ValueType ~= "Instance" and ValueType ~= Fix ) - then - -- * To avoid errors + then -- * To avoid errors continue end end @@ -3588,6 +3655,8 @@ local function synsaveinstance(CustomOptions, CustomOptions2) .. service.HttpService:JSONEncode(OPTIONS) .. "\n\n\t\tElapsed time: " .. os.clock() - elapse_t + .. " Date (UTC): " + .. DateTime.now():FormatUniversalTime("LL LTS", "en-gb") .. " PlaceId: " .. game.PlaceId .. " PlaceVersion: " @@ -3601,11 +3670,11 @@ local function synsaveinstance(CustomOptions, CustomOptions2) end do local tmp = { "" } - for identifier, value in SharedStrings do + for value, identifier in SharedStrings do table.insert(tmp, '' .. value .. "") end - if 1 < #tmp then -- TODO: This sucks so much because we try to iterate a table just to check this (check above) + if 1 < #tmp then -- next(SharedStrings) check also works but seems to be slower savebuffer[savebuffer_size] = table.concat(tmp) savebuffer_size += 1 savebuffer[savebuffer_size] = "" @@ -3717,7 +3786,6 @@ local function synsaveinstance(CustomOptions, CustomOptions2) ) end end - table.clear(SharedStrings) end local Connections