Files
UniversalSynSaveInstance/saveinstance.luau
2026-01-28 18:07:41 +02:00

4044 lines
133 KiB
Lua

--!native
--!optimize 2
--!divine-intellect
-- https://discord.gg/wx4ThpAsmw
local function string_find(s, pattern)
return string.find(s, pattern, nil, true)
end
local function ArrayToDict(t, hydridMode, valueOverride, typeStrict)
local tmp = {}
if hydridMode then
for any1, any2 in t do
if type(any1) == "number" then
tmp[any2] = valueOverride or true
elseif type(any2) == "table" then
tmp[any1] = ArrayToDict(any2, hydridMode) -- any1 is Class, any2 is Name
else
tmp[any1] = any2
end
end
else
for _, key in t do
if not typeStrict or typeStrict and type(key) == typeStrict then
tmp[key] = true
end
end
end
return tmp
end
local global_container
do
local filename = "UniversalMethodFinder"
local finder
finder, global_container = loadstring(
game:HttpGet("https://raw.githubusercontent.com/luau/SomeHub/main/" .. filename .. ".luau", true),
filename
)()
finder({
-- readbinarystring = 'string.find(...,"bin",nil,true)', -- ! Could match some unwanted stuff (getbinaryindex)
-- request = 'string.find(...,"request",nil,true) and not string.find(...,"internal",nil,true)',
base64encode = 'local a={...}local b=a[1]local function c(a,b)return string.find(a,b,nil,true)end;return c(b,"encode")and(c(b,"base64")or c(string.lower(tostring(a[2])),"base64"))',
-- cloneref = 'string.find(...,"clone",nil,true) and string.find(...,"ref",nil,true)',
-- decompile = '(string.find(...,"decomp",nil,true) and string.sub(...,#...) ~= "s")',
gethiddenproperty = 'string.find(...,"get",nil,true) and string.find(...,"h",nil,true) and string.find(...,"prop",nil,true) and string.sub(...,#...) ~= "s"',
gethui = 'string.find(...,"get",nil,true) and string.find(...,"h",nil,true) and string.find(...,"ui",nil,true)',
-- getcon = 'string.find(...,"get",nil,true) and (string.find(...,"conn",nil,true) or string.find(...,"sig",nil,true)) and string.sub(...,#(...))=="s"',
getnilinstances = 'string.find(...,"nil",nil,true) and string.find(...,"get",nil,true) and string.sub(...,#...) == "s"', -- ! Could match some unwanted stuff
getscriptbytecode = 'string.find(...,"get",nil,true) and string.find(...,"bytecode",nil,true)', -- or string.find(...,"dump",nil,true) and string.find(...,"string",nil,true) due to Fluxus (dumpstring returns a function)
-- hash = 'local a={...}local b=a[1]local function c(a,b)return string.find(a,b,nil,true)end;return c(b,"hash")and c(string.lower(tostring(a[2])),"crypt")',
protectgui = 'string.find(...,"protect",nil,true) and string.find(...,"ui",nil,true) and not string.find(...,"un",nil,true)',
setthreadidentity = 'string.find(...,"identity",nil,true) and string.find(...,"set",nil,true)',
}, true, 10)
end
local identify_executor = identifyexecutor or getexecutorname or whatexecutor
local EXECUTOR_NAME = identify_executor and identify_executor() or ""
-- local cloneref = global_container.cloneref
local gethiddenproperty = global_container.gethiddenproperty
-- These should be universal enough
local appendfile = appendfile
local isfile = isfile
local readfile = readfile
local writefile = writefile
local getscriptbytecode = global_container.getscriptbytecode -- * A lot of assumptions are made based on whether this function is defined or not. So in certain edge cases, like if the executor defines "decompile" or "getscripthash" function yet doesn't define this function there might be loss of functionality of the saveinstance. Although that would be very rare and weird
local base64encode = global_container.base64encode
local service = setmetatable({}, {
__index = function(self, serviceName)
local o, s = pcall(Instance.new, serviceName)
local Service = o and s
or game:GetService(serviceName)
or settings():GetService(serviceName)
or UserSettings():GetService(serviceName)
-- if cloneref then
-- Service = cloneref(Service)
-- end
if Service then
self[serviceName] = Service
end
return Service
end,
})
local gethiddenproperty_fallback
do -- * Load Region of Déjà Vu
local UGCValidationService -- = service.UGCValidationService
gethiddenproperty_fallback = function(instance, propertyName)
if not UGCValidationService then
UGCValidationService = service.UGCValidationService
end
return UGCValidationService:GetPropertyValue(instance, propertyName) -- TODO Sadly there's no way to tell whether value is actually nil or the function just couldn't read it (always returns nil for "Class" category properties)
-- TODO `category ~= "Class"` causes WeldConstraint Part1Internal to be read as nil and not get unfiltered. Currently, there are no properties of category "Class" that match the following: NotScriptable, can be read with gethiddenproperty_fallback accurately (it always outputs nil for "Class" category, making that check useless anyway) & don't have a NotScriptableFix.
end
if gethiddenproperty then
local o, r = pcall(gethiddenproperty, workspace, "StreamOutBehavior")
if not o or r ~= nil and typeof(r) ~= "EnumItem" then -- * Tests if gethiddenproperty is broken
gethiddenproperty = nil
else
o, r = pcall(gethiddenproperty, Instance.new("AnimationRigData", Instance.new("Folder")), "parent") -- * Tests how it reacts to property overlap (shadowing) due to AnimationRigData.parent; expected BinaryString
if o and r ~= nil and type(r) ~= "string" then
gethiddenproperty = nil
end
end
end
local function benchmark(funcs, ...)
local ranking = table.create(2)
for i, f in funcs do
local start = os.clock()
for _ = 1, 50 do
f(...)
end
ranking[i] = { t = os.clock() - start, f = f }
end
table.sort(ranking, function(a, b)
return a.t < b.t
end)
return ranking[1].f
end
local test_str = string.rep("\1\0\0\0\1\2\3\4\5\6\7", 50)
do
if not bit32.byteswap or not pcall(bit32.byteswap, 1) then -- Because Fluxus is missing byteswap
bit32 = table.clone(bit32)
local function tobit(num)
num %= (bit32.bxor(num, 32))
if 0x80000000 < num then
num -= bit32.bxor(num, 32)
end
return num
end
bit32.byteswap = function(num)
local BYTE_SIZE = 8
local MAX_BYTE_VALUE = 255
num %= bit32.bxor(2, 32)
local a = bit32.band(num, MAX_BYTE_VALUE)
num = bit32.rshift(num, BYTE_SIZE)
local b = bit32.band(num, MAX_BYTE_VALUE)
num = bit32.rshift(num, BYTE_SIZE)
local c = bit32.band(num, MAX_BYTE_VALUE)
num = bit32.rshift(num, BYTE_SIZE)
local d = bit32.band(num, MAX_BYTE_VALUE)
num = tobit(bit32.lshift(bit32.lshift(bit32.lshift(a, BYTE_SIZE) + b, BYTE_SIZE) + c, BYTE_SIZE) + d)
return num
end
table.freeze(bit32)
end
-- Credits @daily3014 & @XoifaiI
local rbxcrypt_base64encode
pcall(function()
local b64_enc_buf = loadstring(
game:HttpGet(
"https://raw.githubusercontent.com/daily3014/rbx-cryptography/refs/heads/main/src/Utilities/Base64.luau",
true
),
"Base64"
)().Encode
rbxcrypt_base64encode = function(raw)
return buffer.tostring(b64_enc_buf(buffer.fromstring(raw)))
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, EncodingService_base64encode }, test_str)
end
else
base64encode = rbxcrypt_base64encode
end
assert(base64encode, "base64encode not found")
end
end
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(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. <SharedString> 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,
})
local inherited_properties = {}
local default_instances = {}
local referents, ref_size = {}, 0 -- ? Roblox encodes all <Item> 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 GetRef(instance)
local ref = referents[instance]
if not ref then
ref = ref_size
referents[instance] = ref
ref_size += 1
end
return ref
end
local function index(self, index_name)
return self[index_name]
end
local version = version
if not version then
version = function()
return "UNKNOWN"
end
end
local CLIENT_VERSION = tonumber(string.split(version(), ".")[2]) or 9e9 -- Velocity temp fix
local attr_Type_IDs = {
string = 0x02,
boolean = 0x03,
int32 = 0x04,
-- float = 0x05, -- float32
number = 0x06, -- float64 (double)
-- Array = 0x07,
-- Dictionary = 0x08,
UDim = 0x09,
UDim2 = 0x0A,
Ray = 0x0B,
Faces = 0x0C,
Axes = 0x0D,
BrickColor = 0x0E,
Color3 = 0x0F,
Vector2 = 0x10,
Vector3 = 0x11,
Vector2int16 = 0x12,
Vector3int16 = 0x13,
CFrame = 0x14,
EnumItem = 0x15,
NumberSequence = 0x17,
NumberSequenceKeypoint = 0x18,
ColorSequence = 0x19,
ColorSequenceKeypoint = 0x1A,
NumberRange = 0x1B,
Rect = 0x1C,
PhysicalProperties = 0x1D,
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,
["\0\0\128\63\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\128\191\0\0\0\0\0\0\128\63\0\0\0\0"] = 0x03,
["\0\0\128\63\0\0\0\0\0\0\0\0\0\0\0\0\0\0\128\191\0\0\0\0\0\0\0\0\0\0\0\0\0\0\128\191"] = 0x05,
["\0\0\128\63\0\0\0\0\0\0\0\128\0\0\0\0\0\0\0\0\0\0\128\63\0\0\0\0\0\0\128\191\0\0\0\0"] = 0x06,
["\0\0\0\0\0\0\128\63\0\0\0\0\0\0\128\63\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\128\191"] = 0x07,
["\0\0\0\0\0\0\0\0\0\0\128\63\0\0\128\63\0\0\0\0\0\0\0\0\0\0\0\0\0\0\128\63\0\0\0\0"] = 0x09,
["\0\0\0\0\0\0\128\191\0\0\0\0\0\0\128\63\0\0\0\0\0\0\0\128\0\0\0\0\0\0\0\0\0\0\128\63"] = 0x0a,
["\0\0\0\0\0\0\0\0\0\0\128\191\0\0\128\63\0\0\0\0\0\0\0\0\0\0\0\0\0\0\128\191\0\0\0\0"] = 0x0c,
["\0\0\0\0\0\0\128\63\0\0\0\0\0\0\0\0\0\0\0\0\0\0\128\63\0\0\128\63\0\0\0\0\0\0\0\0"] = 0x0d,
["\0\0\0\0\0\0\0\0\0\0\128\191\0\0\0\0\0\0\128\63\0\0\0\0\0\0\128\63\0\0\0\0\0\0\0\0"] = 0x0e,
["\0\0\0\0\0\0\128\191\0\0\0\0\0\0\0\0\0\0\0\0\0\0\128\191\0\0\128\63\0\0\0\0\0\0\0\0"] = 0x10,
["\0\0\0\0\0\0\0\0\0\0\128\63\0\0\0\0\0\0\128\191\0\0\0\0\0\0\128\63\0\0\0\0\0\0\0\128"] = 0x11,
["\0\0\128\191\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\191"] = 0x14,
["\0\0\128\191\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\128\63\0\0\0\0\0\0\128\63\0\0\0\128"] = 0x15,
["\0\0\128\191\0\0\0\0\0\0\0\0\0\0\0\0\0\0\128\191\0\0\0\0\0\0\0\0\0\0\0\0\0\0\128\63"] = 0x17,
["\0\0\128\191\0\0\0\0\0\0\0\128\0\0\0\0\0\0\0\0\0\0\128\191\0\0\0\0\0\0\128\191\0\0\0\128"] = 0x18,
["\0\0\0\0\0\0\128\63\0\0\0\128\0\0\128\191\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\128\63"] = 0x19,
["\0\0\0\0\0\0\0\0\0\0\128\191\0\0\128\191\0\0\0\0\0\0\0\0\0\0\0\0\0\0\128\63\0\0\0\0"] = 0x1b,
["\0\0\0\0\0\0\128\191\0\0\0\128\0\0\128\191\0\0\0\0\0\0\0\128\0\0\0\0\0\0\0\0\0\0\128\191"] = 0x1c,
["\0\0\0\0\0\0\0\0\0\0\128\63\0\0\128\191\0\0\0\0\0\0\0\0\0\0\0\0\0\0\128\191\0\0\0\0"] = 0x1e,
["\0\0\0\0\0\0\128\63\0\0\0\0\0\0\0\0\0\0\0\0\0\0\128\191\0\0\128\191\0\0\0\0\0\0\0\0"] = 0x1f,
["\0\0\0\0\0\0\0\0\0\0\128\63\0\0\0\0\0\0\128\63\0\0\0\128\0\0\128\191\0\0\0\0\0\0\0\0"] = 0x20,
["\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)
AssetRead = 2 ^ 36, --------------- 36 (0x24)
AssetManagement = 2 ^ 37, --------- 37 (0x25)
DynamicGeneration = 2 ^ 38, ------- 38 (0x26)
PlatformAvatarEditing = 2 ^ 39, --- 39 (0x27)
AssetCreateUpdate = 2 ^ 40, ------- 40 (0x28)
Capture = 2 ^ 41, ----------------- 41 (0x29)
SensitiveInput = 2 ^ 42, ---------- 42 (0x2a)
Monetization = 2 ^ 43, ------------ 43 (0x2b)
LoadOwnedAsset = 2 ^ 44, ---------- 44 (0x2c)
Social = 2 ^ 45, ------------------ 45 (0x2d)
ServerCommunication = 2 ^ 46, ----- 46 (0x2e)
Logging = 2 ^ 47, ----------------- 47 (0x2f)
PromptExternalPurchase = 2 ^ 48, -- 48 (0x30)
Groups = 2 ^ 49, ------------------ 49 (0x31)
Teleport = 2 ^ 50, ---------------- 50 (0x32)
Consequences = 2 ^ 51, ------------ 51 (0x33)
Material = 2 ^ 52, ---------------- 52 (0x34)
AvatarBehavior = 2 ^ 53, ---------- 53 (0x35)
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, ---------- 63 (0x3f) - special case for negative values (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 string.split(tostring(raw), " | ") do
local bit = CAPABILITY_BITS[flag]
if bit then
result += bit
end
end
return result
end
local function __COUNT_BITS(...) -- * Credits to Friend (you know yourself)
local Value = 0
for i, bit in { ... } do
if bit then
Value += 2 ^ (i - 1)
end
end
return Value
end
local Binary_Descriptors
Binary_Descriptors = {
__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 += size3
end
local b = buffer.create(len)
buffer.copy(b, 0, buf1)
buffer.copy(b, size1, buf2)
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 Keypoints do
keypoint_handler(keypoint, b, 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,
--------------------------------------------------------------
--------------------------------------------------------------
--------------------------------------------------------------
["string"] = function(raw)
local raw_len = #raw
local len = 4 + raw_len
local b = buffer.create(len)
buffer.writeu32(b, 0, raw_len)
buffer.writestring(b, 4, raw)
return b, len
end,
["boolean"] = function(raw)
local b = buffer.create(1)
buffer.writeu8(b, 0, raw and 1 or 0)
return b, 1
end,
["number"] = function(raw) -- double
local b = buffer.create(8)
buffer.writef64(b, 0, raw)
return b, 8
end,
["UDim"] = function(raw)
local b = buffer.create(8)
buffer.writef32(b, 0, raw.Scale)
buffer.writei32(b, 4, raw.Offset)
return b, 8
end,
["UDim2"] = function(raw)
return Binary_Descriptors.__PACK_MULTIPLE(Binary_Descriptors["UDim"], raw.X, raw.Y)
end,
["Ray"] = function(raw)
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, __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, __COUNT_BITS(raw.X, raw.Y, raw.Z))
return b, 4
end,
["BrickColor"] = function(raw)
local b = buffer.create(4)
buffer.writeu32(b, 0, raw.Number)
return b, 4
end,
["Color3"] = function(raw)
return Binary_Descriptors.__PACK_F32(raw.R, raw.G, raw.B)
end,
["Vector2"] = function(raw)
return Binary_Descriptors.__PACK_F32(raw.X, raw.Y)
end,
["Vector3"] = function(raw)
return Binary_Descriptors.__PACK_F32(raw.X, raw.Y, raw.Z)
end,
["Vector2int16"] = function(raw)
return Binary_Descriptors.__PACK_I16(raw.X, raw.Y)
end,
["Vector3int16"] = function(raw)
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)
buffer.writef32(b, 0, R00)
buffer.writef32(b, 4, R01)
buffer.writef32(b, 8, R02)
buffer.writef32(b, 12, R10)
buffer.writef32(b, 16, R11)
buffer.writef32(b, 20, R12)
buffer.writef32(b, 24, R20)
buffer.writef32(b, 28, R21)
buffer.writef32(b, 32, R22)
rotation_ID = CFrame_Rotation_IDs[buffer.tostring(b)]
end
local len = rotation_ID and 13 or 49
local b = buffer.create(len)
local __PACK_F32 = Binary_Descriptors.__PACK_F32
local position = __PACK_F32(X, Y, Z)
buffer.copy(b, 0, position)
-- buffer.writef32(b, 0, X)
-- buffer.writef32(b, 4, Y)
-- buffer.writef32(b, 8, Z)
if rotation_ID then
buffer.writeu8(b, 12, rotation_ID)
else
buffer.writeu8(b, 12, 0x0)
local xBasis = __PACK_F32(R00, R01, R02)
buffer.copy(b, 13, xBasis)
local yBasis = __PACK_F32(R10, R11, R12)
buffer.copy(b, 13 + 12, yBasis)
local zBasis = __PACK_F32(R20, R21, R22)
buffer.copy(b, 13 + 24, zBasis)
-- buffer.writef32(b, 13, R00)
-- buffer.writef32(b, 17, R01)
-- buffer.writef32(b, 21, R02)
-- buffer.writef32(b, 25, R10)
-- buffer.writef32(b, 29, R11)
-- buffer.writef32(b, 33, R12)
-- buffer.writef32(b, 37, R20)
-- buffer.writef32(b, 41, R21)
-- buffer.writef32(b, 45, R22)
end
return b, len
end,
["EnumItem"] = function(raw)
local b_Name, Name_size = Binary_Descriptors["string"](tostring(raw.EnumType))
local len = Name_size + 4
local b = buffer.create(len)
buffer.copy(b, 0, b_Name)
buffer.writeu32(b, Name_size, raw.Value)
return b, len
end,
["NumberSequence"] = nil,
["NumberSequenceKeypoint"] = function(keypoint, b, offset)
if not b then
return Binary_Descriptors.__PACK_F32(keypoint.Envelope, keypoint.Time, keypoint.Value)
end
buffer.writef32(b, offset, keypoint.Envelope)
offset += 4
buffer.writef32(b, offset, keypoint.Time)
offset += 4
buffer.writef32(b, offset, keypoint.Value)
end,
["ColorSequence"] = nil,
["ColorSequenceKeypoint"] = function(keypoint, b, offset)
local Value = Binary_Descriptors["Color3"](keypoint.Value)
if not b then
b = buffer.create(20)
offset = 0
end
buffer.writef32(b, offset, 0)
offset += 4
buffer.writef32(b, offset, keypoint.Time)
offset += 4
buffer.copy(b, offset, Value)
return b, 20
end,
["NumberRange"] = function(raw)
return Binary_Descriptors.__PACK_F32(raw.Min, raw.Max)
end,
["Rect"] = function(raw)
return Binary_Descriptors.__PACK_MULTIPLE(Binary_Descriptors["Vector2"], raw.Min, raw.Max)
end,
["PhysicalProperties"] = function(raw) -- ? Not sure yet (https://github.com/RobloxAPI/spec/blob/master/properties/drafts/AttributesSerializeFull.md#physicalproperties)
local len = 1
if raw then
len += 24
end
local b = buffer.create(len)
buffer.writeu8(b, 0, raw and 3 or 0) -- 3 means it has set CustomPhysicalProperties & has set AcousticAbsorption
if raw then
buffer.writef32(b, 1, raw.Density)
buffer.writef32(b, 5, raw.Friction)
buffer.writef32(b, 9, raw.Elasticity)
buffer.writef32(b, 13, raw.FrictionWeight)
buffer.writef32(b, 17, raw.ElasticityWeight)
buffer.writef32(b, 21, raw.AcousticAbsorption)
end
return b, len
end,
["Region3"] = function(raw)
local Translation = raw.CFrame.Position
local HalfSize = raw.Size * 0.5
return Binary_Descriptors.__PACK_MULTIPLE(
Binary_Descriptors["Vector3"],
Translation - HalfSize, -- /App/util/Region3.cpp#L38
Translation + HalfSize -- /App/util/Region3.cpp#L42
)
end,
["Region3int16"] = function(raw)
return Binary_Descriptors.__PACK_MULTIPLE(Binary_Descriptors["Vector3int16"], raw.Min, raw.Max)
end,
["Font"] = 636 < CLIENT_VERSION and function(raw)
local string__descriptor = Binary_Descriptors["string"]
local b_Family, Family_size = string__descriptor(raw.Family)
local b_CachedFaceId, CachedFaceId_size = string__descriptor("")
local len = 3 + Family_size + CachedFaceId_size
local b = buffer.create(len)
local ok_w, weight = pcall(index, raw, "Weight")
local ok_s, style = pcall(index, raw, "Style")
buffer.writeu16(b, 0, ok_w and weight.Value or 0)
buffer.writeu8(b, 2, ok_s and style.Value or 0)
buffer.copy(b, 3, b_Family)
buffer.copy(b, 3 + Family_size, b_CachedFaceId)
return b, len
end or function(raw)
local string__descriptor = Binary_Descriptors["string"]
local b_Family, Family_size = string__descriptor(raw.Family)
local b_CachedFaceId, CachedFaceId_size = string__descriptor("")
local len = 3 + Family_size + CachedFaceId_size
local b = buffer.create(len)
local FontString = tostring(raw)
local EmptyWeight = string_find(FontString, "Weight = ,")
local EmptyStyle = string_find(FontString, "Style = }")
buffer.writeu16(b, 0, EmptyWeight and 0 or raw.Weight.Value)
buffer.writeu8(b, 2, EmptyStyle and 0 or raw.Style.Value)
buffer.copy(b, 3, b_Family)
buffer.copy(b, 3 + Family_size, b_CachedFaceId)
return b, len
end,
["SecurityCapabilities"] = function(raw)
local b = buffer.create(8)
if raw == BASE_CAPABILITIES then
return b, 8
end
Binary_Descriptors.__writei64(b, 0, __COUNT_CAPABILITY_BITS(raw))
return b, 8
end,
["Path2DControlPoint"] = function(raw)
return Binary_Descriptors.__PACK_MULTIPLE(
Binary_Descriptors["UDim2"],
raw.Position,
raw.LeftTangent,
raw.RightTangent
)
end,
}
do -- Sequences
Binary_Descriptors["NumberSequence"] =
Binary_Descriptors.__construct_Sequence(Binary_Descriptors["NumberSequenceKeypoint"], 12)
Binary_Descriptors["ColorSequence"] =
Binary_Descriptors.__construct_Sequence(Binary_Descriptors["ColorSequenceKeypoint"], 20)
end
do -- Vectors
Binary_Descriptors.__PACK_F32 = Binary_Descriptors.__construct__PACKER(true)
Binary_Descriptors.__PACK_I16 = Binary_Descriptors.__construct__PACKER()
end
local ESCAPES_PATTERN = "[&<>\"'\0\1-\9\11-\12\14-\31\127-\255]" -- * The safe way is to escape all five characters in text. However, the three characters " ' and > needn't be escaped in text
-- %z (\0 aka NULL) might not be needed as Roblox automatically converts it to space everywhere it seems like
-- Characters from: https://create.roblox.com/docs/en-us/ui/rich-text#escape-forms
-- * EscapesPattern should be ordered from most common to least common characters for sake of speed
-- * Might wanna use their numerical codes instead of named codes for reduced file size (Could be an Option)
-- TODO Maybe we should invert the pattern to only allow certain characters (future-proof)
local ESCAPES = {
["&"] = "&amp;", -- 38
["<"] = "&lt;", -- 60
[">"] = "&gt;", -- 62
['"'] = "&#34;", -- quot
["'"] = "&#39;", -- apos
["\0"] = "",
}
for rangeStart, rangeEnd in string.gmatch(ESCAPES_PATTERN, "(.)%-(.)") do
for charCode = string.byte(rangeStart), string.byte(rangeEnd) do
ESCAPES[string.char(charCode)] = "&#" .. charCode .. ";"
end
end
local XML_Descriptors
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 "<![CDATA[" .. raw .. "]]>"
end,
__NORMALIZE_NUMBER = function(raw)
if raw ~= raw then
return "NAN"
elseif raw == math.huge then
return "INF"
elseif raw == -math.huge then
return "-INF"
end
return raw
end,
__NORMALIZE_RANGE = function(raw)
return raw ~= raw and "0" or raw -- Normally we should return "-nan(ind)" instead of "0" but this adds more compatibility
end,
__MINMAX = function(min, max, descriptor)
return "<min>" .. descriptor(min) .. "</min><max>" .. descriptor(max) .. "</max>"
end,
__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,
__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)
-- ? Trailing whitespace after Envelope is needed for lune compatibility
return function(raw)
local sequence = ""
for _, keypoint in raw.Keypoints do
sequence ..= keypoint_handler(keypoint)
end
return sequence
end
end,
__VECTOR = function(X, Y, Z) -- Each element is a <float>
local Value = "<X>" .. X .. "</X><Y>" .. Y .. "</Y>" -- There is no Vector without at least two Coordinates.. (Vector1, at least on Roblox)
if Z then
Value ..= "<Z>" .. Z .. "</Z>"
end
return Value
end,
--------------------------------------------------------------
--------------------------------------------------------------
--------------------------------------------------------------
Axes = function(raw)
-- The text of this element is formatted as an integer between 0 and 7
return "<axes>" .. __COUNT_BITS(raw.X, raw.Y, raw.Z) .. "</axes>"
end,
-- ! Assuming all base64 encoded strings won't have newlines
BinaryString = function(raw) -- ! only add raw == nil if such edge-case exists (note it)
return raw == "" and "" or base64encode(raw)
end,
BrickColor = function(raw)
return raw.Number -- * Roblox encodes the tags as "int", but this is not required for Roblox to properly decode the type. For better compatibility, it is preferred that third-party implementations encode and decode "BrickColor" tags instead. Could also use "int" or "Color3uint8"
end,
CFrame = function(raw)
local X, Y, Z, R00, R01, R02, R10, R11, R12, R20, R21, R22 = raw:GetComponents()
return XML_Descriptors.__VECTOR(X, Y, Z)
.. "<R00>"
.. R00
.. "</R00><R01>"
.. R01
.. "</R01><R02>"
.. R02
.. "</R02><R10>"
.. R10
.. "</R10><R11>"
.. R11
.. "</R11><R12>"
.. R12
.. "</R12><R20>"
.. R20
.. "</R20><R21>"
.. R21
.. "</R21><R22>"
.. R22
.. "</R22>",
"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>"
-- .. QX
-- .. "</QX><QY>"
-- .. QY
-- .. "</QY><QZ>"
-- .. QZ
-- .. "</QZ><QW>"
-- .. QW
-- .. "</QW>"
-- end,
Color3 = function(raw) -- Each element is a <float>
return "<R>" .. raw.R .. "</R><G>" .. raw.G .. "</G><B>" .. raw.B .. "</B>" -- ? It is recommended that Color3 is encoded with elements instead of text.
end,
Color3uint8 = function(raw)
-- https://github.com/rojo-rbx/rbx-dom/blob/master/docs/xml.md#color3uint8
-- ? It is recommended that Color3uint8 is encoded with text instead of elements.
return 0xFF000000
+ (math.floor(raw.R * 255) * 0x10000)
+ (math.floor(raw.G * 255) * 0x100)
+ math.floor(raw.B * 255)
-- return bit32.bor(
-- bit32.bor(bit32.bor(bit32.lshift(0xFF, 24), bit32.lshift(0xFF * raw.R, 16)), bit32.lshift(0xFF * raw.G, 8)),
-- 0xFF * raw.B
-- )
-- return tonumber(string.format("0xFF%02X%02X%02X",raw.R*255,raw.G*255,raw.B*255))
end,
ColorSequence = nil,
ColorSequenceKeypoint = function(keypoint)
local __NORMALIZE_RANGE = XML_Descriptors.__NORMALIZE_RANGE
local color3 = keypoint.Value
return __NORMALIZE_RANGE(keypoint.Time)
.. " "
.. __NORMALIZE_RANGE(color3.R)
.. " "
.. __NORMALIZE_RANGE(color3.G)
.. " "
.. __NORMALIZE_RANGE(color3.B)
.. " 0 "
end,
Content = function(raw) -- TODO Not sure about Object & Opaque, run tests when possible
local SourceType = raw.SourceType
return SourceType == Enum.ContentSourceType.None and "<null></null>"
or SourceType == Enum.ContentSourceType.Uri and "<uri>" .. XML_Descriptors.string(raw.Uri) .. "</uri>"
or SourceType == Enum.ContentSourceType.Object and "<Ref>" .. GetRef(raw.Object) .. "</Ref>"
or SourceType == Enum.ContentSourceType.Opaque and "<Ref>" .. GetRef(raw.Opaque) .. "</Ref>"
end,
ContentId = function(raw) -- ! only add raw == nil if such edge-case exists (note it)
return raw == "" and "<null></null>" or "<url>" .. XML_Descriptors.string(raw) .. "</url>", "Content" -- TODO Remove "Content" str once Roblox fully releases Content DataType
end,
CoordinateFrame = function(raw)
return "<CFrame>" .. XML_Descriptors.CFrame(raw) .. "</CFrame>"
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 "<faces>" .. __COUNT_BITS(raw.Right, raw.Top, raw.Back, raw.Left, raw.Bottom, raw.Front) .. "</faces>"
end,
Font = 636 < CLIENT_VERSION
and function(raw)
-- TODO (OPTIONAL ELEMENT): Figure out how to determine (ContentId) <CachedFaceId><url>rbxasset://fonts/GothamSSm-Medium.otf</url></CachedFaceId>
--[[
? game:GetService("TextService"):GetFontMemoryData()
? rbxasset://fonts/families/{Enum.Font.BuilderSans.Name}.json
]]
local ok_w, weight = pcall(index, raw, "Weight")
local ok_s, style = pcall(index, raw, "Style")
return "<Family>"
.. XML_Descriptors.ContentId(raw.Family)
.. "</Family><Weight>"
.. (ok_w and XML_Descriptors.EnumItem(weight) or "")
.. "</Weight><Style>"
.. (ok_s and style.Name or "") -- Weird but this field accepts .Name of enum instead..
.. "</Style>"
end
or function(raw)
local FontString = tostring(raw)
local EmptyWeight = string_find(FontString, "Weight = ,")
local EmptyStyle = string_find(FontString, "Style = }")
return "<Family>"
.. XML_Descriptors.ContentId(raw.Family)
.. "</Family><Weight>"
.. (EmptyWeight and "" or XML_Descriptors.EnumItem(raw.Weight))
.. "</Weight><Style>"
.. (EmptyStyle and "" or raw.Style.Name) -- Weird but this field accepts .Name of enum instead..
.. "</Style>"
end,
NetAssetRef = nil,
NumberRange = function(raw) -- tostring(raw) also works
-- The value is the text content, formatted as a space-separated list of floating point numbers.
local __NORMALIZE_RANGE = XML_Descriptors.__NORMALIZE_RANGE
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,
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 "<Position>"
-- .. udim2(raw.Position)
-- .. "</Position>"
-- .. "<LeftTangent>"
-- .. udim2(raw.LeftTangent)
-- .. "</LeftTangent>"
-- .. "<RightTangent>"
-- .. udim2(raw.RightTangent)
-- .. "</RightTangent>"
-- end,
PhysicalProperties = function(raw)
--[[
Contains at least one CustomPhysics element, which is interpreted according to the bool type. If this value is true, then the tag also contains an element for each component of the PhysicalProperties:
Density
Friction
Elasticity
FrictionWeight
ElasticityWeight
AcousticAbsorption
The value of each component is represented by the text content formatted as a 32-bit floating point number (see float)
]]
local CustomPhysics = "<CustomPhysics>" .. XML_Descriptors.bool(raw and true or false) .. "</CustomPhysics>"
return raw
and CustomPhysics .. "<Density>" .. raw.Density .. "</Density><Friction>" .. raw.Friction .. "</Friction><Elasticity>" .. raw.Elasticity .. "</Elasticity><FrictionWeight>" .. raw.FrictionWeight .. "</FrictionWeight><ElasticityWeight>" .. raw.ElasticityWeight .. "</ElasticityWeight><AcousticAbsorption>" .. raw.AcousticAbsorption .. "</AcousticAbsorption>"
or CustomPhysics
end,
-- ProtectedString = function(raw) return tostring(raw), "ProtectedString" end,
Ray = function(raw)
local vector3 = XML_Descriptors.Vector3
return "<origin>" .. vector3(raw.Origin) .. "</origin><direction>" .. vector3(raw.Direction) .. "</direction>"
end,
Rect = function(raw)
return XML_Descriptors.__MINMAX(raw.Min, raw.Max, XML_Descriptors.Vector2), "Rect2D"
end,
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
return XML_Descriptors.__MINMAX(
Translation - HalfSize, -- /App/util/Region3.cpp#L38
Translation + HalfSize, -- /App/util/Region3.cpp#L42
XML_Descriptors.Vector3
)
end,
Region3int16 = function(raw) -- ? Not sure yet (/App/v8tree/EnumProperty.cpp#L346)
return XML_Descriptors.__MINMAX(raw.Min, raw.Max, XML_Descriptors.Vector3int16)
end,
SharedString = function(raw)
return SharedStrings[XML_Descriptors.BinaryString(raw)]
end,
SecurityCapabilities = function(raw)
if raw == BASE_CAPABILITIES then
return 0
end
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 (binaryAddress)
-- TweenInfo = function(raw) -- ? Not sure
-- local __NORMALIZE_NUMBER = XML_Descriptors.__NORMALIZE_NUMBER
-- local EnumItem = XML_Descriptors.EnumItem
-- return "<Time>"
-- .. __NORMALIZE_NUMBER(raw.Time)
-- .. "</Time><DelayTime>"
-- .. __NORMALIZE_NUMBER(raw.DelayTime)
-- .. "</DelayTime><RepeatCount>"
-- .. __NORMALIZE_NUMBER(raw.RepeatCount)
-- .. "</RepeatCount><Reverses>"
-- .. XML_Descriptors.bool(raw.Reverses)
-- .. "</Reverses><EasingDirection>"
-- .. enum(raw.EasingDirection)
-- .. "</EasingDirection><EasingStyle>"
-- .. enum(raw.EasingStyle)
-- .. "</EasingStyle>"
-- end,
UDim = function(raw)
--[[
S: Represents the Scale component. Interpreted as a <float>.
O: Represents the Offset component. Interpreted as an <int>.
]]
return "<S>" .. raw.Scale .. "</S><O>" .. raw.Offset .. "</O>"
end,
UDim2 = function(raw)
--[[
XS: Represents the X.Scale component. Interpreted as a <float>.
XO: Represents the X.Offset component. Interpreted as an <int>.
YS: Represents the Y.Scale component. Interpreted as a <float>.
YO: Represents the Y.Offset component. Interpreted as an <int>.
]]
local X, Y = raw.X, raw.Y
return "<XS>"
.. X.Scale
.. "</XS><XO>"
.. X.Offset
.. "</XO><YS>"
.. Y.Scale
.. "</YS><YO>"
.. Y.Offset
.. "</YO>"
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)
--[[
X: Represents the X component. Interpreted as a <float>.
Y: Represents the Y component. Interpreted as a <float>.
]]
return XML_Descriptors.__VECTOR(raw.X, raw.Y)
end,
Vector2int16 = nil,
-- Vector2int16 = Descriptors.Vector2, -- except as <int>
Vector3 = function(raw)
--[[
X: Represents the X component. Interpreted as a <float>.
Y: Represents the Y component. Interpreted as a <float>.
Z: Represents the Z component. Interpreted as a <float>.
]]
return XML_Descriptors.__VECTOR(raw.X, raw.Y, raw.Z)
end,
Vector3int16 = nil,
-- Vector3int16 = Descriptors.Vector3, -- except as <int>\
bool = function(raw)
return raw and "true" or "false"
end,
double = nil, -- Float64
float = nil, -- Float32
int = nil, -- Int32
int64 = nil, -- Int64 (long)
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,
--------------------------------------------------------------
-----------%localappdata%/Roblox/GlobalSettings_13.xml--------
-----------------------settings().Studio----------------------
--------------------------------------------------------------
-- QDir = function(raw) -- ? Not sure
-- return raw
-- end,
-- QFont = function(raw) -- ? Not sure
-- return raw
-- end,
}
do -- Sequences
XML_Descriptors.NumberSequence = XML_Descriptors.__construct_Sequence(XML_Descriptors.NumberSequenceKeypoint)
XML_Descriptors.ColorSequence = XML_Descriptors.__construct_Sequence(XML_Descriptors.ColorSequenceKeypoint)
end
for descriptorName, redirectName in
{
NetAssetRef = "SharedString",
Vector2int16 = "Vector2",
Vector3int16 = "Vector3",
double = "__NORMALIZE_NUMBER",
float = "__NORMALIZE_NUMBER",
int = "__NORMALIZE_NUMBER",
int64 = "__NORMALIZE_NUMBER",
}
do
XML_Descriptors[descriptorName] = XML_Descriptors[redirectName]
end
if CLIENT_VERSION < math.huge then -- math.huge because unknown yet
-- ! For sake of compatibility with older clients, Roblox currently does this too but it WILL probably change in the future so keep track of that. ONCE ROBLOX MOVES AWAY FROM THIS, MAKE SURE TO KEEP SUPPORT FOR VERY OLD CLIENTS; 645 <= CLIENT Ver < ??? (ver when roblox moves away), THIS IS COMPLICATED SO PROBABLY NO SUPPORT, JUST USE OLDER VER OF USSI; NEWEST CLIENTS
XML_Descriptors.Content = XML_Descriptors.ContentId
end
local ClassList
do
local ClassPropertyExceptions = {
Whitelist = {
MeshPart = ArrayToDict({ "CollisionFidelity" }),
PartOperation = ArrayToDict({ "CollisionFidelity" }),
TriangleMeshPart = ArrayToDict({ "CollisionFidelity" }),
},
Blacklist = {
LuaSourceContainer = ArrayToDict({ "ScriptGuid" }),
Instance = ArrayToDict({ "UniqueId", "HistoryId" }),
},
}
local function AttributesSerialize(attrs)
-- * There are certain restrictions for names of attributes
-- https://create.roblox.com/docs/reference/engine/classes/Instance#SetAttribute
-- But it seems like even if those are present, Studio still opens the file just fine
-- So there is no need to check for them currently
-- ? Return early for empty tags (this proved equally as fast when done using counter/next)
local attrs_n = 0
local buffer_size = 4
local attrs_sorted = {}
local attrs_formatted = table.clone(attrs)
for attr, val in attrs do
attrs_n += 1
attrs_sorted[attrs_n] = attr
local Type = typeof(val)
local Descriptor = Binary_Descriptors[Type]
local attr_size
attrs_formatted[attr], attr_size = Descriptor(val)
buffer_size += 5 + #attr + attr_size
end
table.sort(attrs_sorted)
local b = buffer.create(buffer_size)
buffer.writeu32(b, 0, attrs_n)
local string__descriptor = Binary_Descriptors["string"]
local offset = 4
for _, attr in attrs_sorted do
local b_Name, Name_size = string__descriptor(attr)
buffer.copy(b, offset, b_Name)
offset += Name_size
buffer.writeu8(b, offset, attr_Type_IDs[typeof(attrs[attr])])
offset += 1
local bb = attrs_formatted[attr]
buffer.copy(b, offset, bb)
offset += buffer.len(bb)
end
return buffer.tostring(b)
end
local function AttenuationSerialize(attenuations)
if not next(attenuations) then
return "\0" -- ? [CONSTANT] Version byte (likely)
end
local attenuations_n = 0
local attenuations_sorted = {}
for key in attenuations do
attenuations_n += 1
attenuations_sorted[attenuations_n] = key
end
table.sort(attenuations_sorted) -- ? Seems to be sorted by default, just in case
local b = buffer.create(1 + attenuations_n * 8)
local offset = 1
for _, key in attenuations_sorted do
buffer.writef32(b, offset, key)
offset += 4
buffer.writef32(b, offset, attenuations[key]) -- volume
offset += 4
end
return buffer.tostring(b)
end
local function TransformsSerialize(transforms)
local transforms_n = #transforms
if transforms_n == 0 then
return "\1\0\0\0\0\0\0\0"
end
local b = buffer.create(8 + transforms_n * 48)
buffer.writeu32(b, 0, 1) -- ? [CONSTANT] Version byte (likely)
buffer.writeu32(b, 4, transforms_n)
local __PACK_F32 = Binary_Descriptors.__PACK_F32
local offset = 8
for _, transform in transforms do
local X, Y, Z, R00, R01, R02, R10, R11, R12, R20, R21, R22 = transform:GetComponents()
local xBasis = __PACK_F32(R00, R01, R02)
buffer.copy(b, offset, xBasis)
offset += 12
local yBasis = __PACK_F32(R10, R11, R12)
buffer.copy(b, offset, yBasis)
offset += 12
local zBasis = __PACK_F32(R20, R21, R22)
buffer.copy(b, offset, zBasis)
offset += 12
local position = __PACK_F32(X, Y, Z)
buffer.copy(b, offset, position)
offset += 12
end
return buffer.tostring(b)
end
local NotScriptableFixes = { --[[
For more info:
- https://github.com/luau/UniversalSynSaveInstance/blob/main/Tools/NotScriptable-Related/Potentially%20Missing%20Properties%20Dumper/Potentially%20Missing%20Properties%20Dumper.luau
- https://github.com/luau/UniversalSynSaveInstance/blob/main/Tools/NotScriptable-Related/NotScriptable%20Dumper/NotScriptable%20Dumper.py
]]
Instance = {
AttributesSerialize = function(instance)
local attrs = instance:GetAttributes()
if not next(attrs) then
return ""
end
return AttributesSerialize(attrs)
end,
DefinesCapabilities = "Sandboxed",
Tags = function(instance)
-- https://github.com/RobloxAPI/spec/blob/master/properties/Tags.md
local tags = service.CollectionService:GetTags(instance) -- ? Seems faster than instance:GetTags
if #tags == 0 then
return ""
end
return table.concat(tags, "\0")
end,
},
Path2D = {
PropertiesSerialize = function(instance)
local control_points = instance:GetControlPoints()
local control_points_n = #control_points
if control_points_n == 0 then
return "\0\0\0\0"
end
local b = buffer.create(4 + control_points_n * 49)
buffer.writeu32(b, 0, control_points_n)
local TypeID_Path2DControlPoint = attr_Type_IDs["Path2DControlPoint"]
local Path2DControlPoint_descriptor = Binary_Descriptors["Path2DControlPoint"]
local offset = 4
for i, point in control_points do
local buf = Path2DControlPoint_descriptor(point)
buffer.writeu8(b, offset, TypeID_Path2DControlPoint)
offset += 1
buffer.copy(b, offset, buf)
offset += 48
end
return buffer.tostring(b)
end,
},
PlayerEmulatorService = {
SerializedEmulatedPolicyInfo = function(instance)
local EmulatedPolicyInfo = instance:GetEmulatedPolicyInfo()
if not next(EmulatedPolicyInfo) then
return ""
end
return AttributesSerialize(EmulatedPolicyInfo)
end,
},
StyleRule = {
PropertiesSerialize = function(instance)
local props = instance:GetProperties()
if not next(props) then
return "\0\0\0\0" -- ! Essential
end
return AttributesSerialize(props)
end,
},
StyleQuery = {
ConditionsSerialize = function(instance)
local props = instance:GetConditions()
if not next(props) then
return "\0\0\0\0"
end
return AttributesSerialize(props)
end,
},
MarkerCurve = {
ValuesAndTimes = function(instance)
local markers = instance:GetMarkers()
local markers_n = #markers
if markers_n == 0 then
-- return "" -- Seems to also work
return "\2\0\0\0\0\0\0\0\1\0\0\0\0\0\0\0"
end
local strings_size = 0
for i, marker in markers do
strings_size += #marker.Value + 1
end
local b = buffer.create(8 + strings_size + 8 + (markers_n * 4))
-- Values section
buffer.writeu32(b, 0, 2) -- Constant
buffer.writeu32(b, 4, markers_n)
local offset = 8
for i, marker in markers do
local value = marker.Value
buffer.writestring(b, offset, value)
offset += #value + 1
-- buffer.writeu8(b, offset, 0) -- Null terminator
-- offset += 1
end
-- Times section (as 2400x scaled integers)
buffer.writeu32(b, offset, 1) -- Constant
offset += 4
buffer.writeu32(b, offset, markers_n)
offset += 4
for i, marker in markers do
local scaled_time = math.round(marker.Time * 2400)
buffer.writeu32(b, offset, scaled_time)
offset += 4
end
return buffer.tostring(b)
end,
},
AnimationNodeDefinition = {
InputPinData = function(instance)
local input_pins = instance:GetInputPins()
local input_pins_n = #input_pins
if input_pins_n == 0 then
return "\1\0\0\0\0\0\0\0"
end
local buffer_size = 8
for _, pin in input_pins do
buffer_size += 4 + #pin
end
local b = buffer.create(buffer_size)
buffer.writeu32(b, 0, 1) -- ? [CONSTANT] Version byte (likely)
buffer.writeu32(b, 4, input_pins_n)
local string__descriptor = Binary_Descriptors["string"]
local offset = 8
for _, pin in input_pins do
local b_pin, pin_size = string__descriptor(pin)
buffer.copy(b, offset, b_pin)
offset += pin_size
end
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
local labels_n = #labels
if labels_n == 0 then
return "\1\0\0\0\0\0\0\0"
end
local b = buffer.create(8 + labels_n * 4)
buffer.writeu32(b, 0, 1) -- ? [CONSTANT] Version byte (likely)
buffer.writeu32(b, 4, labels_n)
local offset = 8
for _, label in labels do
buffer.writeu32(b, offset, label)
offset += 4
end
return buffer.tostring(b)
end,
name = function(instance)
local names = instance:GetNames() -- RobloxScriptSecurity
local names_n = #names
if names_n == 0 then
return "\1\0\0\0\0\0\0\0"
end
local buffer_size = 8
for _, name in names do
buffer_size += 4 + #name
end
local b = buffer.create(buffer_size)
buffer.writeu32(b, 0, 1) -- ? [CONSTANT] Version byte (likely)
buffer.writeu32(b, 4, names_n)
local offset = 8
for _, name in names do
buffer.writeu32(b, offset, #name)
offset += 4
end
for _, name in names do
buffer.writestring(b, offset, name)
offset += #name
end
return buffer.tostring(b)
end,
parent = function(instance)
local parents = instance:GetParents() -- RobloxScriptSecurity
local parents_n = #parents
if parents_n == 0 then
return "\1\0\0\0\0\0\0\0"
end
local b = buffer.create(8 + #parents * 2)
buffer.writeu32(b, 0, 1) -- ? [CONSTANT] Version byte (likely)
buffer.writeu32(b, 4, parents_n)
local offset = 8
for _, parent in parents do
buffer.writeu16(b, offset, parent) -- ? likely u8 with \0 (Null) terminators but this is safer in case they overflow beyond 255 values
offset += 2
end
return buffer.tostring(b)
end,
postTransform = function(instance)
return TransformsSerialize(instance:GetPostTransforms()) -- RobloxScriptSecurity
end,
preTransform = function(instance)
return TransformsSerialize(instance:GetPreTransforms()) -- RobloxScriptSecurity
end,
transform = function(instance)
return TransformsSerialize(instance:GetTransforms()) -- RobloxScriptSecurity
end,
},
AudioDeviceInput = {
AccessList = function(instance) -- CanSave & CanLoad false
local userid_accesslist = instance:GetUserIdAccessList()
local uid_n = #userid_accesslist
if uid_n == 0 then
return ""
end
local b = buffer.create(uid_n * 8)
local __writei64 = Binary_Descriptors.__writei64
local offset = 0
for _, user_id in userid_accesslist do
__writei64(b, offset, user_id)
offset += 8
end
return buffer.tostring(b)
end,
},
AudioEmitter = {
AngleAttenuation = function(instance)
return AttenuationSerialize(instance:GetAngleAttenuation())
end,
DistanceAttenuation = function(instance)
return AttenuationSerialize(instance:GetDistanceAttenuation())
end,
},
AudioListener = {
AngleAttenuation = function(instance)
return AttenuationSerialize(instance:GetAngleAttenuation())
end,
DistanceAttenuation = function(instance)
return AttenuationSerialize(instance:GetDistanceAttenuation())
end,
},
DebuggerBreakpoint = { line = "Line" }, -- ? This shouldn't appear in live games (try to prove this wrong)
BallSocketConstraint = { MaxFrictionTorqueXml = "MaxFrictionTorque" },
BasePart = {
Color3uint8 = "Color",
MaterialVariantSerialized = "MaterialVariant",
size = "Size",
},
DoubleConstrainedValue = { value = "Value" },
IntConstrainedValue = { value = "Value" },
-- CustomEvent = {PersistedCurrentValue=function(instance) -- * Class is Deprecated and :SetValue doesn't seem to affect GetCurrentValue anymore
-- local Receiver = instance:GetAttachedReceivers()[1]
-- if Receiver then
-- return Receiver:GetCurrentValue()
-- else
-- error("No Receiver", 2)
-- end
-- end},
Terrain = {
AcquisitionMethod = "LastUsedModificationMethod", -- ? Not sure, RobloxScriptSecurity
MaterialColors = function(instance) -- https://github.com/RobloxAPI/spec/blob/master/properties/MaterialColors.md
local TERRAIN_MATERIAL_COLORS =
{ --https://github.com/rojo-rbx/rbx-dom/blob/master/rbx_dom_lua/src/customProperties.lua#L5
Enum.Material.Grass,
Enum.Material.Slate,
Enum.Material.Concrete,
Enum.Material.Brick,
Enum.Material.Sand,
Enum.Material.WoodPlanks,
Enum.Material.Rock,
Enum.Material.Glacier,
Enum.Material.Snow,
Enum.Material.Sandstone,
Enum.Material.Mud,
Enum.Material.Basalt,
Enum.Material.Ground,
Enum.Material.CrackedLava,
Enum.Material.Asphalt,
Enum.Material.Cobblestone,
Enum.Material.Ice,
Enum.Material.LeafyGrass,
Enum.Material.Salt,
Enum.Material.Limestone,
Enum.Material.Pavement,
}
local b = buffer.create(69) -- 69 bytes: 6 reserved + 63 for colors (21 materials * 3 components)
local offset = 6 -- 6 reserved bytes
local RGB_components = { "R", "G", "B" }
for _, material in TERRAIN_MATERIAL_COLORS do
local color = instance:GetMaterialColor(material)
for _, component in RGB_components do
buffer.writeu8(b, offset, math.floor(color[component] * 255)) -- ? math.floor seems unneeded but it makes it faster
offset += 1
end
end
return buffer.tostring(b)
end,
},
TriangleMeshPart = {
FluidFidelityInternal = "FluidFidelity",
},
MeshPart = { InitialSize = "MeshSize" },
PartOperation = { InitialSize = "MeshSize" },
Part = { shape = "Shape" },
TrussPart = { style = "Style" },
FormFactorPart = {
formFactorRaw = "FormFactor",
},
Fire = { heat_xml = "Heat", size_xml = "Size" },
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 = ""
for name, ids in instance:GetEmotes() do
emotes_data ..= name .. "^" .. table.concat(ids, "^") .. "^\\"
end
return emotes_data
end,
EquippedEmotesDataInternal = function(instance)
local equipped_emotes_data = ""
for _, emote in instance:GetEquippedEmotes() do
equipped_emotes_data ..= emote.Slot .. "^" .. emote.Name .. "\\"
end
return equipped_emotes_data
end,
},
LocalizationTable = {
Contents = function(instance)
return instance:GetContents() --service.HttpService:JSONEncode(instance:GetEntries())
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,
WorldPivotData = "WorldPivot", -- TODO This doesn't accurately represent whether optional type property is present or not (it's never nil), gethiddenproperty or gethiddenproperty_fallback is preferred
},
PackageLink = { PackageIdSerialize = "PackageId", VersionIdSerialize = "VersionNumber" },
Players = { MaxPlayersInternal = "MaxPlayers", PreferredPlayersInternal = "PreferredPlayers" }, -- ? Only needed for execs that lack LocalUserSecurity (Level 2, 5, 9), even so, it's a pretty useless information as it can be viewed elsewhere
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 = {
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)
return __COUNT_BITS(instance.Enabled, instance.Active)
end,
},
Workspace = {
-- SignalBehavior2 = "SignalBehavior", -- * Both are NotScriptable so it doesn't make sense to keep
CollisionGroupData = function()
local collision_groups = game:GetService("PhysicsService"):GetRegisteredCollisionGroups()
local col_groups_n = #collision_groups
if col_groups_n == 0 then
return "\1\0"
end
local buffer_size = 2 -- Initial size
for _, group in collision_groups do
buffer_size += 7 + #group.name
end
local b = buffer.create(buffer_size)
buffer.writeu8(b, 0, 1) -- ? [CONSTANT] Version byte (likely)
buffer.writeu8(b, 1, col_groups_n) -- Group count
local TypeID_int32 = attr_Type_IDs["int32"]
local offset = 2
for i, group in collision_groups do
local name, id, mask = group.name, i - 1, group.mask
local name_len = #name
buffer.writeu8(b, offset, id) -- ID
offset += 1
buffer.writeu8(b, offset, TypeID_int32) -- ? Type ID for int32 (0x04)
offset += 1
buffer.writei32(b, offset, mask) -- Mask value as signed 32-bit integer
offset += 4
buffer.writeu8(b, offset, name_len) -- Name length
offset += 1
buffer.writestring(b, offset, name) -- Name
offset += name_len
end
return buffer.tostring(b)
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
local API_Dump
local ok, err = pcall(function()
if EXECUTOR_NAME == "Velocity" then -- TODO Temp fix as it crashes on HttpGet for sites that return 403 code
return
end
local CLIENT_VERSION_str = tostring(CLIENT_VERSION)
local ok, result = pcall(readfile, CLIENT_VERSION_str)
if
ok
and result
and result ~= ""
and pcall(service.HttpService.JSONDecode, service.HttpService, result)
then
API_Dump = result
return
end
local matching_versions, is_matched = {}
-- * https://setup.rbxcdn.com/versionQTStudio seems to be a bit behind DeployHistory.txt
local DeployHistory = string.split(game:HttpGet("https://setup.rbxcdn.com/DeployHistory.txt", true), "\n")
for i = #DeployHistory, 1, -1 do
local line = DeployHistory[i]
local file_version = string.match(line, "file version: ([%d, ]+)")
if file_version then
if string.split(file_version, ", ")[2] == CLIENT_VERSION_str then
is_matched = true
local version_hash = string.match(line, "(version%-[^%s]+)")
if version_hash then
matching_versions[version_hash] = true
end
elseif is_matched then
break
end
end
end
for version_hash in matching_versions do
ok, result = pcall(
game.HttpGet,
game,
"https://setup.rbxcdn.com/" .. version_hash .. "-Full-API-Dump.json",
true
)
if ok then
local o, r = pcall(service.HttpService.JSONDecode, service.HttpService, result)
if o then
API_Dump = service.HttpService:JSONEncode(r.Classes) -- minify it
break
end
end
end
if writefile then
writefile(CLIENT_VERSION_str, API_Dump)
end
end)
if not ok or not API_Dump then
warn("[DEBUG] Failed to get " .. version() .. " version API Dump, trying latest..")
warn("[DEBUG]", err)
API_Dump = service.HttpService:JSONEncode(
service.HttpService:JSONDecode(
game:HttpGet(
"https://raw.githubusercontent.com/MaximumADHD/Roblox-Client-Tracker/roblox/Mini-API-Dump.json",
true
)
).Classes
)
end
local classList = {}
local tmp_classDict = {}
local ClassesWhitelist, ClassesBlacklist = ClassPropertyExceptions.Whitelist, ClassPropertyExceptions.Blacklist
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,
Superclass = API_Class.Superclass,
-- Tags = {},
NotCreatable = nil,
}
local ClassName = API_Class.Name
local ClassTags = API_Class.Tags
if ClassTags then
local Tags = ArrayToDict(ClassTags, nil, nil, "string")
-- Class.Tags = Tags -- or {}
Class.NotCreatable = Tags.NotCreatable
Class.Service = Tags.Service
end
local NotScriptableFixClass = NotScriptableFixes[ClassName]
-- ? Check 96ea8b2a755e55a78aedb55a7de7e83980e11077 commit - If a NotScriptableFix is needed that relies on another NotScriptable Property (which doesn't really make sense in the first place)
local ClassWhitelist, ClassBlacklist = ClassesWhitelist[ClassName], ClassesBlacklist[ClassName]
for _, Member in API_Class.Members do
-- ? print(game:GetService("ReflectionService"):GetPropertyNames("TextBox"))
if Member.MemberType == "Property" then
local Serialization = Member.Serialization
if Serialization.CanLoad then -- If Roblox doesn't save it why should we; If Roblox doesn't load it we don't need to save it
--[[
-- ! CanSave replaces "Tags.Deprecated" check because there are some old properties which are deprecated yet have CanSave.
Example: Humanoid.Health is CanSave false due to Humanoid.Health_XML being CanSave true (obsolete properties basically) - in this case both of them will Load. (aka PropertyPatches)
CanSave being on same level as CanLoad also fixes potential issues with overlapping properties like Color, Color3 & Color3uint8 of BasePart, out of which only Color3uint8 should save
This also fixes everything in IgnoreClassProperties automatically without need to hardcode :)
A very simple fix for many problems that saveinstance scripts encounter!
--]]
local PropertyName = Member.Name
if
(Serialization.CanSave or ClassWhitelist and ClassWhitelist[PropertyName])
and not (ClassBlacklist and ClassBlacklist[PropertyName])
then
local MemberTags = Member.Tags
local ValueType = Member.ValueType
local ValueType_Name = ValueType.Name
if 645 <= CLIENT_VERSION and ValueType_Name == "Content" then -- TODO: Remove after Roblox adds a descriptor for it
continue
end
local Special, PreferredDescriptorName
if MemberTags then
for _, tag in MemberTags do
if type(tag) == "table" then
PreferredDescriptorName = tag.PreferredDescriptorName
if PreferredDescriptorName and Special then
break
end
elseif tag == "NotScriptable" then
Special = true
if PreferredDescriptorName then
break
end
end
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,
Category = ValueType.Category,
-- Default = Member.Default,
-- Tags = MemberTags,
ValueType = ValueType_Name,
Special = Special,
CanRead = nil,
}
if string.sub(ValueType_Name, 1, 8) == "Optional" then
-- Extract the string after "Optional"
Property.Optional = string.sub(ValueType_Name, 9)
end
local NotScriptableFix = NotScriptableFixClass and NotScriptableFixClass[PropertyName]
local accessFunc = PreferredDescriptorName
and (
preferredDescriptorProp.MemberType == "Property"
and function(instance)
return instance[PreferredDescriptorName]
end
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
return instance[NotScriptableFix]
end or function(instance)
return instance[NotScriptableFix]
end)
or accessFunc
ClassProperties[ClassProperties_size] = Property
ClassProperties_size += 1
-- end
end
end
end
end
classList[ClassName] = Class
end
return classList
end
local ok, result = pcall(FetchAPI)
if ok then
ClassList = result
else
warn("Failed to load the API Dump")
warn(result)
return
end
end
local GLOBAL_ENV = getgenv and getgenv() or _G or shared
--[=[
@class SynSaveInstance
Represents the options for saving instances with custom settings using the synsaveinstance function.
]=]
--- @interface CustomOptions table
--- * Structure of the main CustomOptions table.
--- * Note: Options are case-insensitive, meaning you could type `NilInstances` option as `nilInStaNces` and it would still be valid.
--- @within SynSaveInstance
--- @field __DEBUG_MODE boolean -- This will print debug logs to console about unusual scenarios. Recommended to enable if you wish to help us improve our products and find bugs / issues with it! ___Default:___ false
--- @field ReadMe boolean --___Default:___ true
--- @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
--- .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 function -- If set, the serialized data will be sent to the callback function instead of to file. ___Default:___ false
--- @field mode string -- Valid modes: full, optimized, scripts. Change this to invalid mode like "invalid" if you only want ExtraInstances. "optimized" mode is **NOT** supported with *@Object* option. ___Default:___ `"optimized"`
--- @field noscripts boolean -- ___Aliases:___ `Decompile`. ___Default:___ false
--- @field scriptcache boolean -- ___Default:___ true
--- @field decomptype string -- * Deprecated. ___Default:___ Uses your executor's decompiler, if available.
--- @field timeout number -- If the decompilation run time exceeds this value it gets cancelled. Set to -1 to disable timeout (unreliable). ***Aliases***: `DecompileTimeout`. ___Default:___ 10
--- @field DecompileJobless boolean -- Includes already decompiled code in the output. No new scripts are decompiled. ___Default:___ false
--- @field SaveBytecode boolean -- Includes bytecode in the output. Useful if you wish to be able to decompile it yourself later. ___Default:___ false
--- .DecompileIgnore {Instance | Instance.ClassName | [Instance.ClassName] = {Instance.Name}} -- * Ignores match & it's descendants by default. To Ignore only the instance itself set the value to `= false`. Examples: "Chat", - Matches any instance with "Chat" ClassName, Players = {"MyPlayerName"} - Matches "Players" Class AND "MyPlayerName" Name ONLY, `workspace` - matches Instance by reference, `[workspace] = false` - matches Instance by reference and only ignores the instance itself and not it's descendants. ___Default:___ {TextChatService}
--- .IgnoreList {Instance | Instance.ClassName | [Instance.ClassName] = {Instance.Name}} -- Structure is similar to **@DecompileIgnore** except `= false` meaning if you ignore one instance it will automatically ignore it's descendants. ___Default:___ {CoreGui, CorePackages}
--- .ExtraInstances {Instance} -- If used with any invalid mode (like "invalidmode") it will only save these instances. ___Default:___ {}
--- @field IgnoreProperties table -- Ignores properties by Name. ___Default:___ {}
--- @field SaveCacheInterval number -- The less the value the more often it saves, but that would mean less performance due to constantly saving. ___Default:___ 0x1600 * 10
--- @field FilePath string -- Must only contain the name of the file, no file extension. ___Default:___ false
--- @field AvoidFileOverwrite boolean -- Prevents writing to place file that already exists. ___Default:___ true
--- @field Object Instance -- * If provided, saves as .rbxmx (Model file) instead. If Object is game, it will be saved as a .rbxl file. **MUST BE AN INSTANCE REFERENCE, FOR EXAMPLE - *game.Workspace***. `"optimized"` mode is **NOT** supported with this option. If IsModel is set to false then Object specified here will be saved as a place file. ___Default:___ false
--- @field IsModel boolean -- If Object is specified then sets to true automatically, unless you set it to false. ___Default:___ false
--- @field NilInstances boolean -- Save instances that aren't Parented (Parented to nil). ___Default:___ false
--- .NilInstancesFixes {[Instance.ClassName] = function} -- * This can cause some Classes to be fixed even though they might not need the fix (better be safe than sorry though). For example, Bones inherit from Attachment if we dont define them in the NilInstancesFixes then this will catch them anyways. **TO AVOID THIS BEHAVIOR USE THIS EXAMPLE:** {ClassName_That_Doesnt_Need_Fix = false}. ___Default:___ {Animator = function, AdPortal = function, BaseWrap = function, Attachment = function}
--- @field IgnoreDefaultProperties boolean -- Ignores default properties during saving. ___Default:___ true
--- @field IgnoreNotArchivable boolean -- Ignores the Archivable property and saves Non-Archivable instances. ___Default:___ true
--- @field IgnorePropertiesOfNotScriptsOnScriptsMode boolean -- Ignores property of every instance that is not a script in "scripts" mode. ___Default:___ false
--- @field IgnoreSpecialProperties boolean -- Prevents calls to `gethiddenproperty` and uses fallback methods instead. This also helps with crashes. If your file is corrupted after saving, you can try turning this on. ___Default:___ false
--- @field IsolateLocalPlayer boolean -- Saves Children of LocalPlayer as separate folder and prevents any instance of ClassName Player with .Name identical to LocalPlayer.Name from saving. ___Default:___ false
--- @field IsolateStarterPlayer boolean -- If enabled, StarterPlayer will be cleared and the saved starter player will be placed into folders. ___Default:___ false
--- @field IsolateLocalPlayerCharacter boolean -- Saves Children of LocalPlayer.Character as separate folder and prevents any instance of ClassName Player with .Name identical to LocalPlayer.Name from saving. ___Default:___ false
--- @field RemovePlayerCharacters boolean -- Ignore player characters while saving. (Enables SaveNotCreatable automatically). ___Default:___ true
--- @field SaveNotCreatable boolean -- * Includes non-serializable instances as Folder objects (Name is misleading as this is mostly a fix for certain NilInstances and isn't always related to NotCreatable). ___Default:___ false
--- .NotCreatableFixes table<Instance.ClassName> -- * {"Player"} is the same as {Player = "Folder"}; Format like {SpawnLocation = "Part"} is only to be used when SpawnLocation inherits from "Part" AND "Part" is Creatable. ___Default:___ { "", "Player", "PlayerScripts", "PlayerGui", "TouchTransmitter" }
--- @field IsolatePlayers boolean -- * This option does save players, it's just they won't show up in Studio and can only be viewed through the place file code (in text editor). More info at https://github.com/luau/UniversalSynSaveInstance/issues/2. ___Default:___ false
--- @field AlternativeWritefile boolean -- * Splits file content string into segments and writes them using appendfile. This might help with crashes when it starts writing to file. Though there is a risk of appendfile working incorrectly on some executors. ___Default:___ true
--- @field IgnoreDefaultPlayerScripts boolean -- * **RISKY: Ignores Default PlayerScripts like PlayerModule & RbxCharacterSounds. Prevents crashes on certain Executors. ___Default:___ true
--- @field IgnoreSharedStrings boolean -- * **RISKY: FIXES CRASHES (TEMPORARY, TESTED ON ROEXEC ONLY). FEEL FREE TO DISABLE THIS TO SEE IF IT WORKS FOR YOU**. ___Default:___ true
--- @field SharedStringOverwrite boolean -- * **RISKY:** if the process is not finished aka crashed then none of the affected values will be available. 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, only works on certain types which are all base64 encoded so far). Reason: Allows for potential smaller file size (can also be bigger in some cases). ___Default:___ false
--- @field TreatUnionsAsParts boolean -- * **RISKY:** Converts all UnionOperations to Parts. Useful if your Executor isn't able to save (read) Unions, because otherwise they will be invisible. ___Default:___ false (except Solara)
--- @interface OptionsAliases
--- @within SynSaveInstance
--- Aliases for the [SynSaveInstance.CustomOptions table]. Format: {Option -- Alias1, Alias2}
--- @field FilePath string -- FileName
--- @field IgnoreDefaultProperties string -- IgnoreDefaultProps
--- @field SaveNotCreatable string -- SaveNonCreatable
--- @field InstancesBlacklist string -- IgnoreList
--- @field IsolatePlayerGui string -- IsolateLocalPlayer
--- @field IsolatePlayers string -- SavePlayers
--- @field scriptcache string -- DecompileJobless
--- @field timeout string -- DecompileTimeout
--- @field IgnoreNotArchivable string -- IgnoreArchivable
--- @field RemovePlayerCharacters string -- INVERSE SavePlayerCharacters
--[=[
@function saveinstance
Saves instances with specified options. Example:
```lua
local Params = {
RepoURL = "https://raw.githubusercontent.com/luau/UniversalSynSaveInstance/main/",
SSI = "saveinstance",
}
local synsaveinstance = loadstring(game:HttpGet(Params.RepoURL .. Params.SSI .. ".luau", true), Params.SSI)()
local CustomOptions = { SafeMode = true, timeout = 15, SaveBytecode = true }
synsaveinstance(CustomOptions)
```
@within SynSaveInstance
@yields
@param Parameter_1 variant<table, table<Instance>> -- Can either be [SynSaveInstance.CustomOptions table] or a filled with instances ({Instance}), (then it will be treated as ExtraInstances with an invalid mode and IsModel will be true).
@param Parameter_2 table -- [OPTIONAL] If present, then Parameter_2 will be assumed to be [SynSaveInstance.CustomOptions table]. And then if the Parameter_1 is an Instance, then it will be assumed to be [SynSaveInstance.CustomOptions table].Object. If Parameter_1 is a table filled with instances ({Instance}), then it will be assumed to be [SynSaveInstance.CustomOptions table].ExtraInstances and IsModel will be true). This exists for sake compatibility with `saveinstance(game, {})`
]=]
local function synsaveinstance(CustomOptions, CustomOptions2)
if GLOBAL_ENV.USSI then
return
end
GLOBAL_ENV.USSI = true
do
local setthreadidentity = global_container.setthreadidentity
if setthreadidentity then
pcall(setthreadidentity, 8) -- ? Arceus X Fix
end
end
local currentstr, currentsize, totalsize, chunks = "", 0, 0, table.create(1)
local savebuffer, savebuffer_size = {}, 1
local header =
'<!-- Saved by UniversalSynSaveInstance (Join to Copy Games) https://discord.gg/wx4ThpAsmw --><roblox version="4">'
local StatusText
local OPTIONS = {
mode = "optimized",
noscripts = false,
scriptcache = true,
-- decomptype = "",
timeout = 10,
-- * New:
__DEBUG_MODE = false,
-- Binary = false, -- true in syn newer versions (false in our case because no binary support yet), Description: Saves everything in Binary Mode (rbxl/rbxm).
Callback = false,
--Clipboard/CopyToClipboard = false, -- Description: If set to true, the serialized data will be set to the clipboard, which can be later pasted into studio easily. Useful for saving models. (Binary Only)
-- MaxThreads = 3 -- Description: The number of decompilation threads that can run at once. More threads means it can decompile for scripts at a time.
-- DisableCompression = false, --Description: Disables compression in the binary output
DecompileJobless = false,
DecompileIgnore = { -- * Clean these up (merged Old Syn and New Syn)
-- "Chat",
"TextChatService",
ModuleScript = nil,
},
IgnoreDefaultPlayerScripts = true,
SaveBytecode = false,
IgnoreProperties = {},
IgnoreList = { "CoreGui", "CorePackages" },
ExtraInstances = {},
NilInstances = false,
NilInstancesFixes = {},
SaveCacheInterval = 0x1600 * 10,
ShowStatus = true,
SafeMode = false,
ShutdownWhenDone = false,
AntiIdle = true,
Anonymous = false,
ReadMe = true,
FilePath = false,
AvoidFileOverwrite = true,
Object = false,
IsModel = false,
IgnoreDefaultProperties = true,
IgnoreNotArchivable = true,
IgnorePropertiesOfNotScriptsOnScriptsMode = false,
IgnoreSpecialProperties = ArrayToDict({ "Fluxus", "Delta", "Solara" })[EXECUTOR_NAME] or false, -- ! Please submit more Executors that crash on gethiddenproperty (with this disabled basically)
IsolateLocalPlayer = false, -- #service.StarterGui:GetChildren() == 0
IsolateLocalPlayerCharacter = false,
IsolatePlayers = false,
IsolateStarterPlayer = false,
RemovePlayerCharacters = true,
SaveNotCreatable = false,
NotCreatableFixes = {
-- "InputObject",
-- "LodDataEntity",
-- "Path",
"", -- * FilteredSelection
"AdvancedDragger",
"AnimationTrack",
"Dragger",
"Player",
"PlayerGui",
"PlayerMouse",
"PlayerMouse",
"PlayerScripts",
"ScreenshotHud",
"StudioData",
"TextChatMessage",
"TextSource",
"TouchTransmitter",
"Translator",
CloudLocalizationTable = "LocalizationTable",
Platform = "Part",
Status = "Model", -- gets created by studio automatically usually so there will be duplicates due to this
-- CoreScript = "Script", -- useless
-- ChatWindowMessageProperties = "TextChatMessageProperties", -- ? Not needed as this can be derived with ChatWindowConfiguration.DeriveNewMessageProperties
},
-- ! Risky
IgnoreSharedStrings = EXECUTOR_NAME ~= "Wave",
SharedStringOverwrite = false,
TreatUnionsAsParts = EXECUTOR_NAME == "Solara", -- TODO Temporary true (once removed, remove Note from docs too)
AlternativeWritefile = not ArrayToDict({ "WRD", "Xeno", "Zorara" })[EXECUTOR_NAME],
OptionsAliases = { -- You can't really modify these as a user
DecompileTimeout = "timeout",
FileName = "FilePath",
IgnoreArchivable = "IgnoreNotArchivable",
IgnoreDefaultProps = "IgnoreDefaultProperties",
InstancesBlacklist = "IgnoreList",
IsolatePlayerGui = "IsolateLocalPlayer",
SaveNonCreatable = "SaveNotCreatable",
SavePlayers = "IsolatePlayers",
},
}
local OPTIONS_lowercase, CustomOptions_valid = {}, {}
for option_name in OPTIONS do
local option_name_lowercase = string.lower(option_name)
if OPTIONS_lowercase[option_name_lowercase] then
warn("DUPLICATE OPTION", option_name)
else
OPTIONS_lowercase[option_name_lowercase] = option_name
end
end
for option_alias, option_name in OPTIONS.OptionsAliases do
local option_name_lowercase = string.lower(option_alias)
if OPTIONS_lowercase[option_name_lowercase] then
warn("DUPLICATE ALIAS", option_alias)
else
OPTIONS_lowercase[option_name_lowercase] = option_name
end
end
do -- * Load Settings
local function construct_NilinstanceFix(Name, ClassName, Separate)
return function(instance, instancePropertyOverrides)
local Exists
if not Separate then
Exists = OPTIONS.NilInstancesFixes[Name]
end
local Fix
local DoesntExist = not Exists
if DoesntExist then
Fix = Instance.new(ClassName)
if not Separate then
OPTIONS.NilInstancesFixes[Name] = Fix
end
-- Fix.Name = Name
instancePropertyOverrides[Fix] =
{ __SaveSpecific = true, __Children = { instance }, Properties = { Name = Name } }
else
Fix = Exists
table.insert(instancePropertyOverrides[Fix].__Children, instance)
end
-- InstancesOverrides[instance].Parent = AnimationController
if DoesntExist then
return Fix
end
end
end
-- TODO: Merge BaseWrap & Attachment & AdPortal fix (put all under MeshPart container)
-- TODO?:
-- DebuggerWatch DebuggerWatch must be a child of ScriptDebugger
-- PluginAction Parent of PluginAction must be Plugin or PluginMenu that created it!
OPTIONS.NilInstancesFixes.Animator = construct_NilinstanceFix(
"Animator has to be placed under Humanoid or AnimationController",
"AnimationController"
)
OPTIONS.NilInstancesFixes.AdPortal = construct_NilinstanceFix("AdPortal must be parented to a Part", "Part")
OPTIONS.NilInstancesFixes.Attachment =
construct_NilinstanceFix("Attachments must be parented to a BasePart or another Attachment", "Part") -- * Bones inherit from Attachments
OPTIONS.NilInstancesFixes.BaseWrap =
construct_NilinstanceFix("BaseWrap must be parented to a MeshPart", "MeshPart")
OPTIONS.NilInstancesFixes.PackageLink =
construct_NilinstanceFix("Package already has a PackageLink", "Folder", true)
if CustomOptions2 and type(CustomOptions2) == "table" then
local tmp = CustomOptions
local Type = typeof(tmp)
CustomOptions = CustomOptions2
if Type == "Instance" then
CustomOptions.Object = tmp
elseif Type == "table" and typeof(tmp[1]) == "Instance" then
CustomOptions.ExtraInstances = tmp
OPTIONS.IsModel = true
end
end
local Type = typeof(CustomOptions)
if Type == "table" then
if typeof(CustomOptions[1]) == "Instance" then
OPTIONS.mode = "invalidmode"
OPTIONS.ExtraInstances = CustomOptions
OPTIONS.IsModel = true
CustomOptions = {}
else
for key, value in CustomOptions do
local option = OPTIONS_lowercase[string.lower(key)]
if option then
OPTIONS[option] = value
CustomOptions_valid[option] = true
end
end
local Decompile = CustomOptions.Decompile
if Decompile ~= nil then
OPTIONS.noscripts = not Decompile
end
local SavePlayerCharacters = CustomOptions.SavePlayerCharacters
if SavePlayerCharacters ~= nil then
OPTIONS.RemovePlayerCharacters = not SavePlayerCharacters
end
local RemovePlayers = CustomOptions.RemovePlayers
if RemovePlayers ~= nil then
OPTIONS.IsolatePlayers = not RemovePlayers
end
end
elseif Type == "Instance" then
OPTIONS.mode = "invalidmode"
OPTIONS.Object = CustomOptions
CustomOptions = {}
else
CustomOptions = {}
end
end
if not writefile and not OPTIONS.Callback then
-- appendfile could be used a fallback but what kind of exec has appendfile without writefile
local function coreCall(method, ...)
local StarterGui = service.StarterGui
method = StarterGui[method]
if not method then
return
end
for _ = 1, 10 do -- maxtries
local success, result = pcall(method, StarterGui, ...)
if success then
return result
end
task.wait(1)
end
end
local text = 'Function "writefile" is NOT available\nUse the Option "Callback" instead for now (check docs)'
coreCall("SetCore", "SendNotification", {
Title = "SAVEINSTANCE ERROR",
Text = text,
Duration = 15,
Icon = "rbxassetid://9072920609",
})
coreCall("SetCore", "SendNotification", {
Title = "SAVEINSTANCE ERROR",
Text = "Please ask your executor's developers to add writefile",
Duration = 15,
Icon = "rbxassetid://9072920609",
})
warn(text)
GLOBAL_ENV.USSI = nil
return
end
if OPTIONS.IgnoreDefaultPlayerScripts then
-- TODO This is a bad workaround, find a better automatic way
-- TODO Look into https://robloxapi.github.io/ref/class/LuaSourceContainer.html#member-isPlayerScript
local DecompileIgnore = OPTIONS.DecompileIgnore
local default_scripts = ArrayToDict({
ModuleScript = { "PlayerModule" },
LocalScript = {
"BubbleChat",
"ChatScript",
"PlayerScriptsLoader",
"RbxCharacterSounds",
},
}, true)
local function ignorePath(path)
if path then
for _, child in path:GetChildren() do
local class_match = default_scripts[child.ClassName]
if class_match then
local name_match = class_match[child.Name]
if name_match then
table.insert(DecompileIgnore, child)
end
end
end
end
end
ignorePath(service.StarterPlayer:FindFirstChildOfClass("StarterPlayerScripts"))
local LocalPlayer = service.Players.LocalPlayer
if LocalPlayer then
ignorePath(LocalPlayer:FindFirstChildOfClass("PlayerScripts"))
end
end
local InstancesOverrides = {}
local DecompileIgnore, IgnoreList, IgnoreProperties, NotCreatableFixes =
ArrayToDict(OPTIONS.DecompileIgnore, true),
ArrayToDict(OPTIONS.IgnoreList, true),
ArrayToDict(OPTIONS.IgnoreProperties),
ArrayToDict(OPTIONS.NotCreatableFixes, true, "Folder")
local __DEBUG_MODE = OPTIONS.__DEBUG_MODE
if __DEBUG_MODE and type(__DEBUG_MODE) ~= "function" then
__DEBUG_MODE = warn
end
local FilePath = OPTIONS.FilePath
local SaveCacheInterval = OPTIONS.SaveCacheInterval
local ToSaveInstance = OPTIONS.Object
local IsModel = OPTIONS.IsModel
if ToSaveInstance and CustomOptions.IsModel == nil then
IsModel = true
end
local IgnoreDefaultProperties = OPTIONS.IgnoreDefaultProperties
local IgnoreNotArchivable = not OPTIONS.IgnoreNotArchivable
local IgnorePropertiesOfNotScriptsOnScriptsMode = OPTIONS.IgnorePropertiesOfNotScriptsOnScriptsMode
local old_gethiddenproperty
if OPTIONS and gethiddenproperty then
old_gethiddenproperty = gethiddenproperty
gethiddenproperty = nil
end
local SaveNotCreatable = OPTIONS.SaveNotCreatable
local TreatUnionsAsParts = OPTIONS.TreatUnionsAsParts
local DecompileJobless = OPTIONS.DecompileJobless
if DecompileJobless then
OPTIONS.scriptcache = true
end
local ScriptCache = OPTIONS.scriptcache and getscriptbytecode
local Timeout = OPTIONS.timeout
local IgnoreSharedStrings = OPTIONS.IgnoreSharedStrings
local SharedStringOverwrite = OPTIONS.SharedStringOverwrite
local ldeccache = GLOBAL_ENV.scriptcache
local DecompileIgnoring, ToSaveList, ldecompile, placename, elapse_t, SaveNotCreatableWillBeEnabled, RecoveredScripts
if OPTIONS.ReadMe then
RecoveredScripts = {}
end
if ScriptCache and not ldeccache then
ldeccache = {}
GLOBAL_ENV.scriptcache = ldeccache
end
if ToSaveInstance == game then
OPTIONS.mode = "full"
ToSaveInstance = nil
IsModel = nil
end
local function isLuaSourceContainer(instance)
return instance:IsA("LuaSourceContainer")
end
do
local mode = string.lower(OPTIONS.mode)
local tmp = table.clone(OPTIONS.ExtraInstances)
local PlaceName = game.PlaceId
pcall(function()
PlaceName ..= " " .. service.MarketplaceService:GetProductInfo(PlaceName).Name
end)
local function sanitizeFileName(str)
return string.sub(string.gsub(string.gsub(string.gsub(str, "[^%w _]", ""), " +", " "), " +$", ""), 1, 240)
end
if ToSaveInstance then
if mode == "optimized" then -- ! NOT supported with Model file mode
mode = "full"
end
for _, key in
{
"IsolateLocalPlayer",
"IsolateLocalPlayerCharacter",
"IsolatePlayers",
"IsolateStarterPlayer",
"NilInstances",
}
do
if CustomOptions_valid[key] == nil then
OPTIONS[key] = false
end
end
end
local filetype = IsModel and ".rbxmx" or ".rbxlx"
if FilePath then
placename = FilePath
elseif IsModel then
placename =
sanitizeFileName("model " .. PlaceName .. " " .. (ToSaveInstance or tmp[1] or game):GetFullName())
else
placename = sanitizeFileName("place " .. PlaceName)
end
if OPTIONS.AvoidFileOverwrite and isfile then
local counter = 0
local temp = placename
while isfile(temp .. filetype) do
counter += 1
temp = placename .. "(" .. counter .. ")"
end
placename = temp .. filetype
else
placename = placename .. filetype
end
if GLOBAL_ENV[placename] then
-- warn("UniversalSynSaveInstance is already saving to this file")
return
end
GLOBAL_ENV[placename] = true
GLOBAL_ENV.USSI = nil
if mode ~= "scripts" then
IgnorePropertiesOfNotScriptsOnScriptsMode = nil
end
local TempRoot = ToSaveInstance or game
if mode == "full" then
if not ToSaveInstance then
local Children = TempRoot:GetChildren()
if 0 < #Children then
local tmp_dict = ArrayToDict(tmp)
for _, child in Children do
if not tmp_dict[child] then
table.insert(tmp, child)
end
end
end
end
elseif mode == "optimized" then -- ! Incompatible with .rbxmx (Model file) mode
-- if IsolatePlayers then
-- table.insert(_list_0, "Players")
-- end
local tmp_dict = ArrayToDict(tmp)
for _, serviceName in
{
"Workspace",
"Players",
"Lighting",
"MaterialService",
"ReplicatedFirst",
"ReplicatedStorage",
"ServerScriptService", -- LoadStringEnabled property (doesn't replicate); Just in case
"ServerStorage", -- Just in case
"StarterGui",
"StarterPack",
"StarterPlayer",
"Teams",
"SoundService",
"Chat",
"TextChatService",
"LocalizationService", -- For LocalizationTables
-- "InsertService",
"JointsService",
-- "TestService",
-- "VoiceChatService",
}
do
local _service = game:FindService(serviceName)
if _service and not tmp_dict[_service] then
table.insert(tmp, _service)
end
end
elseif mode == "scripts" then
-- TODO: Only save paths that lead to scripts (nothing else)
-- Currently saves paths along with children of each tree
local unique = {}
for _, instance in TempRoot:GetDescendants() do
if isLuaSourceContainer(instance) then
local Parent = instance.Parent
while Parent and Parent ~= TempRoot do
instance = instance.Parent
Parent = instance.Parent
end
if Parent then
unique[instance] = true
end
end
end
for instance in unique do
table.insert(tmp, instance)
end
end
ToSaveList = tmp
if ToSaveInstance then
table.insert(ToSaveList, 1, ToSaveInstance)
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
-- local totalsize = #totalstr
for i, unit in
{
"B",
"KB",
"MB",
"GB",
"TB",
}
do
if totalsize < 0x400 ^ i then
Size = math.floor(totalsize / (0x400 ^ (i - 1)) * 10) / 10 .. " " .. unit
break
end
end
return Size
end
local RunService = service.RunService
local function wait_for_render()
RunService.RenderStepped:Wait()
end
local Loading
local function run_with_loading(text, keepStatus, waitForRender, taskFunction, ...)
local previousStatus
if StatusText then
if keepStatus then
previousStatus = StatusText.Text
end
Loading = task.spawn(function()
local spinner_count = 0
local chars = { "|", "/", "", "\\" }
local chars_size = #chars
local function getLoadingText()
spinner_count += 1
if chars_size < spinner_count then
spinner_count = 1
end
return chars[spinner_count]
end
text ..= " "
while true do
StatusText.Text = text .. getLoadingText()
task.wait(0.25)
end
end)
if waitForRender then
wait_for_render()
end
end
local result = { taskFunction(...) }
if Loading then
task.cancel(Loading)
Loading = nil
if previousStatus then
StatusText.Text = previousStatus
end
end
return unpack(result)
end
local function construct_TimeoutHandler(timeout, f, timeout_ret)
return timeout < 0 and function(script)
return pcall(f, script)
end or function(script) -- TODO Ideally use ... (vararg) instead of `script` in case this is reused for something other than `decompile` & `getscriptbytecode`
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 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 = decompile
if OPTIONS.noscripts then
ldecompile = function()
return "-- Decompiling is disabled"
end
elseif Decompiler then
local decomp = construct_TimeoutHandler(Timeout, Decompiler, "Decompiler timed out")
ldecompile = function(script)
-- local name = scr.ClassName .. scr.Name
local bytecode
if ScriptCache then
local s
s, bytecode = getbytecode(script)
local cached
if s then
if not bytecode or bytecode == "" then
return "-- The Script is Empty"
end
cached = ldeccache[bytecode]
else
bytecode = nil
end
if cached then
if __DEBUG_MODE then
__DEBUG_MODE("Found in Cache", script:GetFullName())
end
return cached
end
else
if DecompileJobless then
return "-- Not found in already decompiled ScriptCache"
end
-- task.wait() -- TODO Maybe remove?
end
local ok, result = run_with_loading("Decompiling " .. script.Name, true, nil, decomp, script)
if not result then
ok, result = false, "Empty Output"
end
local output
if ok then
result = string.gsub(result, "\0", "\\0") -- ? Some decompilers sadly output \0 which prevents files from opening
output = result
else
output = "--[[ Failed to decompile. Reason:\n" .. (result or "") .. "\n]]"
end
if ScriptCache and bytecode then -- TODO there might(?) be an edgecase where it manages to decompile (built-in) even though getscriptbytecode failed, and the output won't get cached
ldeccache[bytecode] = output -- ? Should we cache even if it timed out?
if __DEBUG_MODE then
__DEBUG_MODE("Cached", script:GetFullName())
end
end
return output
end
else
ldecompile = function()
return "-- Your Executor does NOT have a Decompiler"
end
end
end
local function GetLocalPlayer()
return service.Players.LocalPlayer
or service.Players:GetPropertyChangedSignal("LocalPlayer"):Wait()
or service.Players.LocalPlayer
end
local function filterLinkedSource(str)
local o, r = pcall(service.HttpService.JSONDecode, service.HttpService, str)
if o and r.errors then
return
end
return true
end
local function replaceClassName(instance, InstanceName, ClassName)
local InstanceOverride
if InstanceName ~= ClassName then -- TODO Compare against default instance instead (TouchTransmitter is called TouchInterest by default)
InstanceOverride = InstancesOverrides[instance]
if not InstanceOverride then
InstanceOverride = { Properties = { Name = "[" .. ClassName .. "] " .. InstanceName } }
InstancesOverrides[instance] = InstanceOverride
end
end
return InstanceOverride
end
local function filterPropVal(result, propertyName, category) -- ? raw == nil thanks to SerializedDefaultAttributes; "can't get value" - due to WriteOnly tag; "Invalid value for enum " - "StreamingPauseMode" (old games probably) Roexec
return result == nil
or result == "can't get value"
or type(result) == "string"
and (category == "Enum" or string_find(result, "Unable to get property " .. propertyName))
end
local __BREAK = "__BREAK" .. service.HttpService:GenerateGUID(false)
local function ReadProperty(instance, property, propertyName, special, category, optional)
local raw = __BREAK
local InstanceOverride = InstancesOverrides[instance]
if InstanceOverride then
local PropertiesOverride = InstanceOverride.Properties
if PropertiesOverride then
local PropertyOverride = PropertiesOverride[propertyName]
if PropertyOverride ~= nil then
return PropertyOverride
end
end
end
local CanRead = property.CanRead
if CanRead == false then -- * Skips because we've checked this property before
return __BREAK
end
if special then
if gethiddenproperty then
local ok, result = pcall(gethiddenproperty, instance, propertyName)
if ok then
raw = result
end
if filterPropVal(raw, propertyName, category) then
-- * Skip next time we encounter this too perhaps (unless there's a chance for it to be readable on other instance, somehow)
if result ~= nil or not optional then
if __DEBUG_MODE then
__DEBUG_MODE("Filtered", propertyName)
end
-- Property.Special = false
property.CanRead = false
end
return __BREAK -- ? We skip it because even if we use "" it will just reset to default in most cases, unless it's a string tag for example (same as not being defined)
end
end
else
if CanRead then
raw = instance[propertyName]
else -- Assuming CanRead == nil (untested)
local ok, result = pcall(index, instance, propertyName)
if ok then
raw = result
elseif gethiddenproperty then -- ! Be careful with this 'and gethiddenproperty' logic
ok, result = pcall(gethiddenproperty, instance, propertyName)
if ok then
raw = result
property.Special = true
end
end
property.CanRead = ok
if not ok or filterPropVal(raw, propertyName, category) then
return __BREAK
end
end
end
return raw
end
local function ReturnItem(className, instance)
return '<Item class="' .. className .. '" referent="' .. GetRef(instance) .. '"><Properties>' -- TODO: Ideally this shouldn't return <Properties> as well as the line below to close it IF IgnorePropertiesOfNotScriptsOnScriptsMode is Enabled OR If all properties are default (reduces file size by at least 1.4%)
end
local function ReturnProperty(tag, propertyName, value)
return "<" .. tag .. ' name="' .. propertyName .. '">' .. value .. "</" .. tag .. ">"
end
local function ReturnValueAndTag(raw, valueType, descriptor)
local value, tag = (descriptor or XML_Descriptors[valueType])(raw)
return value, tag or valueType
end
local function InheritsFix(fixes, className, instance)
local Fix = fixes[className]
if Fix then
return Fix
elseif Fix == nil then
for class_name, fix in fixes do
if instance:IsA(class_name) then
return fix
end
end
end
end
local function GetInheritedProps(className)
local cached = inherited_properties[className]
if cached then
return cached
end
local prop_list = {}
local layer = ClassList[className]
while layer do
local layer_props = layer.Properties
table.move(layer_props, 1, #layer_props, #prop_list + 1, prop_list)
-- for _, prop in layer.Properties do
-- prop_list[prop_count] = prop -- ? table.clone is needed for case where .Default is modified
-- prop_count += 1
-- end
layer = ClassList[layer.Superclass]
end
inherited_properties[className] = prop_list
return prop_list
end
local CHUNK_LIMIT = 200 * 1024 * 1024 -- string length overflow prevention
local function save_cache(final)
local savestr = table.concat(savebuffer)
currentstr ..= savestr -- TODO: Causes "not enough memory" error on some exec
-- writefile(placename, totalstr)
-- appendfile(placename, savestr) -- * supposedly causes uneven amount of Tags (e.g. <Item> must be closed with </Item> but sometimes there's more of one than the other). While being under load, the function produces unexpected output?
local savestr_len = #savestr
totalsize += savestr_len
currentsize += savestr_len
table.clear(savebuffer)
savebuffer_size = 1
if CHUNK_LIMIT < currentsize or final then
table.insert(chunks, { size = currentsize, str = currentstr })
currentstr, currentsize = "", 0
end
if StatusText then
StatusText.Text = "Saving.. Size: " .. get_size_format()
end
-- ? Needed for at least 1fps (status text)
-- task.wait()
wait_for_render()
end
local function save_specific(className, properties)
local Ref = Instance.new(className) -- ! Assuming anything passed here is Creatable
local Item = ReturnItem(Ref.ClassName, Ref)
for propertyName, val in properties do
local whitelisted, value, tag
-- TODO: Improve all sort of overrides & exceptions in the code (code below is awful)
if "Source" == propertyName then
tag = "ProtectedString"
value = XML_Descriptors.__PROTECTEDSTRING(val)
whitelisted = true
elseif "Name" == propertyName then
whitelisted = true
value, tag = ReturnValueAndTag(val, "string") -- * Doubt ValueType will change
end
if whitelisted then
Item ..= ReturnProperty(tag, propertyName, value)
end
end
Item ..= "</Properties>"
return Item
end
local function save_hierarchy(hierarchy)
for _, instance in hierarchy do
local InstanceOverride, ClassTagOverride, ClassNameOverride
if not InstanceOverride then
InstanceOverride = InstancesOverrides[instance]
if InstanceOverride then
ClassTagOverride = InstanceOverride.__ClassName
end
end
local ClassName = instance.ClassName
local InstanceName = instance.Name
local SkipEntirely
if not ClassTagOverride then -- ! Assuming anything that has __ClassName comes from save_extra
if IgnoreNotArchivable and not instance.Archivable then
continue
end
SkipEntirely = IgnoreList[instance]
if SkipEntirely then
continue
end
do
local OnIgnoredList = IgnoreList[ClassName]
if OnIgnoredList and (OnIgnoredList == true or OnIgnoredList[InstanceName]) then
continue
end
end
if not DecompileIgnoring then
DecompileIgnoring = DecompileIgnore[instance]
if DecompileIgnoring == nil then
local DecompileIgnored = DecompileIgnore[ClassName]
if DecompileIgnored then
DecompileIgnoring = DecompileIgnored == true or DecompileIgnored[InstanceName]
end
end
if DecompileIgnoring then
DecompileIgnoring = instance
elseif DecompileIgnoring == false then
DecompileIgnoring = 1 -- Ignore one instance
end
end
do
local Fix = NotCreatableFixes[ClassName]
if Fix then
if SaveNotCreatable then
ClassName, InstanceOverride = Fix, replaceClassName(instance, InstanceName, ClassName)
else
continue -- They won't show up in Studio anyway (Enable SaveNotCreatable 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
ClassName, InstanceOverride = "Part", replaceClassName(instance, InstanceName, ClassName)
ClassNameOverride = "BasePart" -- * Mutual Superclass for PartOperation and Part; For properties only
elseif not ClassList[ClassName] then -- ? API Dump is outdated then
if __DEBUG_MODE then
__DEBUG_MODE("Class not Found", ClassName)
end
ClassTagOverride = ClassName -- ? To at least retain .ClassName unlike the rest of the class-specific properties
ClassName = "Folder" -- ? replaceClassName is not needed because of the ClassTagOverride
end
end
end
end
-- ? 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 InstanceOverride and InstanceOverride.__SaveSpecific then
savebuffer[savebuffer_size] = save_specific(ClassName, InstanceOverride.Properties) -- ! Assuming anything that has __SaveSpecific will have .Properties
savebuffer_size += 1
else
-- local Properties =
savebuffer[savebuffer_size] = ReturnItem(ClassTagOverride or ClassName, instance) -- TODO: Ideally this shouldn't return <Properties> as well as the line below to close it IF IgnorePropertiesOfNotScriptsOnScriptsMode is ENABLED
savebuffer_size += 1
if not (IgnorePropertiesOfNotScriptsOnScriptsMode and not isLuaSourceContainer(instance)) then
local default_instance, new_def_inst
if IgnoreDefaultProperties then
default_instance = default_instances[ClassName]
if not default_instance then
local Class = ClassList[ClassName]
if not Class.NotCreatable then -- __api_dump_class_not_creatable__ also indicates this
-- NotCreatableFixes are exceptions to the check above meaning if we don't keep the NotCreatableFixes updated then Instance.new below might start erroring in the future potentially; HOWEVER IsPropertyModified solves this issue and no updates are really needed as NotCreatableFixes is up-to-date as of VERSION-HERE (which is when IPM gets enabled)
local ok, result = pcall(Instance.new, ClassName) -- ! pcall is needed for level 3 execs (for example TestService); EXCEPTION NOTED ABOVE (irrelevant)
if ok then
new_def_inst = result
default_instance = {}
default_instances[ClassName] = default_instance
else
Class.NotCreatable = true
if __DEBUG_MODE then
__DEBUG_MODE("Failed to create default Instance", ClassName, result)
end
end
elseif __DEBUG_MODE then
__DEBUG_MODE("Unable to create default Instance (NotCreatable)", ClassName)
end
end
end
for _, Property in GetInheritedProps(ClassNameOverride or ClassName) do
local PropertyName = Property.Name
if IgnoreProperties[PropertyName] then
continue
end
local ValueType = Property.ValueType
if IgnoreSharedStrings and ValueType == "SharedString" then -- ? More info in Options
continue
end
local Special, Category, Optional = Property.Special, Property.Category, Property.Optional
local raw = ReadProperty(instance, Property, PropertyName, Special, Category, Optional)
if raw == __BREAK then -- ! Assuming __BREAK is always returned when there's a failure to read a property
local GHPFFailed, Fallback = Property.GHPFFailed, Property.Fallback
if GHPFFailed and not Fallback then
continue
end
if not GHPFFailed then
local ok, result = pcall(gethiddenproperty_fallback, instance, PropertyName) -- * This helps in reading: Vector3int16, OptionalCoordinateFrame DataTypes. It also acts as an almost entire fallback for gethiddenproperty in case it is missing
if result == nil and not Optional then
ok = nil
end
if ok then
raw = result
else
GHPFFailed = true
Property.GHPFFailed = GHPFFailed
end
end
if GHPFFailed and Fallback then
local ok, result = pcall(Fallback, instance)
if ok then
raw = result
else
Property.Fallback = nil -- Low level execs might fail due to lack of some Capabilities
if __DEBUG_MODE then
__DEBUG_MODE("Fix Failed", PropertyName, result)
end
continue
end
end
if raw == __BREAK then
continue
end
end
if SharedStringOverwrite and ValueType == "BinaryString" then -- TODO: Convert this to table if more types are added
ValueType = "SharedString"
end
-- Special = Property.Special -- ? Read TODO below (must be updated if it's used frequently afterwards)
if
default_instance
and Property.CanRead
and not Property.Special -- TODO: .Special is checked more than once (because it might be updated during ReadProperty)
and not (PropertyName == "Source" and isLuaSourceContainer(instance))
then -- ? Could be not just "Source" in the future
if new_def_inst then
default_instance[PropertyName] = index(new_def_inst, PropertyName)
end
if default_instance[PropertyName] == raw then
continue
end
end
-- Serialization start
local tag, value
if Category == "Class" then
tag = "Ref"
if raw then
if SaveNotCreatableWillBeEnabled then
local Fix = NotCreatableFixes[raw.ClassName]
if
Fix
and (
PropertyName == "PlayerToHideFrom"
or ValueType ~= "Instance" and ValueType ~= Fix
)
then -- * To avoid errors
continue
end
end
value = GetRef(raw)
else
value = "null"
end
elseif Category == "Enum" then -- ! We do this order (Enums before Descriptors) specifically because Font Enum might get a Font Descriptor despite having Enum Category, unlike Font DataType which that Descriptor is meant for
value, tag = XML_Descriptors.EnumItem(raw)
else
local Descriptor = XML_Descriptors[ValueType]
if Descriptor then
value, tag = ReturnValueAndTag(raw, ValueType, Descriptor)
elseif "ProtectedString" == ValueType then -- TODO: Try fitting this inside Descriptors
tag = ValueType
if PropertyName == "Source" then
if DecompileIgnoring then -- ? Should this really prevent extraction of the original source if present ?
if DecompileIgnoring == 1 then
DecompileIgnoring = nil
end
value = "-- Ignored"
else
local should_decompile = true
local LinkedSource
local LinkedSource_Url = instance.LinkedSource -- ! Assuming every Class that has ProtectedString Source property also has a LinkedSource property
local hasLinkedSource = LinkedSource_Url ~= ""
local LinkedSource_type
if hasLinkedSource then
local Path = instance:GetFullName()
if RecoveredScripts then
table.insert(RecoveredScripts, Path)
end
LinkedSource = string.match(LinkedSource_Url, "%w+$") -- TODO: No sure if this pattern matches all possible cases. Example is: 'rbxassetid://0&hash=cd73dd2fe5e5013137231c227da3167e'
if LinkedSource then
if ScriptCache then
local cached = ldeccache[LinkedSource]
if cached then
value = cached
should_decompile = nil
end
end
if should_decompile then
if DecompileJobless then
value = "-- Not found in LinkedSource ScriptCache"
should_decompile = nil
end
LinkedSource_type = string.find(LinkedSource, "%a") and "hash"
or "id"
local asset = LinkedSource_type .. "=" .. LinkedSource
local ok, source = pcall(function()
-- Credits @halffalse
return game:HttpGet(
"https://assetdelivery.roproxy.com/v1/asset/?" .. asset
)
end)
if ok and filterLinkedSource(source) then
if ScriptCache then
ldeccache[LinkedSource] = source
end
value = source
should_decompile = nil
end
end
else --if __DEBUG_MODE then -- * We print this anyway because very important
warn(
"FAILED TO EXTRACT ORIGINAL SCRIPT SOURCE (OPEN A GITHUB ISSUE): ",
instance:GetFullName(),
LinkedSource_Url
)
end
end
if should_decompile then
local isLocalScript = instance:IsA("LocalScript")
if
isLocalScript and instance.RunContext == Enum.RunContext.Server
or not isLocalScript
and instance:IsA("Script")
and instance.RunContext ~= Enum.RunContext.Client
then
value = "-- [FilteringEnabled] Server Scripts are IMPOSSIBLE to save" -- TODO: Could be not just server scripts in the future
else
value = ldecompile(instance)
if SaveBytecode then
local output = SaveBytecode(instance)
if output then
value = output .. value
end
end
end
end
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
end
value = XML_Descriptors.__PROTECTEDSTRING(value)
else
--OptionalCoordinateFrame and so on, we make it dynamic
if Optional then
Descriptor = XML_Descriptors[Optional]
if Descriptor then
if raw == nil then
-- * It can be empty, because it's optional
-- ? Though why even save it if it's empty considering it's optional
continue
-- value, tag = "", ValueType
else
value, tag = ReturnValueAndTag(raw, ValueType, Descriptor)
end
end
end
end
end
if tag then
savebuffer[savebuffer_size] = ReturnProperty(tag, PropertyName, value)
savebuffer_size += 1
else --if __DEBUG_MODE then -- * We print this anyway because very important
warn("UNSUPPORTED TYPE (OPEN A GITHUB ISSUE): ", ValueType, ClassName, PropertyName)
end
end
end
savebuffer[savebuffer_size] = "</Properties>"
savebuffer_size += 1
if SaveCacheInterval < savebuffer_size then
save_cache()
end
end
if SkipEntirely ~= false then -- ? We save instance without it's descendants in this case (== false)
local Children = InstanceOverride and InstanceOverride.__Children or instance:GetChildren()
if #Children ~= 0 then
save_hierarchy(Children)
end
end
if DecompileIgnoring and DecompileIgnoring == instance then
DecompileIgnoring = nil
end
savebuffer[savebuffer_size] = "</Item>"
savebuffer_size += 1
end
end
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 += 1
if hierarchy then
save_hierarchy(hierarchy)
end
savebuffer[savebuffer_size] = "</Item>"
savebuffer_size += 1
end
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"
Also http can be converted to https but not sure if Roblox cares
-- ? <External>null</External><External>nil</External> - <External> is a legacy concept that is no longer used.
]]
header ..= '<Meta name="ExplicitAutoJoints">true</Meta>'
end
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
SaveNotCreatableWillBeEnabled = SaveNotCreatable
or (IsolateLocalPlayer or IsolateLocalPlayerCharacter) and IsolateLocalPlayer
or IsolatePlayers
or NilInstances and global_container.getnilinstances -- ! Make sure this accurately reflects everything below
save_hierarchy(ToSaveList)
if IsolateLocalPlayer or IsolateLocalPlayerCharacter then
local LocalPlayer = service.Players.LocalPlayer
if LocalPlayer then
if IsolateLocalPlayer then
SaveNotCreatable = true
save_extra("LocalPlayer", LocalPlayer, true)
end
if IsolateLocalPlayerCharacter then
local LocalPlayerCharacter = LocalPlayer.Character
if LocalPlayerCharacter then
save_extra("LocalPlayer Character", LocalPlayerCharacter, true, "Model")
end
end
end
end
if IsolateStarterPlayer then
-- SaveNotCreatable = true -- TODO: Enable if StarterPlayerScripts or StarterCharacterScripts stop showing up in isolated folder in Studio
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) -- no reason to saveprops as you can see the props on the original instance
end
if NilInstances and global_container.getnilinstances then
local nil_instances, nil_instances_size = {}, 1
local NilInstancesFixes = OPTIONS.NilInstancesFixes
for _, instance in global_container.getnilinstances() do
if instance == game then
instance = nil
-- break
else
local ClassName = instance.ClassName
local Fix = InheritsFix(NilInstancesFixes, ClassName, instance)
if Fix then
instance = Fix(instance, InstancesOverrides)
-- continue
end
local Class = ClassList[ClassName]
if Class then
if Class.Service then -- For CSGDictionaryService, NonReplicatedCSGDictionaryService, LogService, ProximityPromptService, TestService & more
-- instance.Parent = game
instance = nil
-- continue
end
end
end
if instance then
nil_instances[nil_instances_size] = instance
nil_instances_size += 1
end
end
SaveNotCreatable = true
save_extra("Nil Instances", nil_instances)
end
if OPTIONS.ReadMe then
save_extra(
"README",
nil,
nil,
"Script",
"--[[\n"
.. (#RecoveredScripts ~= 0 and "\t\tIMPORTANT: Original Source of these Scripts was Recovered: " .. service.HttpService:JSONEncode(
RecoveredScripts
) .. "\n" or "")
.. [[
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
ServerStorage, ServerScriptService and Server Scripts are IMPOSSIBLE to save because of FilteringEnabled.
If your player cannot spawn into the game, please move the scripts in StarterPlayer somewhere else or delete them. Then run `game:GetService("Players").CharacterAutoLoads = true`.
And use "Play Here" to start game instead of "Play" to spawn your Character where your Camera currently is.
If the chat system does not work, please use the explorer and delete everything inside the TextChatService/Chat service(s).
Or run `game:GetService("Chat"):ClearAllChildren() game:GetService("TextChatService"):ClearAllChildren()`
If Union and MeshPart collisions don't work, run the script below in the Studio Command Bar:
local C = game:GetService("CoreGui")
local D = Enum.CollisionFidelity.Default
for _, v in game:GetDescendants() do
if v:IsA("TriangleMeshPart") and not v:IsDescendantOf(C) then
v.CollisionFidelity = D
end
end
print("Done")
If you can't move the Camera, run this script in the Studio Command Bar:
workspace.CurrentCamera.CameraType = Enum.CameraType.Fixed
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: "
.. game.PlaceVersion
.. " Client Version: "
.. version()
.. " Executor: "
.. (identify_executor and table.concat({ identify_executor() }, " ") or "Unknown")
.. "\n]]"
)
end
do
local tmp = { "<SharedStrings>" }
for value, identifier in SharedStrings do
table.insert(tmp, '<SharedString md5="' .. identifier .. '">' .. value .. "</SharedString>")
end
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] = "</SharedStrings>"
savebuffer_size += 1
end
end
savebuffer[savebuffer_size] =
"</roblox><!-- Saved by UniversalSynSaveInstance (Join to Copy Games) https://discord.gg/wx4ThpAsmw -->"
savebuffer_size += 1
save_cache(true)
do
-- ! Assuming we only write to file once hence why we only filter once
-- TODO This might cause issues on non-unique Usernames (ex. "Cake" if game is about cakes then everything supposedly related to your name will be replaced with "Roblox"); Certain UserIds might also affect numbers, like if your UserId is 2481848 and there is some number that goes like "1.248184818837" then that the matched part will be replaced with 1, potentially making the number incorrect.
-- TODO So for now it's best to keep this disabled by default
-- 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)
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 chunks do
chunk.str = gsubCaseInsensitive(
string.gsub(chunk.str, LocalPlayer.UserId, Anonymous.UserId),
LocalPlayer.Name,
Anonymous.Name
)
end
end
end
local Callback = OPTIONS.Callback
if Callback then
local totalstr = header
for _, chunk in chunks do
totalstr ..= chunk.str
end
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.
local totallen, currentlen = math.ceil(totalsize / SEGMENT_SIZE), 1
for _, chunk in chunks do
local length = math.ceil(chunk.size / SEGMENT_SIZE)
for i = 1, length do
local savestr = string.sub(chunk.str, (i - 1) * SEGMENT_SIZE + 1, i * SEGMENT_SIZE)
run_with_loading(
"Writing to File " .. math.round(currentlen / totallen * 100) .. "% (Depends on Exec)",
nil,
true,
appendfile,
placename,
savestr
)
currentlen += 1
if i ~= length then
task.wait()
end
end
end
else
local totalstr = header
for _, chunk in chunks do
totalstr ..= chunk.str
end
run_with_loading(
"Writing " .. get_size_format() .. " to File (Depends on Exec)",
nil,
true,
writefile,
placename,
totalstr
)
end
end
end
local Connections
do
local Players = service.Players
if IgnoreList.Model ~= true then
Connections = {}
local function ignoreCharacter(player)
table.insert(
Connections,
player.CharacterAdded:Connect(function(character)
IgnoreList[character] = true
end)
)
local Character = player.Character
if Character then
IgnoreList[Character] = true
end
end
if OPTIONS.RemovePlayerCharacters then
table.insert(
Connections,
Players.PlayerAdded:Connect(function(player)
ignoreCharacter(player)
end)
)
for _, player in Players:GetPlayers() do
ignoreCharacter(player)
end
else
IgnoreNotArchivable = false -- TODO Bad solution (Characters are NotArchivable); Also make sure the next solution is compatible with IsolateLocalPlayerCharacter
if IsolateLocalPlayerCharacter then
task.spawn(function()
ignoreCharacter(GetLocalPlayer())
end)
end
end
end
if IsolateLocalPlayer and IgnoreList.Player ~= true then
task.spawn(function()
IgnoreList[GetLocalPlayer()] = true
end)
end
end
if IsolateStarterPlayer then
IgnoreList.StarterPlayer = false
end
if IsolatePlayers then
IgnoreList.Players = false
end
if OPTIONS.ShowStatus then
do
local Exists = GLOBAL_ENV._statustext
if Exists then
Exists:Destroy()
end
end
local StatusGui = Instance.new("ScreenGui")
GLOBAL_ENV._statustext = StatusGui
StatusGui.DisplayOrder = 2e9
pcall(function() -- ? Compatibility with level 2
StatusGui.OnTopOfCoreBlur = true
end)
StatusText = Instance.new("TextLabel")
StatusText.Text = "Saving..."
StatusText.BackgroundTransparency = 1
StatusText.Font = Enum.Font.Code
StatusText.AnchorPoint = Vector2.new(1)
StatusText.Position = UDim2.new(1)
StatusText.Size = UDim2.new(0.3, 0, 0, 20)
StatusText.TextColor3 = Color3.new(1, 1, 1)
StatusText.TextScaled = true
StatusText.TextStrokeTransparency = 0.7
StatusText.TextXAlignment = Enum.TextXAlignment.Right
StatusText.TextYAlignment = Enum.TextYAlignment.Top
StatusText.Parent = StatusGui
local function randomString()
local length = math.random(10, 20)
local randomarray = table.create(length)
for i = 1, length do
randomarray[i] = string.char(math.random(32, 126))
end
return table.concat(randomarray)
end
if global_container.gethui then
StatusGui.Name = randomString()
StatusGui.Parent = global_container.gethui()
else
if global_container.protectgui then
StatusGui.Name = randomString()
global_container.protectgui(StatusGui)
StatusGui.Parent = game:GetService("CoreGui")
else
local RobloxGui = game:GetService("CoreGui"):FindFirstChild("RobloxGui")
if RobloxGui then
StatusGui.Parent = RobloxGui
else
StatusGui.Name = randomString()
StatusGui.Parent = game:GetService("CoreGui")
end
end
end
end
do
local SafeMode = OPTIONS.SafeMode
if SafeMode then
task.spawn(function()
local LocalPlayer = GetLocalPlayer()
local PlayerScripts = LocalPlayer:FindFirstChildOfClass("PlayerScripts")
if PlayerScripts then
local function construct_InstanceOverride(instance)
local children = instance:GetChildren()
InstancesOverrides[instance] = {
__Children = children,
}
for _, child in 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)
service.RunService:Set3dRenderingEnabled(false)
end
local anti_idle
if OPTIONS.AntiIdle then
task.spawn(function()
local Idled = GetLocalPlayer().Idled
if getconnections then
for _, c in getconnections(Idled) do
if not pcall(function()
c:Disable()
end) then
pcall(function()
c:Disconnect()
end)
end
end
end
anti_idle = Idled:Connect(function()
service.VirtualInputManager:SendMouseWheelEvent(
service.UserInputService:GetMouseLocation().X,
service.UserInputService:GetMouseLocation().Y,
true,
game
)
end)
end)
end
elapse_t = os.clock()
local ok, err = xpcall(save_game, function(err)
return debug.traceback(err)
end)
if SafeMode then
service.GuiService:ClearError()
service.RunService:Set3dRenderingEnabled(true)
end
if old_gethiddenproperty then
gethiddenproperty = old_gethiddenproperty
end
if anti_idle then
anti_idle:Disconnect()
end
if Connections then
for _, connection in Connections do
connection:Disconnect()
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()
if ok then
StatusText.Text = string.format("Saved! Time %.3f seconds; Size %s", elapse_t, get_size_format())
StatusText.TextColor3 = Color3.new(0, 1)
task.wait(Log10 * 2 + ExtraTime)
else
if Loading then
task.cancel(Loading)
Loading = nil
end
StatusText.Text = "Failed! Check F9 console for more info"
StatusText.TextColor3 = Color3.new(1)
warn("Error found while saving:")
warn(err)
task.wait(Log10 + ExtraTime)
end
StatusText:Destroy()
end)
end
if OPTIONS.ShutdownWhenDone and ok then
task.wait(Log10 * 2 + ExtraTime)
game:Shutdown()
end
end
end
return synsaveinstance