de8df15d3f
Instance classes are now strongly typed with real property fields that are derived from the JSON API Dump! This required a lot of reworking across the board: - Classes and Enums are auto-generated in the 'Generated' folder now. This is done using a custom built-in plugin, which can be found in the Plugins folder of this project. - Property objects are now tied to .NET's reflection system. Reading and writing from them will try to redirect into a field of the Instance they are bound to. - Property types that were loosely defined now have proper data types (such as Color3uint8, Content, ProtectedString, SharedString, etc) - Fixed an error with the CFrame directional vectors. - The binary PRNT chunk now writes instances in child->parent order. - Enums are now generated correctly, with up-to-date values. - INST chunks are now referred to as 'Classes' instead of 'Types'. - Unary operator added to Vector2 and Vector3. - CollectionService tags can now be manipulated per-instance using the Instance.Tags member. - The Instance.Archivable property now works correctly. - XML files now save/load metadata correctly. - Cleaned up the property tokens directory. I probably missed a few things, but that's a general overview of everything that changed.
607 lines
13 KiB
Lua
607 lines
13 KiB
Lua
local HttpService = game:GetService("HttpService")
|
|
local ServerStorage = game:GetService("ServerStorage")
|
|
local StarterPlayer = game:GetService("StarterPlayer")
|
|
local StudioService = game:GetService("StudioService")
|
|
|
|
local classes = {}
|
|
local outStream = ""
|
|
local stackLevel = 0
|
|
|
|
local singletons =
|
|
{
|
|
Terrain = workspace:WaitForChild("Terrain");
|
|
StarterPlayerScripts = StarterPlayer:WaitForChild("StarterPlayerScripts");
|
|
StarterCharacterScripts = StarterPlayer:WaitForChild("StarterCharacterScripts");
|
|
}
|
|
|
|
local isCoreScript = pcall(function ()
|
|
local restricted = game:GetService("RobloxPluginGuiService")
|
|
return tostring(restricted)
|
|
end)
|
|
|
|
local function write(formatString, ...)
|
|
local tabs = string.rep(' ', stackLevel * 4)
|
|
local fmt = formatString or ""
|
|
|
|
local value = tabs .. fmt:format(...)
|
|
outStream = outStream .. value
|
|
end
|
|
|
|
local function writeLine(formatString, ...)
|
|
if not formatString then
|
|
outStream = outStream .. '\n'
|
|
return
|
|
end
|
|
|
|
write(formatString .. '\n', ...)
|
|
end
|
|
|
|
local function openStack()
|
|
writeLine('{')
|
|
stackLevel = stackLevel + 1
|
|
end
|
|
|
|
local function closeStack()
|
|
stackLevel = stackLevel - 1
|
|
writeLine('}')
|
|
end
|
|
|
|
local function clearStream()
|
|
stackLevel = 0
|
|
outStream = ""
|
|
end
|
|
|
|
local function exportStream(label)
|
|
local results = outStream:gsub("\n\n\n", "\n\n")
|
|
|
|
if plugin then
|
|
local export = Instance.new("Script")
|
|
export.Archivable = false
|
|
export.Source = results
|
|
export.Name = label
|
|
|
|
plugin:OpenScript(export)
|
|
end
|
|
|
|
if isCoreScript then
|
|
StudioService:CopyToClipboard(results)
|
|
elseif not plugin then
|
|
warn(label)
|
|
print(results)
|
|
end
|
|
end
|
|
|
|
local function getTags(object)
|
|
local tags = {}
|
|
|
|
if object.Tags ~= nil then
|
|
for _,tag in pairs(object.Tags) do
|
|
tags[tag] = true
|
|
end
|
|
end
|
|
|
|
if object.Name == "Terrain" then
|
|
tags.NotCreatable = nil
|
|
end
|
|
|
|
return tags
|
|
end
|
|
|
|
local function upcastInheritance(class, root)
|
|
local superClass = classes[class.Superclass]
|
|
|
|
if not superClass then
|
|
return
|
|
end
|
|
|
|
if not root then
|
|
root = class
|
|
end
|
|
|
|
if not superClass.Inherited then
|
|
superClass.Inherited = root
|
|
end
|
|
|
|
upcastInheritance(superClass, root)
|
|
end
|
|
|
|
local function canCreateClass(class)
|
|
local tags = getTags(class)
|
|
local canCreate = true
|
|
|
|
if tags.NotCreatable then
|
|
canCreate = false
|
|
end
|
|
|
|
if tags.Service then
|
|
canCreate = true
|
|
end
|
|
|
|
if tags.Settings then
|
|
canCreate = false
|
|
end
|
|
|
|
if singletons[class.Name] then
|
|
canCreate = true
|
|
end
|
|
|
|
return canCreate
|
|
end
|
|
|
|
local function collectProperties(class)
|
|
local propMap = {}
|
|
|
|
for _,member in ipairs(class.Members) do
|
|
if member.MemberType == "Property" then
|
|
local propName = member.Name
|
|
propMap[propName] = member
|
|
end
|
|
end
|
|
|
|
return propMap
|
|
end
|
|
|
|
local function createProperty(propName, propType)
|
|
local category = "DataType";
|
|
local name = propType
|
|
|
|
if propType:find(':') then
|
|
local data = string.split(propType, ':')
|
|
category = data[1]
|
|
name = data[2]
|
|
end
|
|
|
|
return
|
|
{
|
|
Name = propName;
|
|
|
|
Serialization =
|
|
{
|
|
CanSave = true;
|
|
CanLoad = true;
|
|
};
|
|
|
|
ValueType =
|
|
{
|
|
Category = category;
|
|
Name = name;
|
|
};
|
|
|
|
Security = "None";
|
|
}
|
|
end
|
|
|
|
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
|
-- Formatting
|
|
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
|
|
|
local formatting = require(script.Parent.Formatting)
|
|
|
|
local formatLinks =
|
|
{
|
|
["int"] = "Int";
|
|
["nil"] = "Null";
|
|
["long"] = "Int";
|
|
|
|
["float"] = "Float";
|
|
["byte[]"] = "Bytes";
|
|
["double"] = "Double";
|
|
|
|
["string"] = "String";
|
|
["Content"] = "String";
|
|
["Instance"] = "Null";
|
|
|
|
["Color3uint8"] = "Color3";
|
|
["ProtectedString"] = "String";
|
|
}
|
|
|
|
local function getFormatFunction(valueType)
|
|
if not formatting[valueType] then
|
|
valueType = formatLinks[valueType]
|
|
end
|
|
|
|
return formatting[valueType]
|
|
end
|
|
|
|
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
|
-- Property Patches
|
|
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
|
|
|
local patches = require(script.Parent.PropertyPatches)
|
|
local patchIndex = {}
|
|
|
|
function patchIndex:__index(key)
|
|
if not rawget(self, key) then
|
|
rawset(self, key, {})
|
|
end
|
|
|
|
return self[key]
|
|
end
|
|
|
|
local function getPatches(className)
|
|
local classPatches = patches[className]
|
|
return setmetatable(classPatches, patchIndex)
|
|
end
|
|
|
|
setmetatable(patches, patchIndex)
|
|
|
|
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
|
-- Main
|
|
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
|
|
|
local baseUrl = "https://raw.githubusercontent.com/CloneTrooper1019/Roblox-Client-Tracker/roblox/"
|
|
local toolbar, classButton, enumButton
|
|
|
|
if plugin then
|
|
toolbar = plugin:CreateToolbar("C# API Dump")
|
|
|
|
classButton = toolbar:CreateButton(
|
|
"Dump Classes",
|
|
"Generates a C# dump of Roblox's Class API.",
|
|
"rbxasset://textures/Icon_Stream_Off@2x.png"
|
|
)
|
|
|
|
enumButton = toolbar:CreateButton(
|
|
"Dump Enums",
|
|
"Generates a C# dump of Roblox's Enum API.",
|
|
"rbxasset://textures/Icon_Stream_Off@2x.png"
|
|
)
|
|
end
|
|
|
|
local function getAsync(url)
|
|
local enabled
|
|
|
|
if isCoreScript then
|
|
enabled = HttpService:GetHttpEnabled()
|
|
HttpService:SetHttpEnabled(true)
|
|
end
|
|
|
|
local result = HttpService:GetAsync(url)
|
|
|
|
if isCoreScript then
|
|
HttpService:SetHttpEnabled(enabled)
|
|
end
|
|
|
|
return result
|
|
end
|
|
|
|
local function generateClasses()
|
|
local version = getAsync(baseUrl .. "version.txt")
|
|
|
|
local apiDump = getAsync(baseUrl .. "API-Dump.json")
|
|
apiDump = HttpService:JSONDecode(apiDump)
|
|
|
|
local classNames = {}
|
|
classes = {}
|
|
|
|
for _,class in ipairs(apiDump.Classes) do
|
|
local className = class.Name
|
|
local superClass = classes[class.Superclass]
|
|
|
|
if singletons[className] then
|
|
class.Singleton = true
|
|
class.Object = singletons[className]
|
|
end
|
|
|
|
if superClass and canCreateClass(class) then
|
|
local classTags = getTags(class)
|
|
|
|
if classTags.Service then
|
|
pcall(function ()
|
|
if not className:find("Network") then
|
|
class.Object = game:GetService(className)
|
|
end
|
|
end)
|
|
elseif not classTags.NotCreatable then
|
|
pcall(function ()
|
|
class.Object = Instance.new(className)
|
|
|
|
if ServerStorage:FindFirstChild("DumpFolder") then
|
|
class.Object.Name = className
|
|
class.Object.Parent = ServerStorage.DumpFolder
|
|
end
|
|
end)
|
|
end
|
|
|
|
upcastInheritance(class)
|
|
end
|
|
|
|
classes[className] = class
|
|
table.insert(classNames, className)
|
|
end
|
|
|
|
outStream = ""
|
|
|
|
writeLine("// Auto-generated list of creatable Roblox classes.")
|
|
writeLine("// Updated as of %s", version)
|
|
writeLine()
|
|
|
|
writeLine("using System;")
|
|
writeLine()
|
|
|
|
writeLine("using RobloxFiles.DataTypes;")
|
|
writeLine("using RobloxFiles.Enums;")
|
|
writeLine("using RobloxFiles.Utility;")
|
|
writeLine()
|
|
|
|
writeLine("namespace RobloxFiles")
|
|
openStack()
|
|
|
|
for i,className in ipairs(classNames) do
|
|
local class = classes[className]
|
|
local classTags = getTags(class)
|
|
|
|
local registerClass = canCreateClass(class)
|
|
|
|
if class.Inherited then
|
|
registerClass = true
|
|
end
|
|
|
|
if class.Name == "Instance" or class.Name == "Studio" then
|
|
registerClass = false
|
|
end
|
|
|
|
local object = class.Object
|
|
|
|
if not object then
|
|
if class.Inherited then
|
|
object = class.Inherited.Object
|
|
elseif singletons[className] then
|
|
object = singletons[className]
|
|
else
|
|
registerClass = false
|
|
end
|
|
end
|
|
|
|
if registerClass then
|
|
local objectType
|
|
|
|
if classTags.NotCreatable and class.Inherited and not class.Singleton then
|
|
objectType = "abstract class"
|
|
else
|
|
objectType = "class"
|
|
end
|
|
|
|
writeLine("public %s %s : %s", objectType, className, class.Superclass)
|
|
openStack()
|
|
|
|
local classPatches = getPatches(className)
|
|
local redirectProps = classPatches.Redirect
|
|
|
|
local propMap = collectProperties(class)
|
|
local propNames = {}
|
|
|
|
for _,propName in pairs(classPatches.Remove) do
|
|
propMap[propName] = nil
|
|
end
|
|
|
|
for propName in pairs(propMap) do
|
|
table.insert(propNames, propName)
|
|
end
|
|
|
|
for propName, propType in pairs(classPatches.Add) do
|
|
if not propMap[propName] then
|
|
propMap[propName] = createProperty(propName, propType)
|
|
table.insert(propNames, propName)
|
|
else
|
|
propMap[propName].Serialization.CanLoad = true
|
|
end
|
|
end
|
|
|
|
local firstLine = true
|
|
table.sort(propNames)
|
|
|
|
|
|
if classTags.Service then
|
|
writeLine("public %s()", className)
|
|
openStack()
|
|
|
|
writeLine("IsService = true;")
|
|
closeStack()
|
|
|
|
if #propNames > 0 then
|
|
writeLine()
|
|
end
|
|
end
|
|
|
|
for i, propName in ipairs(propNames) do
|
|
local prop = propMap[propName]
|
|
|
|
local serial = prop.Serialization
|
|
local valueType = prop.ValueType.Name
|
|
|
|
if serial.CanLoad then
|
|
local propTags = getTags(prop)
|
|
|
|
local redirect = redirectProps[propName]
|
|
local name = propName
|
|
local default = ""
|
|
|
|
if propName == className then
|
|
name = name .. '_'
|
|
end
|
|
|
|
if valueType == "int64" then
|
|
valueType = "long"
|
|
elseif valueType == "BinaryString" then
|
|
valueType = "byte[]"
|
|
end
|
|
|
|
local first = name:sub(1, 1)
|
|
|
|
if first == first:lower() then
|
|
local pascal = first:upper() .. name:sub(2)
|
|
if propMap[pascal] ~= nil and propTags.Deprecated then
|
|
redirect = pascal
|
|
end
|
|
end
|
|
|
|
if redirect then
|
|
local get, set
|
|
|
|
if typeof(redirect) == "string" then
|
|
get = redirect
|
|
set = redirect .. " = value"
|
|
else
|
|
get = redirect.Get
|
|
set = redirect.Set
|
|
end
|
|
|
|
if not firstLine then
|
|
writeLine()
|
|
end
|
|
|
|
if propTags.Deprecated then
|
|
writeLine("[Obsolete]")
|
|
end
|
|
|
|
writeLine("public %s %s", valueType, name)
|
|
|
|
openStack()
|
|
writeLine("get { return %s; }", get)
|
|
writeLine("set { %s; }", set)
|
|
closeStack()
|
|
|
|
if (i ~= #propNames) then
|
|
writeLine()
|
|
end
|
|
else
|
|
local value = classPatches.Defaults[propName]
|
|
local gotValue = (value ~= nil)
|
|
|
|
if not gotValue then
|
|
gotValue, value = pcall(function ()
|
|
return object[propName]
|
|
end)
|
|
end
|
|
|
|
local comment = " // Default missing!"
|
|
local category = prop.ValueType.Category
|
|
|
|
if gotValue then
|
|
local category = prop.ValueType.Category
|
|
local formatFunc = getFormatFunction(valueType)
|
|
|
|
if not formatFunc then
|
|
local literal = typeof(value)
|
|
formatFunc = getFormatFunction(literal)
|
|
end
|
|
|
|
if not formatFunc then
|
|
formatFunc = tostring
|
|
end
|
|
|
|
local result
|
|
|
|
if typeof(formatFunc) == "string" then
|
|
result = formatFunc
|
|
else
|
|
result = formatFunc(value)
|
|
end
|
|
|
|
if not serial.CanSave and not propTags.Deprecated then
|
|
comment = " // [Load-only]"
|
|
else
|
|
comment = ""
|
|
end
|
|
|
|
default = " = " .. result
|
|
end
|
|
|
|
if propTags.Deprecated then
|
|
if not firstLine then
|
|
writeLine()
|
|
end
|
|
|
|
writeLine("[Obsolete]")
|
|
end
|
|
|
|
if category == "Class" then
|
|
default = " = null"
|
|
comment = ""
|
|
end
|
|
|
|
writeLine("public %s %s%s;%s", valueType, name, default, comment)
|
|
|
|
if propTags.Deprecated and i ~= #propNames then
|
|
writeLine()
|
|
end
|
|
end
|
|
|
|
firstLine = false
|
|
end
|
|
end
|
|
|
|
closeStack()
|
|
|
|
if (i ~= #classNames) then
|
|
writeLine()
|
|
end
|
|
end
|
|
end
|
|
|
|
closeStack()
|
|
exportStream("Classes")
|
|
end
|
|
|
|
local function generateEnums()
|
|
local version = getfenv().version():gsub("%. ", ".")
|
|
clearStream()
|
|
|
|
writeLine("// Auto-generated list of Roblox enums.")
|
|
writeLine("// Updated as of %s", version)
|
|
writeLine()
|
|
|
|
writeLine("namespace RobloxFiles.Enums")
|
|
openStack()
|
|
|
|
local enums = Enum:GetEnums()
|
|
|
|
for i, enum in ipairs(enums) do
|
|
writeLine("public enum %s", tostring(enum))
|
|
openStack()
|
|
|
|
local enumItems = enum:GetEnumItems()
|
|
local lastValue = -1
|
|
|
|
table.sort(enumItems, function (a, b)
|
|
return a.Value < b.Value
|
|
end)
|
|
|
|
for i, enumItem in ipairs(enumItems) do
|
|
local text = ""
|
|
local comma = ','
|
|
|
|
local name = enumItem.Name
|
|
local value = enumItem.Value
|
|
|
|
if (value - lastValue) ~= 1 then
|
|
text = " = " .. value;
|
|
end
|
|
|
|
if i == #enumItems then
|
|
comma = ""
|
|
end
|
|
|
|
lastValue = value
|
|
writeLine("%s%s%s", name, text, comma)
|
|
end
|
|
|
|
closeStack()
|
|
|
|
if i ~= #enums then
|
|
writeLine()
|
|
end
|
|
end
|
|
|
|
closeStack()
|
|
exportStream("Enums")
|
|
end
|
|
|
|
if plugin then
|
|
classButton.Click:Connect(generateClasses)
|
|
enumButton.Click:Connect(generateEnums)
|
|
else
|
|
generateClasses()
|
|
generateEnums()
|
|
end |