mcpField
R
roblox-studio-emulation

roblox-studio-emulation

github.com/flatbedjTesting & quality049 installsUpdated 6w ago
Repository

About roblox-studio-emulation

roblox-studio-emulation is a testing skill for Roblox Studio workflows, aimed at developers who need to verify mobile, multiplayer, and edge-case behavior without relying solely on solo playtests. It explains Studio menu automation, remote-event testing, and how to report verification limits.

Key features

  1. 01Emulate touch and device conditions
  2. 02Drive Studio Test menu with AppleScript
  3. 03Test multiplayer flows through real remotes
  4. 04Set up scenarios with client attributes
  5. 05Distinguish live, logic, and untested results

Use cases

  1. 01Check touch-only UI behavior
  2. 02Exercise party state updates without a second client
  3. 03Test branches using synthetic inputs

MCP support

The skill is for use alongside the Roblox Studio MCP tools; it references `list_roblox_studios`, `set_active_studio`, and `start_stop_play` for reconnecting after emulator changes.

Install

Copy the command and run it in a terminal.

$npx skills add https://github.com/flatbedj/roblox-skills --skill roblox-studio-emulation

After installing, type/roblox-studio-emulationin Claude Code to confirm it loaded.

Details

From the project README

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.TouchEnabled

TouchEnabled = 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):

ItemNotes
Device Emulatortoggles emulation
Controller Emulator / Player Emulatorgamepad / player-state emulation
Start Test Sessionrequired before Add Clients
Add Clientsdisabled in Play Solo; enabled inside a Test Session
Toggle Client View, Pause Client, End Sessionmulti-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 on

Always 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 up
When 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/N

That'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.

Comments

Markdown supported