#!/usr/bin/env python3 """Push MapBuilder.luau source into a LIVE (Play-mode) Studio via the MCP bridge. Rojo cannot reach the running server datamodel in Play mode, so the source is shipped as ~9000-char chunks into named StringValue children (p0, p1, ...) under ReplicatedStorage, then concatenated and assigned to the ModuleScript's .Source. Named children matter: a retried chunk overwrites its slot instead of appending a duplicate. Bridge calls fail intermittently ("Unable to find an active Studio instance") even while Studio is up, so every call is retried. """ import sys, time sys.path.insert(0, "/tmp") import rbx SRC_PATH = sys.argv[1] MODULE_NAME = sys.argv[2] # Which datamodel to write into. "Server" is the PLAY-mode sandbox: Studio # DISCARDS it when Play stops, so pushes there are silently lost on the next # stop/start (this burned an entire debugging session -- pushes verified at the # correct byte length, then vanished). "Edit" is the persistent datamodel and # is what you almost always want; it survives into the next Play session. # Edit is only reachable while Studio is actually in Edit mode. DATAMODEL = sys.argv[3] if len(sys.argv) > 3 else "Edit" MODULE = 'game:GetService("ServerScriptService"):FindFirstChild("Server", true)' CHUNK = 9000 def call(code, tries=4): last = None for a in range(tries): try: r = rbx.call( "execute_luau", {"code": code, "datamodel_type": DATAMODEL}, warmup=6, wait=45, timeout=300, ) if r and "Unable to find an active Studio" not in str(r): return r last = r except Exception as e: # noqa: BLE001 last = e time.sleep(5) raise RuntimeError(f"bridge failed after {tries} tries: {last}") src = open(SRC_PATH, encoding="utf-8").read() nbytes = len(src.encode()) print(f"source: {len(src)} chars, {nbytes} utf-8 bytes") # Pick a long-bracket level that does not occur in the source. lvl = 0 while ("]" + "=" * lvl + "]") in src or ("[" + "=" * lvl + "[") in src: lvl += 1 eq = "=" * lvl print(f"long-bracket level: {lvl}") chunks = [src[i : i + CHUNK] for i in range(0, len(src), CHUNK)] print(f"chunks: {len(chunks)}") print(call('local rs=game:GetService("ReplicatedStorage")\n' 'local b=rs:FindFirstChild("MBBuf") if b then b:Destroy() end\n' 'b=Instance.new("Folder") b.Name="MBBuf" b.Parent=rs\n' 'return "buffer reset"')) for i, c in enumerate(chunks): # Lua long-bracket literals SWALLOW a newline that immediately follows the # opening bracket. When a chunk happened to start with "\n" that byte was # silently lost, giving assembled=65158 vs expected=65159. Emit a deliberate # leading newline for Lua to eat so the payload always survives intact. code = ( 'local rs=game:GetService("ReplicatedStorage")\n' 'local b=rs:WaitForChild("MBBuf")\n' f'local n="p{i}"\n' "local sv=b:FindFirstChild(n) or Instance.new(\"StringValue\")\n" "sv.Name=n sv.Parent=b\n" f"sv.Value=[{eq}[\n{c}]{eq}]\n" f'return "p{i} "..#sv.Value' ) print(f" chunk {i+1}/{len(chunks)}: {call(code)}") assemble = f""" local rs=game:GetService("ReplicatedStorage") local b=rs:WaitForChild("MBBuf") local parts={{}} for i=0,{len(chunks)-1} do local sv=b:FindFirstChild("p"..i) if not sv then return "MISSING CHUNK p"..i end parts[#parts+1]=sv.Value end local s=table.concat(parts) -- Search the WHOLE game, not just ServerScriptService: shared modules live in -- ReplicatedStorage and client ones in StarterPlayerScripts. Searching only -- SSS silently reported "NOT FOUND" for Chassis while the wrapper still -- exited 0, so a half-applied push looked like a success. local roots={{ game:GetService("ServerScriptService"), game:GetService("ReplicatedStorage"), game:GetService("StarterPlayer"), game:GetService("Workspace"), }} local m=nil for _,r in ipairs(roots) do local hit=r:FindFirstChild("{MODULE_NAME}", true) if hit and hit:IsA("ModuleScript") then m=hit break end end if not m then return "{MODULE_NAME} ModuleScript NOT FOUND" end m.Source=s return ("assembled=%d expected={nbytes} match=%s class=%s path=%s"):format( #s, tostring(#s=={nbytes}), m.ClassName, m:GetFullName()) """ result = call(assemble) print(result) # Exit non-zero on any failure. Previously this always exited 0, so a # "NOT FOUND" or byte-count mismatch was reported as success by the caller. if "match=true" not in str(result): print("PUSH FAILED -- module not updated", file=sys.stderr) sys.exit(1)