Skip to content

Instantly share code, notes, and snippets.

@grilme99
Last active September 8, 2023 15:20
Show Gist options
  • Save grilme99/361574912f06384f490f3d02e9b17afe to your computer and use it in GitHub Desktop.
Save grilme99/361574912f06384f490f3d02e9b17afe to your computer and use it in GitHub Desktop.
Roblox elevated/internal plugin demoter
--[[
ELEVATED PLUGIN DEMOTER
"One-click" script to demote elevated (usually internal) plugins by removing or
replacing their elevated API calls.
NOTE:
I've only tested this on the "Developer" Storybook plugin (the irony). It will probably
need some modification for other internal plugins, like the Roact Inspector.
STEPS TO USE:
1. Find the plugin .rbxm file in Studio's BuiltInPlugins folder. Windows with Mod Manager:
%LOCALAPPDATA%\Roblox Studio\BuiltInPlugins
2. Drop into a Studio place (empy baseplate, doesn't matter) and select the top-level folder.
3. Copy this script into the command bar and run! Your plugin has now been demoted and you can
save it as a local plugin to use.
NOTE FOR THE STORYBOOK PLUGIN:
There is a restriction which prevents plugins from requiring normal modules. You should use the
Mod Manager to enable the "FFlagEnableLoadModule" flag so that the plugin can use debug.loadmodule
to bypass this restriction.
This script is mostly a fork of the "DeveloperFramework" (ironic, I know) Embed module, but with
things added on top for our needs.
Happy developing!
]]
local CONFIG = {
FallbackFlagValue = false,
SpoofInternal = true, -- You're gonna wanna leave this on for most plugins to run
}
local root = game:GetService("Selection"):Get()[1]
if (root == nil) then
warn("You must select the plugin before running this script.")
end
local pluginName = root.Name
local function rewriteMethodCall(name: string, value: any)
return string.format('select(2, "%s", %s)', name, tostring(value))
end
local function tryCall(fn: () -> any, default: any)
local ok, result = pcall(function()
return fn()
end)
if ok then
return result
else
return default
end
end
local function getFlag(flagName: string)
return tryCall(function()
return game:GetFastFlag(flagName)
end, CONFIG.FallbackFlagValue)
end
local function getFeature(featureName: string)
return tryCall(function()
return game:GetEngineFeature(featureName)
end, featureName)
end
local function getString(stringName: string)
return tryCall(function()
return game:GetFastString(stringName)
end, stringName)
end
local function getInt(intName: string)
return tryCall(function()
return game:GetFastInt(intName)
end, intName)
end
local function getFVariable(settingName: string)
return tryCall(function()
return settings():GetFVariable(settingName)
end, CONFIG.FallbackFlagValue)
end
local function rewriteFlag(flagName: string)
return rewriteMethodCall(flagName, getFlag(flagName))
end
local function rewriteEngineFeature(featureName: string)
return rewriteMethodCall(featureName, getFeature(featureName))
end
local function rewriteString(stringName: string)
return rewriteMethodCall(stringName, getString(stringName))
end
local function rewriteInt(intName: string)
return rewriteMethodCall(intName, getInt(intName))
end
local function rewriteSetting(settingName: string)
return rewriteMethodCall(settingName, getFVariable(settingName))
end
local function rewriteFlagsForScript(script: ModuleScript)
-- These API calls are roblox-security only, so cannot be used in a normal plugin
local src = script.Source
:gsub([[game:DefineFastFlag%(%s*"([^"]+)"[^)]*%)]], rewriteFlag)
:gsub([[game:DefineFastString%(%s*"([^"]+)"[^)]*%)]], rewriteString)
:gsub([[game:GetEngineFeature%(%s*"([^"]+)"[^)]*%)]], rewriteEngineFeature)
:gsub([[game:GetFastFlag%(%s*"([^"]+)"[^)]*%)]], rewriteFlag)
:gsub([[game:GetFastString%(%s*"([^"]+)"[^)]*%)]], rewriteString)
:gsub([[game:GetFastInt%(%s*"([^"]+)"[^)]*%)]], rewriteInt)
:gsub([[settings%(%):GetFFlag%(%s*"([^"]+)"%)]], rewriteFlag)
:gsub([[settings%(%):GetFVariable%(%s*"([^"]+)"%)]], rewriteSetting)
:gsub([[settings%(%):GetFVariable%(%s*"([^"]+)"%)]], rewriteSetting)
:gsub([[game:GetService%("StudioService"%):HasInternalPermission%(%)]], tostring(CONFIG.SpoofInternal))
:gsub([[game:GetService%("StudioService"%)]], [[({StudioLocaleId = "en-us", GetPropertyChangedSignal = function() return {Connect = function() return {Disconnect = function() end} end} end, HasInternalPermission = function() return true end, FindFirstChild = function(n) game:GetService("StudioService"):FindFirstChild(n) end})]])
:gsub([[game:GetService%("ABTestService"%)]], [[({GetVariant = function() return "Control" end})]])
-- The "Flags" util module produces some annoying warnings, remove them.
:gsub([[warn%("An error occurred defining the fast flag: " .. flagName%)]], "")
:gsub([[warn%(err%)]], "")
-- Places a bindable event under StudioService, doesn't work with this bypass. Put it somewhere else.
:gsub([[BindableEventBridge%.new%(Services.getStudioService%(%)%)]], [[BindableEventBridge.new(game:GetService("CoreGui"))]])
-- Used by the Roact Inspector to get plugin GUIs, but we probably won't need this - keeping it will error.
:gsub([[Services%.getRobloxPluginGuiService%(%):WaitForChild%(pluginName, 10%)]], "nil")
-- There's not really any good way to replace the dialog setup, so just disable it entirely
:gsub([[local Dialog = createPluginWidget%("Dialog", function%(props%)+.+%send%)]], "local Dialog = function() end")
:gsub([[Typecheck%.wrap%(Dialog, script%)]], "")
-- Prevent elevated plugins from setting PluginAction.Enabled
:gsub([[action%.Enabled = %a+]], "")
:gsub([[action%.DefaultShortcut = definition%.defaultShortcut]], "")
-- PLUGIN SPECIFIC FIXES
if (pluginName == "DeveloperStorybook") then
src = src
:gsub([[local STORYBOOK_SOURCES]], [[local STORYBOOK_SOURCES = {"ReplicatedStorage","ReplicatedFirst","ServerScriptService","StarterGui","StarterPlayer"} local STORYBOOK_SOURCES_OLD]])
elseif (pluginName == "DeveloperInspector") then
src = src
:gsub([[toggleProfile.Enabled = %w+]], "")
end
local success = pcall(function()
script.Source = src
end)
if not success then
warn("Failed to write source of module:", script:GetFullName())
end
end
local function visitInstance(obj: Instance)
if (not (obj:IsA("ModuleScript") or obj:IsA("BaseScript"))) then return end
if (obj == script) then return end
if (obj.Name == "runTests") then
(obj :: Script).Disabled = true
return
end
rewriteFlagsForScript(obj :: ModuleScript)
end
local function run()
local instances = root:GetDescendants()
local instanceCount = #instances
print(("Demoting plugin: %s\nInstances to scan:%s"):format(pluginName, instanceCount))
for _, obj in ipairs(instances) do
visitInstance(obj)
end
print(("Demoted plugin %s"):format(pluginName))
end
run()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment