Documentation
Everything you need to obfuscate Luau: the HTTP API and the in-script macros.
Overview
Macros are inline directives you drop into your Luau to control obfuscation function by function. They come in two forms: call-style like MV_VM(function() ... end) and --! comment directives placed above a function:
--!mv:vm local function verify(pw) return pw == "s3cret" end
Using comment directives offers a seamless alternative to inline directives that aims to fit better with modern Luau language servers. This allows you to obfuscate the exact same script you use to debug against without annoying Luau type check warnings or a SDK header.
Performance
Some macros are tailored specifically for you to optimize performance, such as MV_OMIT:
--!mv:omit
local function handlePreRender(deltaTime)
local fps = math.round(1 / deltaTime)
label.Text = "FPS: " .. fps
end
game:getService("RunService").PreRender:Connect(handlePreRender)
-- or --
game:getService("RunService").PreRender:Connect(MV_OMIT(function(deltaTime)
local fps = math.round(1 / deltaTime)
label.Text = "FPS: " .. fps
end))Code which is expected to run often (e.g. per game tick, render frame, or otherwise multiple times per second) may cause application freezes, drop FPS, reduce throughput, and increase latency even if optimized properly. Most of the time obfuscation is not necessary on these functions. If you do find yourself needing to still obfuscate a function which is too slow to virtualize, MV_CFF may be a better candidate:
--!mv:cff
local function GetTransparentRecursive(instance, partsTable)
local partsTable = partsTable or {}
for _, child in pairs(instance:GetChildren()) do
if child:IsA('BasePart') or child:IsA('Decal') then
table.insert(partsTable, child)
end
GetTransparentRecursive(child, partsTable)
end
return partsTable
end
GetTransparentRecursive(game.Workspace)
MV_CFF functions are completely lifted from virtualized functions and are emitted as a separate opaque state machine. This can give much better performance but comes at a cost of quality obfuscation.
Security
MV_VM(fn, vmType?)Virtualizes the function passed to MV_VM, translating it to the selected VM implementation's abstracted ISA ran by a bundled interpreter.
Comment Directive
--!mv:vm vmType?
| Parameter | Description |
|---|---|
fn | The function to virtualize. |
vmType? | The selected VM implementation to use. "fox" | "skid". Default to selected VM option |
local verify = MV_VM(function(pw)
return pw == "s3cret"
end)
local verify = MV_VM(function(pw)
return pw == "s3cret"
end, "fox")
-- or --
--!mv:vm skid
local function factorial(n)
if n <= 0 then return 1 end
return n * factorial(n-1)
endMV_CFF(fn, decompose?, mangleExpr?, cfManglePercent?)Rewrites the function's control flow into a flat state machine dispatched by a loop.
Comment Directive
--!mv:cff decompose? mangleExpr? cfManglePercent?
| Parameter | Description |
|---|---|
fn | The function to protect. |
decompose? | Break control flow into a state machine. Default false. |
mangleExpr? | Also mangle indices, strings, and globals. Default false. |
cfManglePercent? | Insert fake branches based on the provided chance. Default 0. |
local step = MV_CFF(function()
for i = 1, 10 do print(i) end
end, true, true, 10)
-- or --
--!mv:cff true false 10
local function factorial(n)
if n <= 0 then return 1 end
return n * factorial(n-1)
endMV_PRECHECK(fn, ...keys)Runs a precheck function before the script is decrypted and folds its return values into the script's decryption key. Each key argument must match the corresponding return value in order and type; on any mismatch (wrong value, wrong type, or a missing return) the key breaks and the script fails, usually with an unhelpful error.
The function must be anonymous, non-variadic, and take no parameters. Globals are fine, but it cannot capture upvalues. Only available with the safeEnv compile type.
| Parameter | Description |
|---|---|
fn | Anonymous function that returns the environment values being checked. |
...keys | Expected values, one per returned value, each a constant number, string or boolean literal. Numbers must be non-negative integers below 4294967295. |
MV_PRECHECK(function() return 0x1234 end, 0x1234) -- multiple keys are checked in order against each returned value -- MV_PRECHECK(function() return true, "rosiej", 255 end, true, "rosiej", 255) MV_PRECHECK(function() -- globals are fine return game.PlaceId end, 25522223443)
MV_ENC_FUNC(fn, key, rtKey, vmType?)Lifts and encrypts the function body at build time to the selected VM implementation. rtKey has to reproduce the key to decrypt it at runtime.
| Parameter | Description |
|---|---|
fn | The function to protect. |
key | Constant string used to encrypt at build time. |
rtKey | Runtime value that must match to decrypt. |
vmType? | The selected VM implementation to use. "fox" | "skid". Default to selected VM option |
local load = MV_ENC_FUNC(function() return payload() end, "buildKey", getKey())
MV_ENC_STR(str, key, rtKey)Encrypts a string literal, rebuilt at runtime once rtKey matches the build key.
| Parameter | Description |
|---|---|
str | The string to encrypt. |
key | Constant string used to encrypt at build time. |
rtKey | Runtime value that must match to decrypt. |
local url = MV_ENC_STR("https://api.site/x", "buildKey", getKey())MV_INDEX_TO_NUM(tbl)Swaps the table's string keys for generated numeric indices. Applies to every named index.
| Parameter | Description |
|---|---|
tbl | The input table. |
local input = MV_INDEX_TO_NUM({
_mv_jump = 1, -- *all* instances of x._mv_jump will be patched
_mv_crouch = 2, -- the '_mv_' prefix is optional but makes unwanted patches less likely
})MV_OBFUSCATEDResolves to true in an obfuscated build and nil otherwise.
if not MV_OBFUSCATED then
error("run the obfuscated build")
endMV_CRASHExpands to an expression that faults the VM.
if beingDebugged() then MV_CRASH() end
Optimization
MV_OMIT(fn)Omits the function from all obfuscation passes, emits as plaintext. Referenced upvalues are patched where used and can be used liberally. This can substantially improve performance on the target function.
Comment Directive
--!mv:omit
| Parameter | Description |
|---|---|
fn | The function to omit. |
local upval = "meow"
local hot = MV_OMIT(function()
upval = "woof!"
return heavyMath()
end)
hot()
print(upval) -- 'woof!'
-- or --
--!mv:omit
local function factorial(n)
if n <= 0 then return 1 end
return n * factorial(n-1) -- 'factorial' is an upvalue too !
endlocal x = MV_INLINE(fn)Inlines the function into its call sites instead of emitting a callable. Can also be used as an inline directive above a local function or const function declaration.
Comment Directive
--!mv:inline
| Parameter | Description |
|---|---|
x | inline function name. |
fn | inline function body. |
local add = MV_INLINE(function(a, b)
return a + b
end)
print(add(2, 3))
-- or --
--!mv:inline
local function halve(n)
return n / 2
end
print(halve(10))Drop in
Migrating from Luraph? MoonVeil recognizes the LPH_* macro names as aliases, so existing scripts keep working without a rewrite.
LPH_OBFUSCATEDaliases MV_OBFUSCATEDLPH_LINEaliases MV_LINELPH_CRASHaliases MV_CRASHLPH_NO_VIRTUALIZEaliases MV_OMITLPH_ENCFUNCaliases MV_ENC_FUNCLPH_PRECHECKaliases MV_PRECHECKLPH_INLINEFUNCaliases MV_INLINELPH_ENCSTR, LPH_ENCNUM, LPH_JIT, and LPH_NO_UPVALUES are also recognized and safely ignored, so nothing breaks on import.
Miscellaneous
MV_LINEResolves to the line number it sits on.
error("failed at line " .. MV_LINE)MV_COMPRESS(value)Flags the value for compression. Currently a passthrough.
local data = MV_COMPRESS([[long long long string]])