mirror of
https://github.com/luau/UniversalSynSaveInstance.git
synced 2026-02-03 14:13:07 +02:00
4067 lines
135 KiB
Lua
4067 lines
135 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 next, 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 next, 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 next, 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 = num % (bit32.bxor(num, 32))
|
|
if 0x80000000 < num then
|
|
num = num - bit32.bxor(num, 32)
|
|
end
|
|
return num
|
|
end
|
|
|
|
bit32.byteswap = function(num)
|
|
local BYTE_SIZE = 8
|
|
local MAX_BYTE_VALUE = 255
|
|
|
|
num = 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 = 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 = 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)
|
|
RemoteCommand = 2 ^ 59, -------- 59 (0x3b)
|
|
InternalTest = 2 ^ 60, --------- 60 (0x3c), Related to TestingGameScript
|
|
PluginOrOpenCloud = 2 ^ 61, ---- 61 (0x3d)
|
|
Assistant = 2 ^ 62, ------------ 62 (0x3e)
|
|
-- Restricted = 2 ^ 63, -------- for negative (highest bit for i64)
|
|
}
|
|
|
|
local function __COUNT_CAPABILITY_BITS(raw)
|
|
-- TODO tostring & string.split aren't ideal but this is the only way until the feature is out of the experimental phase (SecurityCapabilities.Contains exists but the Enums that it accepts lacks some hidden bits) - NotAccessible, Unknown, Restricted
|
|
-- ! Seems like both tostring & .Contains ignore high / internal bits (anything above CapabilityControl): RemoteCommand, InternalTest, PluginOrOpenCloud, Assistant. They're present when created & saved by Studio but can't be read through current means
|
|
|
|
local result = 0
|
|
for _, flag in next, string.split(tostring(raw), " | ") do
|
|
local bit = CAPABILITY_BITS[flag]
|
|
if bit then
|
|
result = result + bit
|
|
end
|
|
end
|
|
return result
|
|
end
|
|
|
|
local function __COUNT_BITS(...) -- * Credits to Friend (you know yourself)
|
|
local Value = 0
|
|
|
|
for i, bit in next, { ... } do
|
|
if bit then
|
|
Value = Value + 2 ^ (i - 1)
|
|
end
|
|
end
|
|
|
|
return Value
|
|
end
|
|
|
|
local Binary_Descriptors
|
|
Binary_Descriptors = {
|
|
__PACK_MULTIPLE = function(descriptor, value1, value2, value3)
|
|
local buf1, size1 = descriptor(value1)
|
|
local buf2, size2 = descriptor(value2)
|
|
|
|
local len = size1 + size2
|
|
local buf3, size3
|
|
|
|
if value3 ~= nil then
|
|
buf3, size3 = descriptor(value3)
|
|
len = len + size3
|
|
end
|
|
|
|
local 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 next, Keypoints do
|
|
keypoint_handler(keypoint, b, offset)
|
|
offset = offset + keypointSize
|
|
end
|
|
|
|
return b, len
|
|
end
|
|
end,
|
|
__writei64 = function(b, offset, raw)
|
|
local low = bit32.band(raw, 0xFFFFFFFF)
|
|
local high = (raw - low) / 0x100000000
|
|
|
|
buffer.writei32(b, offset, low)
|
|
buffer.writei32(b, offset + 4, high)
|
|
end,
|
|
__PACK_F32 = nil,
|
|
__PACK_I16 = nil,
|
|
__construct__PACKER = function(float)
|
|
local writeFunc = float and buffer.writef32 or buffer.writei16
|
|
local elementSize = float and 4 or 2
|
|
|
|
-- local zbuf, nozbuf = buffer.create(elementSize * 3), buffer.create(elementSize * 2)
|
|
|
|
return function(X, Y, Z)
|
|
local len = Z and (elementSize * 3) or (elementSize * 2)
|
|
local b = buffer.create(len)
|
|
|
|
writeFunc(b, 0, X)
|
|
writeFunc(b, elementSize, Y)
|
|
if Z then
|
|
writeFunc(b, elementSize * 2, Z)
|
|
end
|
|
|
|
return b, len
|
|
end
|
|
end,
|
|
--------------------------------------------------------------
|
|
--------------------------------------------------------------
|
|
--------------------------------------------------------------
|
|
["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 = offset + 4
|
|
buffer.writef32(b, offset, keypoint.Time)
|
|
offset = 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 = offset + 4
|
|
buffer.writef32(b, offset, keypoint.Time)
|
|
offset = 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 = 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 = {
|
|
["&"] = "&", -- 38
|
|
["<"] = "<", -- 60
|
|
[">"] = ">", -- 62
|
|
['"'] = """, -- quot
|
|
["'"] = "'", -- 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 next, raw.Keypoints do
|
|
sequence = 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 = 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
|
|
next,
|
|
{
|
|
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 next, attrs do
|
|
attrs_n = 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 = 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 next, attrs_sorted do
|
|
local b_Name, Name_size = string__descriptor(attr)
|
|
|
|
buffer.copy(b, offset, b_Name)
|
|
offset = offset + Name_size
|
|
|
|
buffer.writeu8(b, offset, attr_Type_IDs[typeof(attrs[attr])])
|
|
offset = offset + 1
|
|
|
|
local bb = attrs_formatted[attr]
|
|
|
|
buffer.copy(b, offset, bb)
|
|
offset = 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 next, attenuations do
|
|
attenuations_n = 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 next, attenuations_sorted do
|
|
buffer.writef32(b, offset, key)
|
|
offset = offset + 4
|
|
buffer.writef32(b, offset, attenuations[key]) -- volume
|
|
offset = 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 next, 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 = offset + 12
|
|
|
|
local yBasis = __PACK_F32(R10, R11, R12)
|
|
buffer.copy(b, offset, yBasis)
|
|
offset = offset + 12
|
|
|
|
local zBasis = __PACK_F32(R20, R21, R22)
|
|
buffer.copy(b, offset, zBasis)
|
|
offset = offset + 12
|
|
|
|
local position = __PACK_F32(X, Y, Z)
|
|
buffer.copy(b, offset, position)
|
|
offset = 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 next, control_points do
|
|
local buf = Path2DControlPoint_descriptor(point)
|
|
|
|
buffer.writeu8(b, offset, TypeID_Path2DControlPoint)
|
|
offset = offset + 1
|
|
|
|
buffer.copy(b, offset, buf)
|
|
offset = 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 next, markers do
|
|
strings_size = 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 next, markers do
|
|
local value = marker.Value
|
|
buffer.writestring(b, offset, value)
|
|
offset = 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 = offset + 4
|
|
buffer.writeu32(b, offset, markers_n)
|
|
offset = offset + 4
|
|
|
|
for i, marker in next, markers do
|
|
local scaled_time = math.round(marker.Time * 2400)
|
|
buffer.writeu32(b, offset, scaled_time)
|
|
offset = 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 next, input_pins do
|
|
buffer_size = 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 next, input_pins do
|
|
local b_pin, pin_size = string__descriptor(pin)
|
|
|
|
buffer.copy(b, offset, b_pin)
|
|
offset = 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 next, labels do
|
|
buffer.writeu32(b, offset, label)
|
|
offset = 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 next, names do
|
|
buffer_size = 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 next, names do
|
|
buffer.writeu32(b, offset, #name)
|
|
offset = offset + 4
|
|
end
|
|
for _, name in next, names do
|
|
buffer.writestring(b, offset, name)
|
|
offset = 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 next, 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 = 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 next, userid_accesslist do
|
|
__writei64(b, offset, user_id)
|
|
offset = 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 next, TERRAIN_MATERIAL_COLORS do
|
|
local color = instance:GetMaterialColor(material)
|
|
for _, component in next, RGB_components do
|
|
buffer.writeu8(b, offset, math.floor(color[component] * 255)) -- ? math.floor seems unneeded but it makes it faster
|
|
offset = 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 next, instance:GetEmotes() do
|
|
emotes_data = emotes_data .. name .. "^" .. table.concat(ids, "^") .. "^\\"
|
|
end
|
|
return emotes_data
|
|
end,
|
|
EquippedEmotesDataInternal = function(instance)
|
|
local equipped_emotes_data = ""
|
|
for _, emote in next, instance:GetEquippedEmotes() do
|
|
equipped_emotes_data = 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 next, collision_groups do
|
|
buffer_size = 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 next, collision_groups do
|
|
local name, id, mask = group.name, i - 1, group.mask
|
|
local name_len = #name
|
|
|
|
buffer.writeu8(b, offset, id) -- ID
|
|
offset = offset + 1
|
|
|
|
buffer.writeu8(b, offset, TypeID_int32) -- ? Type ID for int32 (0x04)
|
|
offset = offset + 1
|
|
|
|
buffer.writei32(b, offset, mask) -- Mask value as signed 32-bit integer
|
|
offset = offset + 4
|
|
|
|
buffer.writeu8(b, offset, name_len) -- Name length
|
|
offset = offset + 1
|
|
buffer.writestring(b, offset, name) -- Name
|
|
offset = offset + name_len
|
|
end
|
|
|
|
return buffer.tostring(b)
|
|
end,
|
|
},
|
|
}
|
|
for _, enum_item in next, 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 next, 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 next, API_Dump_Decoded do
|
|
local ClassName = API_Class.Name
|
|
local props = {}
|
|
|
|
for _, Member in next, 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 next, 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 next, API_Class.Members do
|
|
local __DARKLUA_CONTINUE_42 = false
|
|
repeat -- ? 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
|
|
__DARKLUA_CONTINUE_42 = true
|
|
break
|
|
end
|
|
|
|
local Special, PreferredDescriptorName
|
|
|
|
if MemberTags then
|
|
for _, tag in next, 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 = ClassProperties_size + 1
|
|
|
|
-- end
|
|
end
|
|
end
|
|
end
|
|
__DARKLUA_CONTINUE_42 = true
|
|
until true
|
|
if not __DARKLUA_CONTINUE_42 then
|
|
break
|
|
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 next, 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 next, 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 next, 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 next, 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 = 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
|
|
next,
|
|
{
|
|
"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 = 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 next, 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
|
|
next,
|
|
{
|
|
"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 next, 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 next, 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
|
|
next,
|
|
{
|
|
"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 = spinner_count + 1
|
|
|
|
if chars_size < spinner_count then
|
|
spinner_count = 1
|
|
end
|
|
|
|
return chars[spinner_count]
|
|
end
|
|
|
|
text = 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 next, 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 next,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 = 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 = totalsize + savestr_len
|
|
currentsize = 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 next, 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 = Item .. ReturnProperty(tag, propertyName, value)
|
|
end
|
|
end
|
|
Item = Item .. "</Properties>"
|
|
return Item
|
|
end
|
|
|
|
local function save_hierarchy(hierarchy)
|
|
for _, instance in next, hierarchy do
|
|
local __DARKLUA_CONTINUE_62 = false
|
|
repeat
|
|
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
|
|
__DARKLUA_CONTINUE_62 = true
|
|
break
|
|
end
|
|
|
|
SkipEntirely = IgnoreList[instance]
|
|
if SkipEntirely then
|
|
__DARKLUA_CONTINUE_62 = true
|
|
break
|
|
end
|
|
|
|
do
|
|
local OnIgnoredList = IgnoreList[ClassName]
|
|
if OnIgnoredList and (OnIgnoredList == true or OnIgnoredList[InstanceName]) then
|
|
__DARKLUA_CONTINUE_62 = true
|
|
break
|
|
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
|
|
__DARKLUA_CONTINUE_62 = true
|
|
break -- 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 = 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 = 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 next, GetInheritedProps(ClassNameOverride or ClassName) do
|
|
local __DARKLUA_CONTINUE_63 = false
|
|
repeat
|
|
local PropertyName = Property.Name
|
|
|
|
if IgnoreProperties[PropertyName] then
|
|
__DARKLUA_CONTINUE_63 = true
|
|
break
|
|
end
|
|
|
|
local ValueType = Property.ValueType
|
|
|
|
if IgnoreSharedStrings and ValueType == "SharedString" then -- ? More info in Options
|
|
__DARKLUA_CONTINUE_63 = true
|
|
break
|
|
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
|
|
__DARKLUA_CONTINUE_63 = true
|
|
break
|
|
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
|
|
__DARKLUA_CONTINUE_63 = true
|
|
break
|
|
end
|
|
end
|
|
|
|
if raw == __BREAK then
|
|
__DARKLUA_CONTINUE_63 = true
|
|
break
|
|
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
|
|
__DARKLUA_CONTINUE_63 = true
|
|
break
|
|
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
|
|
__DARKLUA_CONTINUE_63 = true
|
|
break
|
|
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
|
|
__DARKLUA_CONTINUE_63 = true -- * It can be empty, because it's optional
|
|
-- ? Though why even save it if it's empty considering it's optional
|
|
break
|
|
-- 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 = 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
|
|
__DARKLUA_CONTINUE_63 = true
|
|
until true
|
|
if not __DARKLUA_CONTINUE_63 then
|
|
break
|
|
end
|
|
end
|
|
end
|
|
savebuffer[savebuffer_size] = "</Properties>"
|
|
savebuffer_size = 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 = savebuffer_size + 1
|
|
__DARKLUA_CONTINUE_62 = true
|
|
until true
|
|
if not __DARKLUA_CONTINUE_62 then
|
|
break
|
|
end
|
|
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 = savebuffer_size + 1
|
|
if hierarchy then
|
|
save_hierarchy(hierarchy)
|
|
end
|
|
savebuffer[savebuffer_size] = "</Item>"
|
|
savebuffer_size = 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 = 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 next, 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 = 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 next, 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 = savebuffer_size + 1
|
|
savebuffer[savebuffer_size] = "</SharedStrings>"
|
|
savebuffer_size = savebuffer_size + 1
|
|
end
|
|
end
|
|
|
|
savebuffer[savebuffer_size] =
|
|
"</roblox><!-- Saved by UniversalSynSaveInstance (Join to Copy Games) https://discord.gg/wx4ThpAsmw -->"
|
|
savebuffer_size = 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 next, 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 next, chunks do
|
|
totalstr = 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 next, 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 = currentlen + 1
|
|
|
|
if i ~= length then
|
|
task.wait()
|
|
end
|
|
end
|
|
end
|
|
else
|
|
local totalstr = header
|
|
for _, chunk in next, chunks do
|
|
totalstr = 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 next, 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 next, children do
|
|
construct_InstanceOverride(child)
|
|
end
|
|
end
|
|
construct_InstanceOverride(PlayerScripts)
|
|
|
|
InstancesOverrides[LocalPlayer] = {
|
|
__Children = LocalPlayer:GetChildren(),
|
|
Properties = { Name = "[" .. LocalPlayer.ClassName .. "] " .. LocalPlayer.Name },
|
|
}
|
|
end
|
|
|
|
LocalPlayer:Kick("\n[SAFEMODE] Saving in Progress..\nPlease do NOT leave")
|
|
wait_for_render()
|
|
task.delay(10, service.GuiService.ClearError, service.GuiService)
|
|
end)
|
|
|
|
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 next, 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 next, 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
|