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?

ParameterDescription
fnThe 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)
end
MV_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?

ParameterDescription
fnThe 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)
end
MV_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.

ParameterDescription
fnAnonymous function that returns the environment values being checked.
...keysExpected 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.

ParameterDescription
fnThe function to protect.
keyConstant string used to encrypt at build time.
rtKeyRuntime 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.

ParameterDescription
strThe string to encrypt.
keyConstant string used to encrypt at build time.
rtKeyRuntime 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.

ParameterDescription
tblThe 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_OBFUSCATED

Resolves to true in an obfuscated build and nil otherwise.

if not MV_OBFUSCATED then
  error("run the obfuscated build")
end
MV_CRASH

Expands 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

ParameterDescription
fnThe 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 !
end
local 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

ParameterDescription
xinline function name.
fninline 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_OBFUSCATED
LPH_LINEaliases MV_LINE
LPH_CRASHaliases MV_CRASH
LPH_NO_VIRTUALIZEaliases MV_OMIT
LPH_ENCFUNCaliases MV_ENC_FUNC
LPH_PRECHECKaliases MV_PRECHECK
LPH_INLINEFUNCaliases MV_INLINE

LPH_ENCSTR, LPH_ENCNUM, LPH_JIT, and LPH_NO_UPVALUES are also recognized and safely ignored, so nothing breaks on import.

Miscellaneous

MV_LINE

Resolves 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]])