10 4 No KeyRap Ability Test Script (Delta Mobile) – Autoplay: Auto Notes, Skip Mines
Rap Ability Test 0.22 · RobloxDescription
Rap Ability Autoplay is a Rap Ability Test 0.22 script you can run in Delta on Android, iOS or PC. It includes enable autoplay, skip mines, long notes and more. It is open source and uses the WindUI menu. Created by SarahHenderson. No key.
Key Features
- Autoplay: hits notes with press and release
- Skip mines and damage notes
- Long note (sustain) support
- Auto-detects your keybinds
- Press and release offset settings
- Player side override
Rap Ability Test 0.22 is a 4-key rhythm battle with daily and weekly leaderboards and Cheddar rewards. SarahHenderson’s Rap Ability Test script plays the chart for you through the same input path the game uses for real key presses. It also keeps your timing a little imperfect on purpose.
Tabs in the WindUI window
Autoplay
Enable Autoplay and Skip mines / damage notes. Long notes are held for their full tail and released at the end.
Timing
Press Offset (−100 to 100 ms), Release Offset for long-note tails (−100 to 200 ms), Press Leeway, and a Reset Timing button. The script notes these shifts don’t affect detection. They just fix early or late hits, like a tail that reads “Fresh −X ms”.
Keys
Your four lane keys are read from the game’s Settings → Controls. Edit them here if you rebind after launching.
Advanced
The humanizer: press and release jitter, clamps, Miss Chance and Bad/Good Chance (0–5%), plus a Player Side override for 1v1 lanes.
About the shadowban warning
The creator says the default humanizer values are user-tested to pass the game’s shadowban. A shadowbanned account can’t earn Cheddar or post leaderboard scores. If you drag a slider under the safe threshold, a “Risky setting” alert pops up. Leave the defaults alone unless you’re on an alt.
Loading
No key is needed. The code box holds the readable source, which pulls the WindUI library from a GitHub mirror (ofkotov/lib). Execute it (Delta is the headline executor, and the page also tests Arceus X, Fluxus, Codex and PC ones), then toggle the menu with the Delete key. Your executor has to support VirtualInputManager and debug upvalues for the note hits to work.
FAQ
Does it play on mobile? The page is marked mobile-friendly, and the game advertises strong mobile support. Use the window’s buttons, since there’s no Delete key on a phone.
Why do I sometimes get a Bad? That’s the humanizer doing its job. The small Bad chance is part of what keeps runs looking human.
Chart tip: if long notes keep finishing early, nudge Release Offset up in small steps instead of touching the jitter sliders. It only moves the tail release, so you fix the timing without going near the risky zone.
--[[
Rap Ability Test — Autoplay
Toggle the menu with the Delete key.
]]--
------------------------------------------------------------
-- 0) Cleanup previous instance(s)
------------------------------------------------------------
do
local function killState(g)
if type(g) ~= "table" then return end
pcall(function() g.heartbeat:Disconnect() end)
pcall(function() g.spoof:Disconnect() end)
pcall(function() g.window:Destroy() end)
pcall(function() g.window:Close() end)
if g.players then
for _, p in pairs(g.players) do
if type(p) == "table" and not p.Ghost then p.Bot = nil end
end
end
end
killState(_G.__RAT_AUTOPLAY); _G.__RAT_AUTOPLAY = nil
killState(_G.__FF_AUTOPLAY); _G.__FF_AUTOPLAY = nil
if _G.__AUTOPLAY_CONN then pcall(function() _G.__AUTOPLAY_CONN:Disconnect() end); _G.__AUTOPLAY_CONN = nil end
if _G.__AUTOPLAY_SPOOF_CONN then pcall(function() _G.__AUTOPLAY_SPOOF_CONN:Disconnect() end); _G.__AUTOPLAY_SPOOF_CONN = nil end
pcall(game:GetService("RunService").UnbindFromRenderStep,
game:GetService("RunService"), "__AUTOPLAY_RENDER")
end
------------------------------------------------------------
-- 1) Services / module
------------------------------------------------------------
local RS = game:GetService("ReplicatedStorage")
local Players = game:GetService("Players")
local RunService = game:GetService("RunService")
local VIM = game:GetService("VirtualInputManager")
local PlayerModule = require(RS:WaitForChild("Game"):WaitForChild("PlayerModule"))
local Trigger = PlayerModule.Trigger
local Tick = PlayerModule.Tick
------------------------------------------------------------
-- 2) Localised globals (hot-path micro-opt)
------------------------------------------------------------
local mathMax, mathSqrt, mathLog, mathCos, mathPi =
math.max, math.sqrt, math.log, math.cos, math.pi
local taskDefer = task.defer
local pcallF = pcall
local debugGU = debug.getupvalue
------------------------------------------------------------
-- 3) Upvalue accessors
------------------------------------------------------------
local function getUp(fn, idx)
local ok, val = pcallF(debugGU, fn, idx)
if ok then return val end
return nil
end
local function getPlayers() return getUp(Trigger, 3) end
local function getCurrentTime() return getUp(Tick, 3) or 0 end
local function getInputOffset() return getUp(Tick, 14) or 0 end
------------------------------------------------------------
-- 4) Danger thresholds (GUI warnings only)
-- Below these values is the territory where the server's anti-cheat has
-- been observed to shadow-ban accounts. They are advisory; the engine
-- still applies whatever value you set.
------------------------------------------------------------
local DANGER = {
pressJitterMs = 3,
releaseJitterMs = 3,
badChance = 0.4,
}
------------------------------------------------------------
-- 5) Settings (mutable from GUI)
-- Defaults are the user's tested-safe humanizer values.
------------------------------------------------------------
local settings = {
enabled = true,
pressOffsetMs = 0,
releaseOffsetMs = 0,
pressLeewayMs = 5,
pressJitterMs = 5,
pressJitterClampMs = 5,
releaseJitterMs = 5,
releaseJitterClampMs = 5,
missChance = 0.0,
badChance = 0.7,
sendVirtualInput = true,
bindKey = { "A", "S", "K", "L" },
hideBotplayLabel = true,
skipMines = true,
sideOverride = 0,
}
------------------------------------------------------------
-- 6) Auto-detect bound keys from in-game preferences
------------------------------------------------------------
local function pickKeyboardKey(bind)
if type(bind) ~= "table" then return nil end
local fallback
for _, v in pairs(bind) do
local name
if type(v) == "string" then name = v
elseif typeof(v) == "EnumItem" then name = v.Name end
if name then
local up = name:upper()
if Enum.KeyCode[up] then
if up ~= "LEFT" and up ~= "RIGHT" and up ~= "UP" and up ~= "DOWN" then
return up
end
fallback = fallback or up
end
end
end
return fallback
end
do
local plr = Players.LocalPlayer
local getPref = plr and plr:FindFirstChild("GetPref")
if getPref then
local ok, binds = pcallF(function() return getPref:Invoke("Binds") end)
if ok and type(binds) == "table" then
local laneNames = { "Left", "Down", "Up", "Right" }
for i, n in ipairs(laneNames) do
local k = pickKeyboardKey(binds[n])
if k then settings.bindKey[i] = k end
end
end
end
end
------------------------------------------------------------
-- 7) RNG / Gauss (Box-Muller)
------------------------------------------------------------
local rng = Random.new(tick() * 1e3)
local function gauss()
local u1 = mathMax(rng:NextNumber(), 1e-7)
return mathSqrt(-2 * mathLog(u1)) * mathCos(2 * mathPi * rng:NextNumber())
end
local function jitterMs(sigma, clampMag)
if sigma <= 0 then return 0 end
local v = gauss() * sigma
if v < -clampMag then return -clampMag end
if v > clampMag then return clampMag end
return v
end
------------------------------------------------------------
-- 7b) Mint-type lookup
-- Each mint module has a Type field:
-- 0 = "must hit" pickup. Missing it kills or heavily damages you
-- (Cheddar, MustHit, Power, Bait, Coin64, KennyCheddar, Bullet…).
-- ➜ ALWAYS hit at perfect timing, bypass humanizer miss/bad chance.
-- 1 = special note like Tap/Hopo — normal humanizer
-- 2 = damaging on hit (Mine, Hurt Note…) — SKIP
-- 3 = damaging on hit (Kill, Axe, Knife, Nuke…) — SKIP
------------------------------------------------------------
local mintIsDamaging = {}
local mintIsMustHit = {}
do
local mints = RS:FindFirstChild("Game") and RS.Game:FindFirstChild("Mints")
if mints then
for _, m in ipairs(mints:GetChildren()) do
local ok, mod = pcall(require, m)
if ok and type(mod) == "table" then
local t = mod.Type
if t == 2 or t == 3 then
mintIsDamaging[m.Name] = true
elseif t == 0 then
mintIsMustHit[m.Name] = true
end
end
end
end
end
------------------------------------------------------------
-- 8) Virtual input
------------------------------------------------------------
local function keyCodeForLane(laneIdx)
if not settings.sendVirtualInput then return nil end
local name = settings.bindKey[laneIdx]
if not name or name == "" then return nil end
return Enum.KeyCode[name:upper()]
end
local function vimPress(lane)
local kc = keyCodeForLane(lane)
if kc then pcallF(VIM.SendKeyEvent, VIM, true, kc, false, game) end
end
local function vimRelease(lane)
local kc = keyCodeForLane(lane)
if kc then pcallF(VIM.SendKeyEvent, VIM, false, kc, false, game) end
end
------------------------------------------------------------
-- 9) Engine state
------------------------------------------------------------
local pressedNotes = setmetatable({}, { __mode = "k" })
local heldSustains = {}
local plannedDecision = setmetatable({}, { __mode = "k" })
local function clearState()
pressedNotes = setmetatable({}, { __mode = "k" })
plannedDecision = setmetatable({}, { __mode = "k" })
heldSustains = {}
end
local function findLocalPlayerSlot(players)
local ov = settings.sideOverride
if ov == 1 or ov == 2 then
local p = players[ov]
if type(p) == "table" and p.Strums then return ov, p end
end
for k, p in pairs(players) do
if type(p) == "table" and p.Strums and p.Controller ~= nil and not p.Ghost then
return k, p
end
end
for k, p in pairs(players) do
if type(p) == "table" and p.Strums and not p.Ghost then
return k, p
end
end
return nil, nil
end
local function planFor(note)
local plan = plannedDecision[note]
if plan then return plan end
plan = { jitter = 0, releaseJ = 0, miss = false, bad = false, mine = false }
-- Damaging mints (Mine / Hurt Note / Kill / Nuke …) — skip entirely.
if settings.skipMines and note.mint and mintIsDamaging[note.mint] then
plan.mine = true
plannedDecision[note] = plan
return plan
end
-- "Must-hit" / golden mints (Cheddar / MustHit / KennyCheddar / Bait …) —
-- missing them is a guaranteed loss, so always hit them at perfect timing,
-- bypassing humanizer miss/bad rolls and jitter.
if note.mint and mintIsMustHit[note.mint] then
plannedDecision[note] = plan
return plan
end
plan.jitter = jitterMs(settings.pressJitterMs, settings.pressJitterClampMs)
plan.releaseJ = jitterMs(settings.releaseJitterMs, settings.releaseJitterClampMs)
if not (note.len and note.len > 0) then
local mc, bc = settings.missChance, settings.badChance
if mc > 0 or bc > 0 then
local r = rng:NextNumber() * 100
if r < mc then
plan.miss = true
elseif r < mc + bc then
local sign = (rng:NextNumber() < 0.5) and -1 or 1
plan.jitter = sign * (80 + rng:NextNumber() * 50)
plan.bad = true
end
end
end
plannedDecision[note] = plan
return plan
end
-- Tracks whether the previous tick had a valid player slot so we can release
-- any keys/sustains that were still down when the song ends.
local hadPlayer = false
local lastSideIdx = nil
local function releaseAllPressed()
for laneIdx = 1, 4 do
if heldSustains[laneIdx] then
if lastSideIdx then
pcallF(Trigger, PlayerModule, lastSideIdx, laneIdx, false, true)
end
heldSustains[laneIdx] = nil
end
-- Always tell the OS the key is up — VirtualInputManager doesn't
-- auto-release when the game session ends, so a sustained key would
-- otherwise stay logically pressed (causing menu navigation, etc.).
vimRelease(laneIdx)
end
end
local function autoplayTick()
if not settings.enabled then return end
local players = getPlayers()
if not players then
if hadPlayer then
releaseAllPressed()
hadPlayer = false
end
return
end
local sideIdx, p = findLocalPlayerSlot(players)
if not p then
if hadPlayer then
releaseAllPressed()
hadPlayer = false
end
return
end
hadPlayer = true
lastSideIdx = sideIdx
if p.BlockPlay or p.Killed then return end
local hitTime = getCurrentTime() - getInputOffset()
local pressOff = settings.pressOffsetMs
local releaseOff = settings.releaseOffsetMs
local leeway = settings.pressLeewayMs
local strums = p.Strums
for laneIdx = 1, #strums do
local strum = strums[laneIdx]
-- Sustain release
local heldNote = heldSustains[laneIdx]
if heldNote then
local plan = plannedDecision[heldNote] or planFor(heldNote)
local releaseAt = heldNote.t + (heldNote.len or 0) + releaseOff + plan.releaseJ
if heldNote.removed or hitTime >= releaseAt then
pcallF(Trigger, PlayerModule, sideIdx, laneIdx, false, true)
vimRelease(laneIdx)
heldSustains[laneIdx] = nil
end
end
-- Press
local closestIdx = strum.Closest
local notes = strum.Notes
local note = closestIdx and notes and notes[closestIdx]
if note and not pressedNotes[note] then
local plan = planFor(note)
if not plan.mine then
if plan.miss then
pressedNotes[note] = true
else
local target = note.t + pressOff + plan.jitter
if hitTime - target >= -leeway then
pressedNotes[note] = true
vimPress(laneIdx)
pcallF(Trigger, PlayerModule, sideIdx, laneIdx, true, true)
if note.len and note.len > 0 then
heldSustains[laneIdx] = note
else
local capturedLane = laneIdx
taskDefer(function()
pcallF(Trigger, PlayerModule, sideIdx, capturedLane, false, true)
vimRelease(capturedLane)
end)
end
end
end
end
end
end
end
------------------------------------------------------------
-- 10) Botplay label spoof — cached + throttled
------------------------------------------------------------
local botplayLabel = nil
local lastSpoofTime = 0
local function refreshBotplayLabel()
botplayLabel = nil
local plr = Players.LocalPlayer
local pg = plr and plr:FindFirstChildOfClass("PlayerGui")
if not pg then return end
local hud = pg:FindFirstChild("GameHUD")
if not hud then return end
for _, d in ipairs(hud:GetDescendants()) do
if d.Name == "Botplay" and d:IsA("GuiObject") then
botplayLabel = d
return
end
end
end
local function spoofBotplayLabel()
if not settings.hideBotplayLabel then return end
local now = os.clock()
if now - lastSpoofTime < 0.5 then return end
lastSpoofTime = now
if not botplayLabel or not botplayLabel.Parent then
refreshBotplayLabel()
end
if botplayLabel and botplayLabel.Visible then
botplayLabel.Visible = false
end
end
------------------------------------------------------------
-- 11) WindUI GUI
------------------------------------------------------------
local WindUI
do
local ok, lib = pcallF(function()
return loadstring(game:HttpGet(
"https://raw.githubusercontent.com/ofkotov/lib/refs/heads/main/123"))()
end)
if not ok or not lib then
warn("[RAT Autoplay] WindUI failed to load: " .. tostring(lib))
return
end
WindUI = lib
end
local Window = WindUI:CreateWindow({
Title = "Rap Ability Autoplay",
Icon = "music",
Author = "by claude opus vibecodus",
Folder = "RAT_Autoplay",
Size = UDim2.fromOffset(440, 380),
Theme = "Rose",
User = { Enabled = false },
SideBarWidth = 150,
ScrollBarEnabled = true,
})
Window:EditOpenButton({
Title = "Autoplay",
Icon = "music",
CornerRadius = UDim.new(0, 16),
StrokeThickness = 2,
Color = ColorSequence.new(Color3.fromHex("ff6b9d"), Color3.fromHex("c850c0")),
OnlyMobile = false,
Enabled = true,
Draggable = true,
})
Window:SetToggleKey(Enum.KeyCode.Delete)
local function notify(opts)
pcallF(function() WindUI:Notify(opts) end)
end
local function makeSlider(tab, opts)
local function fire(v)
opts.Callback(v)
if opts.DangerBelow and v < opts.DangerBelow then
notify({
Title = "Risky setting",
Content = (opts.Title or "value") .. " = " .. tostring(v) ..
" is below the safe threshold (" .. tostring(opts.DangerBelow) ..
"). This may trigger the anti-cheat and shadow-ban your account.",
Icon = "alert-triangle",
Duration = 5,
})
end
end
if tab.Slider then
return tab:Slider({
Title = opts.Title,
Icon = opts.Icon,
Value = { Min = opts.Min, Max = opts.Max, Default = opts.Default },
Step = opts.Step or 1,
Callback = fire,
})
end
return tab:Input({
Title = opts.Title .. " (" .. tostring(opts.Min) .. ".." .. tostring(opts.Max) .. ")",
Value = tostring(opts.Default),
InputIcon = opts.Icon or "hash",
Placeholder = "number",
Callback = function(text)
local n = tonumber(text)
if not n then return end
if opts.Min and n < opts.Min then n = opts.Min end
if opts.Max and n > opts.Max then n = opts.Max end
fire(n)
end,
})
end
------------------------------------------------------------
-- MAIN TAB
------------------------------------------------------------
local MainTab = Window:Tab({ Title = "Autoplay", Icon = "play", Desc = "Main controls" })
MainTab:Paragraph({
Title = "Rap Ability Autoplay",
Desc = "Auto-hits every note through the same code path the game uses for real keypresses. A light humanizer (gaussian timing jitter + tiny Bad chance) keeps your runs under the detection so Cheddar and lb plays keeps coming.",
Image = "music",
Color = "HotPink",
})
MainTab:Paragraph({
Title = "Notes",
Desc = "Defaults settings are pre-tuned and confirmed safe. If you want to tweak them, the Advanced tab exposes everything — values inside the safe stay undetected; the GUI only warns you if you drop into the risky zone.",
Image = "info",
Color = "HotPink",
})
MainTab:Toggle({
Title = "Enable Autoplay",
Icon = "power",
Default = settings.enabled,
Callback = function(v)
settings.enabled = v
if not v then
releaseAllPressed()
clearState()
hadPlayer = false
end
end,
})
MainTab:Toggle({
Title = "Skip mines / damage notes",
Icon = "bomb",
Default = settings.skipMines,
Callback = function(v) settings.skipMines = v end,
})
------------------------------------------------------------
-- TIMING TAB
------------------------------------------------------------
local TimingTab = Window:Tab({ Title = "Timing", Icon = "timer", Desc = "Press / Release offsets" })
TimingTab:Paragraph({
Title = "Press / Release offsets",
Desc = "Constant timing shifts (ms). These do NOT affect anti-cheat — they just nudge everything. Useful when long-note tails read Fresh -X ms (raise Release Offset by +X).",
Image = "info",
Color = "HotPink",
})
makeSlider(TimingTab, { Title = "Press Offset", Icon = "arrow-down", Min = -100, Max = 100, Default = settings.pressOffsetMs, Step = 1, Callback = function(v) settings.pressOffsetMs = v end })
makeSlider(TimingTab, { Title = "Release Offset (LN tail)", Icon = "arrow-up", Min = -100, Max = 200, Default = settings.releaseOffsetMs, Step = 1, Callback = function(v) settings.releaseOffsetMs = v end })
makeSlider(TimingTab, { Title = "Press Leeway", Icon = "clock", Min = 0, Max = 30, Default = settings.pressLeewayMs, Step = 1, Callback = function(v) settings.pressLeewayMs = v end })
TimingTab:Button({
Title = "Reset Timing",
Icon = "rotate-ccw",
Callback = function()
settings.pressOffsetMs = 0
settings.releaseOffsetMs = 0
settings.pressLeewayMs = 5
notify({ Title = "Timing reset", Content = "Offsets back to 0", Icon = "check", Duration = 2 })
end,
})
------------------------------------------------------------
-- KEYS TAB
------------------------------------------------------------
local KeysTab = Window:Tab({ Title = "Keys", Icon = "keyboard", Desc = "Bind keys for virtual input" })
KeysTab:Paragraph({
Title = "Bound keys",
Desc = "Auto-detected from your in-game settings (Settings -> Controls). Edit manually if you rebound them after launching the script.",
Image = "keyboard",
Color = "HotPink",
})
local laneLabels = { "Lane 1 (Left)", "Lane 2 (Down)", "Lane 3 (Up)", "Lane 4 (Right)" }
for i = 1, 4 do
local laneIdx = i
KeysTab:Input({
Title = laneLabels[i],
Value = settings.bindKey[i],
InputIcon = "keyboard",
Placeholder = "Letter, e.g. A",
Callback = function(txt)
if txt and #txt > 0 then settings.bindKey[laneIdx] = txt:upper() end
end,
})
end
------------------------------------------------------------
-- ADVANCED TAB
------------------------------------------------------------
local AdvTab = Window:Tab({ Title = "Advanced", Icon = "settings", Desc = "Humanizer knobs" })
AdvTab:Paragraph({
Title = "Humanizer",
Desc = "Per-note timing variance plus a tiny chance of a Bad note. Defaults are user-tested and known to pass the shadowban (you are not abble to gain cheese or set your scores into leaderboard). Going lower than the defaults values may send you to shadowban",
Image = "sliders-horizontal",
Color = "HotPink",
})
makeSlider(AdvTab, { Title = "Press Jitter sigma (ms)", Icon = "shuffle", Min = 0, Max = 20, Default = settings.pressJitterMs, Step = 1, DangerBelow = DANGER.pressJitterMs, Callback = function(v) settings.pressJitterMs = v end })
makeSlider(AdvTab, { Title = "Press Jitter Clamp (ms)", Icon = "shuffle", Min = 1, Max = 30, Default = settings.pressJitterClampMs, Step = 1, Callback = function(v) settings.pressJitterClampMs = v end })
makeSlider(AdvTab, { Title = "Release Jitter sigma (ms)", Icon = "shuffle", Min = 0, Max = 20, Default = settings.releaseJitterMs, Step = 1, DangerBelow = DANGER.releaseJitterMs, Callback = function(v) settings.releaseJitterMs = v end })
makeSlider(AdvTab, { Title = "Release Clamp (ms)", Icon = "shuffle", Min = 1, Max = 30, Default = settings.releaseJitterClampMs, Step = 1, Callback = function(v) settings.releaseJitterClampMs = v end })
makeSlider(AdvTab, { Title = "Miss Chance (%)", Icon = "x", Min = 0, Max = 5, Default = settings.missChance, Step = 0.1, Callback = function(v) settings.missChance = v end })
makeSlider(AdvTab, { Title = "Bad/Good Chance (%)", Icon = "minus", Min = 0, Max = 5, Default = settings.badChance, Step = 0.1, DangerBelow = DANGER.badChance, Callback = function(v) settings.badChance = v end })
AdvTab:Dropdown({
Title = "Player Side override",
Values = { "Auto", "Side 1 (Left)", "Side 2 (Right)" },
Multi = false,
Default = "Auto",
Callback = function(v)
if v == "Side 1 (Left)" then settings.sideOverride = 1
elseif v == "Side 2 (Right)" then settings.sideOverride = 2
else settings.sideOverride = 0 end
end,
})
------------------------------------------------------------
-- 12) Hooks
------------------------------------------------------------
local heartbeatConn = RunService.Heartbeat:Connect(autoplayTick)
local spoofConn = RunService.Heartbeat:Connect(spoofBotplayLabel)
_G.__RAT_AUTOPLAY = {
heartbeat = heartbeatConn,
spoof = spoofConn,
window = Window,
settings = settings,
players = getPlayers(),
}
notify({
Title = "Rap Ability Autoplay",
Content = "Loaded. Keys: " ..
table.concat(settings.bindKey, " ") ..
". Press Delete to toggle the menu.",
Icon = "music",
Duration = 5,
})


