Roblox-File-Format/Plugins/GenerateApiDump/init.server.lua

852 lines
18 KiB
Lua
Raw Normal View History

2021-02-22 17:41:55 +00:00
local Selection = game:GetService("Selection")
local HttpService = game:GetService("HttpService")
local StarterPlayer = game:GetService("StarterPlayer")
local StudioService = game:GetService("StudioService")
local TextChatService = game:GetService("TextChatService")
local classes = {}
local outStream = ""
local stackLevel = 0
local singletons = {
Speaker = Instance.new("Sound"), -- close enough
Terrain = workspace:WaitForChild("Terrain", 1000),
ParabolaAdornment = Instance.new("BoxHandleAdornment"), -- close enough
StarterPlayerScripts = StarterPlayer:WaitForChild("StarterPlayerScripts"),
StarterCharacterScripts = StarterPlayer:WaitForChild("StarterCharacterScripts"),
2022-12-27 12:36:50 +00:00
BubbleChatConfiguration = TextChatService:WaitForChild("BubbleChatConfiguration", 10),
ChatWindowConfiguration = TextChatService:WaitForChild("ChatWindowConfiguration", 10),
ChatInputBarConfiguration = TextChatService:WaitForChild("ChatInputBarConfiguration", 10),
}
local exceptionClasses = {
PackageLink = true,
ScriptDebugger = true,
ChatWindowConfiguration = true,
ChatInputBarConfiguration = true,
2021-08-09 17:22:26 +00:00
}
local numberTypes = {
int = true,
long = true,
int64 = true,
float = true,
double = true,
2020-06-30 00:06:14 +00:00
}
local stringTypes = {
string = true,
Content = true,
BinaryString = true,
ProtectedString = true,
2020-06-30 00:06:14 +00:00
}
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("{")
2021-04-02 06:10:34 +00:00
stackLevel += 1
end
local function closeStack()
2021-04-02 06:10:34 +00:00
stackLevel -= 1
writeLine("}")
end
local function clearStream()
stackLevel = 0
outStream = ""
end
local function exportStream(label)
local results = outStream:gsub("\n\n\n", "\n\n")
2021-04-15 23:40:16 +00:00
local export
if plugin then
2021-04-15 23:40:16 +00:00
export = Instance.new("Script")
export.Archivable = false
export.Source = results
export.Name = label
2020-12-06 22:04:49 +00:00
export.Parent = workspace
2021-02-22 17:41:55 +00:00
Selection:Add({ export })
end
if isCoreScript then
StudioService:CopyToClipboard(results)
elseif not plugin then
warn(label)
print(results)
2021-04-15 23:40:16 +00:00
else
wait()
plugin:OpenScript(export)
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
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Formatting
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------
local formatting: Format = require(script.Formatting)
2022-06-28 20:21:05 +00:00
type FormatFunc = formatting.FormatFunc
type Format = formatting.Format
local formatLinks = {
["int"] = "Int",
["long"] = "Int",
["float"] = "Float",
["byte[]"] = "Bytes",
["double"] = "Double",
["boolean"] = "Bool",
["string"] = "String",
["Content"] = "String",
["Color3uint8"] = "Color3",
["ProtectedString"] = "String",
2023-07-21 06:21:48 +00:00
["OptionalCoordinateFrame"] = "Optional<CFrame>",
}
2022-06-28 20:21:05 +00:00
local function getFormatFunction(valueType: string): FormatFunc
if not formatting[valueType] then
valueType = formatLinks[valueType]
end
2022-06-28 20:21:05 +00:00
return formatting[valueType] or formatting.Null
end
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Property Patches
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------
2019-12-23 03:23:06 +00:00
local patches = require(script.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
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------
2022-06-28 20:21:05 +00:00
local baseUrl = "https://raw.githubusercontent.com/MaximumADHD/Roblox-Client-Tracker/roblox/"
2020-08-14 17:35:27 +00:00
local toolbar, button
if plugin then
toolbar = plugin:CreateToolbar("C# API Dump")
2020-08-14 17:35:27 +00:00
button = toolbar:CreateButton(
"Dump API",
"Generates a C# dump of Roblox's Class/Enum API.",
"rbxasset://textures/Icon_Stream_Off@2x.png"
)
2020-08-14 17:35:27 +00:00
button.ClickableWhenViewportHidden = true
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()
2020-06-30 00:06:14 +00:00
local env = getfenv()
local version = getAsync(baseUrl .. "version.txt")
2023-07-21 06:21:48 +00:00
local apiDump = getAsync(baseUrl .. "Full-API-Dump.json")
apiDump = HttpService:JSONDecode(apiDump)
local classNames = {}
classes = {}
2020-08-14 17:35:27 +00:00
local enumMap = {
Axis = true,
2022-12-16 20:22:34 +00:00
Font = true,
FontSize = true,
FontStyle = true,
FontWeight = true,
2023-07-21 06:21:48 +00:00
AdPortalType = true,
2020-08-14 17:35:27 +00:00
}
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()
2021-02-22 17:41:55 +00:00
local dumpFolder = game:FindFirstChild("DumpFolder")
class.Object = Instance.new(className)
2021-02-22 17:41:55 +00:00
if dumpFolder then
local old = dumpFolder:FindFirstChildOfClass(className)
if old then
old:Destroy()
end
class.Object.Name = className
2021-02-22 17:41:55 +00:00
class.Object.Parent = 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()
2020-08-14 17:35:27 +00:00
writeLine("#pragma warning disable IDE1006 // Naming Styles")
2023-07-21 06:21:48 +00:00
writeLine("#pragma warning disable CS0612 // Type or member is obsolete")
writeLine()
writeLine("namespace RobloxFiles")
openStack()
2021-04-02 06:10:34 +00:00
for i, className in ipairs(classNames) do
local class = classes[className]
local classTags = getTags(class)
local registerClass = canCreateClass(class)
2021-04-02 06:10:34 +00:00
local object = class.Object
if class.Inherited then
registerClass = true
end
if class.Name == "Instance" or class.Name == "Studio" then
registerClass = false
end
2021-04-02 06:10:34 +00:00
local noSecurityCheck = pcall(function()
2021-04-02 06:10:34 +00:00
if not classTags.Service then
return tostring(object)
end
2022-12-16 20:22:34 +00:00
return nil
2021-04-02 06:10:34 +00:00
end)
if not noSecurityCheck then
object = nil
end
if not object then
if class.Inherited then
object = class.Inherited.Object
elseif singletons[className] then
object = singletons[className]
else
registerClass = false
end
end
2021-08-09 17:22:26 +00:00
if exceptionClasses[class.Name] then
registerClass = true
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 = {}
2023-07-21 06:21:48 +00:00
--[[
for _, propName in pairs(classPatches.Remove) do
propMap[propName] = nil
end
2023-07-21 06:21:48 +00:00
]]
for propName in pairs(propMap) do
table.insert(propNames, propName)
end
2023-07-21 06:21:48 +00:00
--[[
for propName, propType in pairs(classPatches.Add) do
2020-07-06 00:04:20 +00:00
local prop = propMap[propName]
2020-07-06 00:04:20 +00:00
if prop then
local serial = prop.Serialization
serial.CanSave = true
serial.CanLoad = true
else
propMap[propName] = createProperty(propName, propType)
table.insert(propNames, propName)
end
end
2023-07-21 06:21:48 +00:00
]]
local firstLine = true
2022-06-30 21:18:00 +00:00
class.PropertyMap = propMap
2022-06-30 21:18:00 +00:00
local ancestor = class
local diffProps = {}
2022-06-30 21:18:00 +00:00
while object do
ancestor = classes[ancestor.Superclass]
if not ancestor then
break
end
local inheritProps = ancestor.PropertyMap
local inherited = ancestor.Inherited
local baseObject = if inherited then inherited.Object else nil
2022-06-30 21:18:00 +00:00
if inheritProps and baseObject then
for name, prop in pairs(inheritProps) do
local tags = getTags(prop)
2022-06-30 21:18:00 +00:00
if tags.ReadOnly then
continue
end
local gotPropValue, propValue = pcall(function()
2022-06-30 21:18:00 +00:00
return object[name]
end)
local gotBaseValue, baseValue = pcall(function()
2022-06-30 21:18:00 +00:00
return baseObject[name]
end)
2022-06-30 21:18:00 +00:00
if gotBaseValue and gotPropValue then
if propValue ~= baseValue then
diffProps[name] = propValue
end
end
end
end
end
2022-06-30 21:18:00 +00:00
if classTags.Service or next(diffProps) then
local headerFormat = "public %s()"
if next(diffProps) then
headerFormat ..= " : base()"
end
writeLine(headerFormat, className)
openStack()
2022-06-30 21:18:00 +00:00
if classTags.Service then
writeLine("IsService = true;")
2022-06-30 21:18:00 +00:00
if next(diffProps) then
writeLine()
end
end
if next(diffProps) then
local diffNames = {}
for name in pairs(diffProps) do
table.insert(diffNames, name)
end
table.sort(diffNames)
for i, name in ipairs(diffNames) do
2022-12-16 20:22:34 +00:00
if redirectProps[name] then
continue
end
2022-06-30 21:18:00 +00:00
local value = diffProps[name]
local valueType = typeof(value)
local formatFunc = getFormatFunction(valueType)
2022-06-30 21:18:00 +00:00
if formatFunc ~= formatting.Null then
local result = formatFunc(value)
if result == "" then
result = tostring(value)
end
writeLine("%s = %s;", name, result)
end
end
end
closeStack()
end
2022-06-30 21:18:00 +00:00
table.sort(propNames)
2021-04-02 06:10:34 +00:00
for j, propName in ipairs(propNames) do
local prop = propMap[propName]
local propTags = getTags(prop)
local serial = prop.Serialization
local typeData = prop.ValueType
local category = typeData.Category
local valueType = typeData.Name
local redirect = redirectProps[propName]
local couldSave = (serial.CanSave or propTags.Deprecated or redirect)
if serial.CanLoad and couldSave then
2022-06-30 21:18:00 +00:00
if firstLine and (classTags.Service or next(diffProps)) then
writeLine()
end
local name = propName
local default = ""
if propName == className then
2023-07-21 06:21:48 +00:00
name ..= "_"
end
if name:find(" ") then
name = name:gsub(" ", "_")
end
if valueType == "int64" then
valueType = "long"
elseif valueType == "BinaryString" then
valueType = "byte[]"
elseif valueType == "Font" and category ~= "Enum" then
valueType = "FontFace"
2023-07-21 06:21:48 +00:00
elseif valueType == "OptionalCoordinateFrame" then
valueType = "Optional<CFrame>"
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, flag
if typeof(redirect) == "string" then
get = redirect
set = redirect .. " = value"
if redirect == "value" then
set = "this." .. set
end
else
get = redirect.Get
set = redirect.Set
flag = redirect.Flag
end
2019-10-24 20:17:36 +00:00
if not firstLine and set then
writeLine()
end
if propTags.Deprecated then
writeLine("[Obsolete]")
end
2019-10-24 20:17:36 +00:00
if set then
if flag then
writeLine("public %s %s %s", flag, valueType, name)
else
writeLine("public %s %s", valueType, name)
end
2019-10-24 20:17:36 +00:00
openStack()
2020-09-25 00:38:30 +00:00
writeLine("get => %s;", get)
if set:find("\n") then
writeLine()
writeLine("set")
openStack()
for line in set:gmatch("[^\r\n]+") do
writeLine(line)
end
closeStack()
else
2020-09-25 00:38:30 +00:00
writeLine("set => %s;", set)
end
2019-10-24 20:17:36 +00:00
closeStack()
else
writeLine("public %s %s => %s;", valueType, name, get)
end
2021-04-02 06:10:34 +00:00
if j ~= #propNames and set 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
2020-06-30 00:06:14 +00:00
if not gotValue and category ~= "Class" then
-- Fallback to implicit defaults
if numberTypes[valueType] then
2020-06-30 00:06:14 +00:00
value = 0
gotValue = true
elseif stringTypes[valueType] then
2020-06-30 00:06:14 +00:00
value = ""
gotValue = true
elseif valueType == "SharedString" then
2021-02-22 17:41:55 +00:00
value = "yuZpQdnvvUBOTYh1jqZ2cA=="
gotValue = true
2020-06-30 00:06:14 +00:00
elseif category == "DataType" then
local DataType = env[valueType]
if DataType and typeof(DataType) == "table" and not rawget(env, valueType) then
pcall(function()
2020-06-30 00:06:14 +00:00
value = DataType.new()
gotValue = true
end)
end
2021-06-05 22:22:53 +00:00
elseif category == "Enum" then
2022-12-16 20:22:34 +00:00
local enum = (Enum :: any)[valueType]
2021-06-05 22:22:53 +00:00
local lowestId = math.huge
local lowest
for _, item in pairs(enum:GetEnumItems()) do
2021-06-05 22:22:53 +00:00
local itemValue = item.Value
if itemValue < lowestId then
lowest = item
lowestId = itemValue
end
end
if lowest then
value = lowest
gotValue = true
end
2020-06-30 00:06:14 +00:00
end
local id = string.format("%s.%s", className, propName)
local src = string.format("[%s]", script.Parent:GetFullName())
if gotValue then
warn(src, "Fell back to implicit value for property:", id)
else
warn(
src,
"!! Could not figure out default value for property:",
id,
"value error was:",
value
)
2020-06-30 00:06:14 +00:00
end
end
if gotValue then
local formatKey = if category == "Enum" then "Enum" else valueType
local formatFunc = getFormatFunction(formatKey)
2022-06-28 20:21:05 +00:00
if formatFunc == formatting.Null then
local literal = typeof(value)
formatFunc = getFormatFunction(literal)
end
local result
if formatFunc then
if typeof(formatFunc) == "string" then
result = formatFunc
else
result = formatFunc(value)
end
end
2022-06-28 20:21:05 +00:00
if result == "" then
result = nil
end
2020-08-14 17:35:27 +00:00
if result ~= nil then
default = " = " .. result
end
if formatFunc == formatting.EnumItem then
local enumName = tostring(value.EnumType)
enumMap[enumName] = true
end
end
2021-03-09 22:56:27 +00:00
if className == "Sound" and propName == "EmitterSize" then
-- .____.
propTags.Deprecated = false
end
if propTags.Deprecated then
if not firstLine then
writeLine()
end
writeLine("[Obsolete]")
end
2020-06-30 00:06:14 +00:00
writeLine("public %s %s%s;", valueType, name, default)
2021-04-02 06:10:34 +00:00
if propTags.Deprecated and j ~= #propNames then
writeLine()
end
end
firstLine = false
end
end
closeStack()
if i ~= #classNames then
writeLine()
end
end
end
closeStack()
exportStream("Classes")
2020-08-14 17:35:27 +00:00
return enumMap
end
2020-08-14 17:35:27 +00:00
local function generateEnums(whiteList)
local version = getfenv().version():gsub("%. ", ".")
clearStream()
writeLine("// Auto-generated list of Roblox enums.")
writeLine("// Updated as of %s", version)
writeLine()
2020-08-14 17:35:27 +00:00
writeLine("namespace RobloxFiles.Enums")
openStack()
local enums = Enum:GetEnums()
for i, enum in ipairs(enums) do
2020-08-14 17:35:27 +00:00
local enumName = tostring(enum)
if whiteList and not whiteList[enumName] then
continue
end
2020-08-14 17:35:27 +00:00
writeLine("public enum %s", enumName)
openStack()
local enumItems = enum:GetEnumItems()
local lastValue = -1
2020-08-14 17:35:27 +00:00
local mapped = {}
table.sort(enumItems, function(a, b)
return a.Value < b.Value
end)
2021-04-02 06:10:34 +00:00
for j, enumItem in ipairs(enumItems) do
local text = ""
local comma = ","
local name = enumItem.Name
local value = enumItem.Value
2020-08-14 17:35:27 +00:00
if not mapped[value] then
if (value - lastValue) ~= 1 then
text = " = " .. value
2020-08-14 17:35:27 +00:00
end
2021-04-02 06:10:34 +00:00
if j == #enumItems then
2020-08-14 17:35:27 +00:00
comma = ""
end
2020-08-14 17:35:27 +00:00
lastValue = value
mapped[value] = true
writeLine("%s%s%s", name, text, comma)
end
end
closeStack()
if i ~= #enums then
writeLine()
end
end
closeStack()
exportStream("Enums")
end
2020-08-14 17:35:27 +00:00
local function generateAll()
local enumList = generateClasses()
generateEnums(enumList)
end
if plugin then
2020-08-14 17:35:27 +00:00
button.Click:Connect(generateAll)
else
2020-08-14 17:35:27 +00:00
generateAll()
2021-04-15 23:40:16 +00:00
end
if game.Name:sub(1, 9) == "Null.rbxl" then
generateAll()
end