How to use
- Copy the installation command and review the source package before installing it in your own agent environment.
- Check the source document for required tools, credentials and supported hosts.
- Describe your input and desired deliverable; review the output against the source material.
npx skills add https://github.com/flatbedj/roblox-skills --skill roblox-studio-emulationBefore you start
- Installation does not include a model subscription, API allowance or permission to access your files.
- Repository stars and installation counts describe activity, not an independent quality or security test.
Rendered from the source repository. Headings, examples, code, tables, links, and referenced images are preserved.
Emulating conditions a solo playtest can't reach
Two whole classes of testing look impossible from the MCP tools alone, so it's tempting to punt them to the user. Both are self-serviceable. Check here before claiming you need the user to test something.
For the normal dev loop see roblox-studio-workflow; for the MCP toolbox see roblox-studio-mcp.
1. Device emulation (mobile / tablet / touch) — via Studio's menu bar
Studio exposes no windows to macOS Accessibility (count windows → 0), so you can't resize it. But it exposes its entire menu bar, which is enough to drive the Test menu.
# one-time: the user must grant Terminal (or your host app) Accessibility permission
# System Settings → Privacy & Security → Accessibility
# symptom if missing: "osascript is not allowed assistive access. (-1719)"
osascript -e 'tell application "Roblox Studio" to activate'
osascript -e 'tell application "System Events" to tell process "RobloxStudio" \
to click menu item "Device Emulator" of menu 1 of menu bar item "Test" of menu bar 1'Process name is `RobloxStudio` (not "Roblox Studio", not "RobloxStudioBeta").
Verify it took, don't assume — start Play and read the real values:
local cam = workspace.CurrentCamera
local UIS = game:GetService("UserInputService")
-- desktop: ~1920x1080, TouchEnabled=false
-- emulated: device-sized viewport, TouchEnabled=TRUE
return cam.ViewportSize, UIS.TouchEnabledTouchEnabled = true is the signal that touch-only code paths (thumbstick clearance, touch layout branches, POS_SHOWN offsets) are now live.
The full Test menu (useful items):
| Item | Notes |
|---|---|
Device Emulator | toggles emulation |
Controller Emulator / Player Emulator | gamepad / player-state emulation |
Start Test Session | required before Add Clients |
Add Clients | disabled in Play Solo; enabled inside a Test Session |
Toggle Client View, Pause Client, End Session | multi-client session control |
Enumerate + check enabled state before clicking:
osascript -e 'tell application "System Events" to tell process "RobloxStudio" \
to get name of every menu item of menu 1 of menu bar item "Test" of menu bar 1'
osascript -e 'tell application "System Events" to tell process "RobloxStudio" \
to get enabled of menu item "Add Clients" of menu 1 of menu bar item "Test" of menu bar 1'Limits
- Picking a specific device (iPhone vs iPad vs …) uses an in-window dropdown that is NOT
AX-exposed. You get whatever device is currently selected. Ask the user to set it once if you need a particular width — narrow phones are where layout actually breaks.
- Toggling the emulator ends the current Play session and can drop the MCP link. Re-point with
list_roblox_studios → set_active_studio, then start_stop_play.
- Toggle it back OFF when done, or every later screenshot is a touch device.
2. Multiplayer features WITHOUT a second client — fire the real remotes
This is usually faster and more precise than launching a multi-client session. The Server and Client VMs share the DataModel, so a FireClient from the Server VM reaches the real client modules and drives everything downstream — signal handlers, cached state, UI refreshes.
-- datamodel_type = "Server"
local Remotes = require(game:GetService("ReplicatedStorage"):WaitForChild("Shared").Remotes)
local plr = game.Players:GetPlayers()[1]
-- pretend the server told us we joined someone's party
Remotes.event("PartyUpdated"):FireClient(plr, {
ownerUserId = 99999999, ownerName = "TestHost",
members = { { userId = plr.UserId, name = plr.DisplayName } },
})Then assert on observable instance state from the Client VM (never module internals — separate VM, require gives a phantom copy):
-- datamodel_type = "Client"
local on = 0
for _, p in game:GetService("CollectionService"):GetTagged("Plot") do
for _, d in p:GetDescendants() do
if d:IsA("ProximityPrompt") and d.Name == "PartyPrompt" and d.Enabled then on += 1 end
end
end
return onAlways test the round trip — fire the "joined" state, assert, then fire the "left" state (FireClient(plr, nil)) and assert it reverts. A one-way test hides stuck state, which is the scarier bug (a prompt stuck hidden = the feature is dead for that player).
Faking the preconditions
Client-side attribute writes are enough to set up a scenario, because client code reads the same attributes and GetAttributeChangedSignal fires locally:
-- datamodel_type = "Client" — pretend another player owns this plot
workspace.Map.Plots.Plot1:SetAttribute("OwnerUserId", 99999999)
-- ... run the test ...
workspace.Map.Plots.Plot1:SetAttribute("OwnerUserId", nil) -- ALWAYS clean upWhen this does NOT work
Code that requires the faked user to be a real online `Player` can't be faked this way. Example: a poll loop that does Players:GetPlayerByUserId(owner) and tears down anything failing that check will immediately undo your setup. Symptom: the thing you created vanishes within a second.
Fall back to either:
- `Test ▸ Start Test Session ▸ Add Clients` for a genuine second player, or
- unit-test the pure logic with synthetic inputs in the Client VM — mirror the shipped
guard/branch verbatim and table-test it:
local cases = {
{ "nil payload", nil, true },
{ "empty string", "", true },
{ "garbage base64", "!!!not-base64!!!", true },
{ "short buffer", Base64.encode(short),true },
{ "valid buffer", Base64.encode(full), false },
}
-- assert blocked == expected for each; report N/NThat's a real, reportable result — just be explicit that it verifies the logic, not the full render path.
Reporting honestly
Distinguish these three, every time:
- Verified live — drove it and observed the effect.
- Logic-verified — table-tested the branch with synthetic inputs; full path untested.
- Untested — say so, and say exactly what it needs.
Never let "it compiles and looks right" be reported as verified.



