golem-bridge 2.0.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/golem.py DELETED
@@ -1,1093 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Golem helper - control a live Roblox Studio session from anywhere.
3
-
4
- The Studio plugin polls the command channel below and posts results to the
5
- result channel. This helper sends a command, waits for the matching result,
6
- and prints it as JSON. Python 3.8+, no dependencies.
7
-
8
- Usage:
9
- python3 golem.py ping
10
- python3 golem.py debug # full connection + marketplace diagnostics
11
- python3 golem.py exec '{"op":"list","args":{"path":"game"}}'
12
- python3 golem.py lua 'return 1+1'
13
- python3 golem.py lua - < code.lua
14
- python3 golem.py list ServerScriptService
15
- python3 golem.py list game --recursive --max 1000
16
- python3 golem.py tree game --depth 3
17
- python3 golem.py read ServerScriptService/Main
18
- python3 golem.py find Coin --class Part --scope Workspace
19
- python3 golem.py grep applyDamage --scope ServerScriptService
20
- python3 golem.py script ReplicatedStorage Config --class ModuleScript < source.lua
21
- python3 golem.py delete Workspace/OldPart
22
- python3 golem.py move Workspace/Part ServerStorage
23
- python3 golem.py selection --set Workspace/Part
24
- python3 golem.py waypoint "before refactor"
25
- python3 golem.py turn begin
26
- python3 golem.py turn end --note "short reply the user reads in the chat"
27
- python3 golem.py place <path> <x,y,z> [--orientation x,y,z] # absolute position
28
- python3 golem.py paint <path> [--color #RRGGBB] [--material Grass] [--transparency 0.2]
29
- python3 golem.py rename <path> <newName>
30
- python3 golem.py look <path> [--distance 40] # aim the editor camera
31
- python3 golem.py count [scope] [--class Part] # quick instance count
32
- python3 golem.py undo # one Studio undo step
33
- python3 golem.py anchor <path> [--off] # anchor parts (models: all parts)
34
- python3 golem.py collide <path> [--off] # toggle collision
35
- python3 golem.py light <path> [--type point|spot|surface] [--color #RRGGBB] [--range 30] [--brightness 1]
36
- python3 golem.py sound <parent> <audioId> [--volume 0.5] [--loop] [--play] [--name N]
37
- python3 golem.py scatter <path> --count 20 --radius 60 [--y-jitter 2] [--parent P]
38
- python3 golem.py weld <path> # weld a model's parts together
39
- python3 golem.py hitbox <path> [--padding 1] [--name N] [--collide]
40
- python3 golem.py prompt <path> "Chop" [--object Tree] [--hold 0.5] [--distance 8]
41
- python3 golem.py particles <path> leaves|sparks|smoke|magic|fire|snow|rain|bubbles|dust|confetti|fireflies [--rate N] [--color #RRGGBB]
42
- python3 golem.py sign "Camp rules: no griefing" [--position x,y,z] [--parent P] [--size 6,4,0.5]
43
- python3 golem.py attr <path> [--set k=v ...] [--clear k ...] # Studio attributes
44
- python3 golem.py beam <FROM> <TO> [--color #RRGGBB] [--width 0.4] [--curve 0] # glowing beam between parts
45
- python3 golem.py trail <path> [--color #RRGGBB] [--lifetime 0.6] # motion trail on a moving part
46
- python3 golem.py explosion [--position x,y,z] [--radius 8] # one-shot boom (visual only)
47
- python3 golem.py ui_screen <name> [--parent P] [--order N] # ScreenGui under StarterGui
48
- python3 golem.py ui_frame <parent> <name> [--position 0,0,0,0] [--size 1,0,1,0] [--anchor 0,0]
49
- [--color #RRGGBB] [--radius 8] [--transparency 0] [--clip]
50
- python3 golem.py ui_label <parent> <name> --text "..." [--align left|center|right] [--wrap]
51
- [--color #RRGGBB] [--font medium] [--text-size 16]
52
- python3 golem.py ui_button <parent> <name> --text "..." [--color #RRGGBB] [--text-color #RRGGBB]
53
- python3 golem.py ui_input <parent> <name> [--placeholder "..."] [--background #RRGGBB]
54
- python3 golem.py ui_image <parent> <name> --asset <assetId> [--scale fit|stretch|tile]
55
- python3 golem.py ui_list <parent> [--direction vertical|horizontal] [--padding 8]
56
- [--halign left|center|right] [--valign top|middle|bottom]
57
- python3 golem.py play [--mode play|run] # start play-testing the game
58
- python3 golem.py stop # stop the play test
59
- python3 golem.py logs [--all] [--limit N] [--since TS] # output log (errors+warnings by default)
60
- python3 golem.py tag <path> --add Choppable [--remove Old] # + find --tag Choppable
61
- python3 golem.py match <from> <to...> # copy color/material onto targets
62
- python3 golem.py search <query> [--category C] [--limit N] # marketplace (runs directly, no Studio)
63
- python3 golem.py info <assetId> # asset details (runs directly, no Studio)
64
- python3 golem.py insert <assetId> [parent] # place a FREE marketplace asset into the place
65
-
66
- Add --timeout N (seconds, default 120) to any command.
67
- Exit codes: 0 = ok, 1 = Studio reported an error, 2 = transport/usage error.
68
- """
69
- import argparse
70
- import json
71
- import os
72
- import sys
73
- import time
74
- import urllib.error
75
- import urllib.parse
76
- import urllib.request
77
-
78
- FIREBASE_DB = os.environ.get("AIB_FIREBASE_DB", "__DB_URL__")
79
- CHANNEL = os.environ.get("AIB_CHANNEL", "__CHANNEL_ID__")
80
- CMD_URL = "%s/channels/%s/cmd" % (FIREBASE_DB, CHANNEL)
81
- RES_URL = "%s/channels/%s/res" % (FIREBASE_DB, CHANNEL)
82
- POLL_INTERVAL = float(os.environ.get("AIB_POLL_INTERVAL", "2"))
83
-
84
-
85
- def vec3(s):
86
- parts = [float(v) for v in str(s).replace(",", " ").split()]
87
- if not parts:
88
- raise ValueError("empty vector")
89
- if len(parts) == 1:
90
- return {"x": parts[0], "y": 0.0, "z": 0.0}
91
- if len(parts) != 3:
92
- raise ValueError("expected x,y,z")
93
- return {"x": parts[0], "y": parts[1], "z": parts[2]}
94
-
95
-
96
- def _post(url, body, timeout=30):
97
- req = urllib.request.Request(url, data=body.encode("utf-8"),
98
- headers={"Content-Type": "application/json"}, method="POST")
99
- with urllib.request.urlopen(req, timeout=timeout) as resp:
100
- return json.loads(resp.read().decode("utf-8", "replace"))
101
-
102
-
103
- def _get(url, timeout=60):
104
- with urllib.request.urlopen(url, timeout=timeout) as resp:
105
- return resp.read().decode("utf-8", "replace")
106
-
107
-
108
- MARKETPLACE_CATEGORIES = {
109
- "model": "Model", "models": "Model", "mesh": "MeshPart", "meshes": "MeshPart",
110
- "meshpart": "MeshPart", "decal": "Decal", "decals": "Decal", "image": "Decal",
111
- "images": "Decal", "picture": "Decal", "texture": "Decal", "audio": "Audio",
112
- "sound": "Audio", "sounds": "Audio", "music": "Audio", "video": "Video",
113
- "videos": "Video", "plugin": "Plugin", "plugins": "Plugin",
114
- }
115
- ASSET_TYPE_NAMES = {1: "Image", 3: "Audio", 4: "Mesh", 9: "Decal", 10: "Model",
116
- 18: "Video", 19: "Font", 40: "MeshPart"}
117
-
118
-
119
- def marketplace_get(url, timeout=20):
120
- req = urllib.request.Request(url, headers={
121
- "Accept": "application/json", "User-Agent": "Golem-aib/1.0"})
122
- with urllib.request.urlopen(req, timeout=timeout) as resp:
123
- return json.loads(resp.read().decode("utf-8", "replace"))
124
-
125
-
126
- def marketplace_search(query, category="model", limit=10, cursor=None):
127
- asset_type = MARKETPLACE_CATEGORIES.get(str(category or "model").lower())
128
- if asset_type is None:
129
- raise ValueError("unknown category '%s' (valid: model, mesh, image, audio, video, plugin)" % category)
130
- try:
131
- limit = max(1, min(int(limit or 10), 50))
132
- except (TypeError, ValueError):
133
- limit = 10
134
- url = ("https://apis.roblox.com/toolbox-service/v1/marketplace/%s?keyword=%s&limit=%d"
135
- % (asset_type, urllib.parse.quote(str(query)), limit))
136
- if cursor:
137
- url += "&cursor=" + urllib.parse.quote(str(cursor))
138
- data = marketplace_get(url)
139
- ids = [it["id"] for it in (data.get("data") or [])
140
- if isinstance(it, dict) and it.get("id") is not None]
141
- details, thumbs = {}, {}
142
- for i, aid in enumerate(ids[:12]):
143
- try:
144
- details[aid] = marketplace_get("https://economy.roblox.com/v2/assets/%s/details" % aid)
145
- except Exception:
146
- pass
147
- if i < 11:
148
- time.sleep(0.4) # the economy API rate limits hard
149
- if ids:
150
- try:
151
- tdata = marketplace_get(
152
- "https://thumbnails.roblox.com/v1/assets?assetIds=%s&size=420x420&format=Png"
153
- % ",".join(str(x) for x in ids[:12]))
154
- for th in (tdata.get("data") or []):
155
- if isinstance(th, dict) and th.get("targetId") is not None:
156
- thumbs[th["targetId"]] = th
157
- except Exception:
158
- pass
159
- results = []
160
- for aid in ids:
161
- d, th = details.get(aid), thumbs.get(aid)
162
- entry = {"id": aid, "category": asset_type}
163
- if isinstance(d, dict):
164
- entry["name"] = d.get("Name")
165
- entry["assetTypeId"] = d.get("AssetTypeId")
166
- entry["assetType"] = ASSET_TYPE_NAMES.get(d.get("AssetTypeId"))
167
- creator = d.get("Creator")
168
- if isinstance(creator, dict):
169
- entry["creator"] = creator.get("Name")
170
- if d.get("PriceInRobux") is not None:
171
- entry["priceInRobux"] = d["PriceInRobux"]
172
- if d.get("IsForSale") is not None:
173
- entry["forSale"] = d["IsForSale"]
174
- desc = d.get("Description")
175
- if isinstance(desc, str) and desc:
176
- entry["description"] = desc[:280]
177
- else:
178
- entry["detailsUnavailable"] = True
179
- if isinstance(th, dict):
180
- entry["thumbnail"] = th.get("imageUrl")
181
- entry["thumbnailState"] = th.get("state")
182
- results.append(entry)
183
- out = {"totalResults": data.get("totalResults"), "results": results}
184
- if data.get("nextPageCursor"):
185
- out["nextPageCursor"] = data["nextPageCursor"]
186
- return out
187
-
188
-
189
- def marketplace_info(asset_id):
190
- d = marketplace_get("https://economy.roblox.com/v2/assets/%s/details" % int(asset_id))
191
- out = {"id": int(asset_id)}
192
- if isinstance(d, dict):
193
- out["name"] = d.get("Name")
194
- out["assetTypeId"] = d.get("AssetTypeId")
195
- out["assetType"] = ASSET_TYPE_NAMES.get(d.get("AssetTypeId"))
196
- creator = d.get("Creator")
197
- if isinstance(creator, dict):
198
- out["creator"] = creator.get("Name")
199
- if d.get("PriceInRobux") is not None:
200
- out["priceInRobux"] = d["PriceInRobux"]
201
- if d.get("IsForSale") is not None:
202
- out["forSale"] = d["IsForSale"]
203
- desc = d.get("Description")
204
- if isinstance(desc, str):
205
- out["description"] = desc[:1000]
206
- try:
207
- tdata = marketplace_get(
208
- "https://thumbnails.roblox.com/v1/assets?assetIds=%d&size=420x420&format=Png" % int(asset_id))
209
- if isinstance(tdata.get("data"), list) and tdata["data"]:
210
- out["thumbnail"] = tdata["data"][0].get("imageUrl")
211
- out["thumbnailState"] = tdata["data"][0].get("state")
212
- except Exception:
213
- pass
214
- return out
215
-
216
-
217
- def _marketplace_via_plugin():
218
- return os.environ.get("AIB_MARKETPLACE", "").lower() == "plugin"
219
-
220
-
221
- def call(op, args=None, timeout=120):
222
- if str(FIREBASE_DB).startswith("__") or str(CHANNEL).startswith("__"):
223
- return {"ok": False, "error": "helper not configured (channel never stamped) - re-run: npx golem-bridge connect <channelId>"}
224
- cid = "c%d" % time.time_ns()
225
- payload = json.dumps({"id": cid, "op": op, "args": args or {}, "ts": int(time.time())})
226
- try:
227
- pub = None
228
- for attempt in range(4):
229
- try:
230
- pub = _post(CMD_URL + ".json", payload)
231
- break
232
- except urllib.error.HTTPError as exc:
233
- if exc.code == 429 and attempt < 3:
234
- time.sleep(float(exc.headers.get("Retry-After") or (5 * (attempt + 1))))
235
- else:
236
- raise
237
- except urllib.error.HTTPError as exc:
238
- return {"ok": False, "error": "relay rejected the command: HTTP %d: %s" % (exc.code, exc.read()[:200])}
239
- except Exception as exc:
240
- return {"ok": False, "error": "cannot reach the relay (%s) - check internet/DNS" % exc}
241
-
242
- since_key = str((pub or {}).get("name") or "") or None
243
- deadline = time.time() + timeout
244
- attempt = 0
245
- while time.time() < deadline:
246
- try:
247
- body = _get(RES_URL + ".json?shallow=true") # keys only - tiny
248
- attempt = 0
249
- keys = json.loads(body) if body != "null" else {}
250
- if isinstance(keys, dict):
251
- for key in sorted(k for k in keys if since_key is None or k > since_key):
252
- since_key = key
253
- raw = _get(RES_URL + "/" + key + ".json")
254
- try:
255
- entry = json.loads(raw)
256
- except ValueError:
257
- continue
258
- if isinstance(entry, dict) and entry.get("id") == cid:
259
- if entry.get("resultEncoded") and isinstance(entry.get("result"), str):
260
- try:
261
- entry["result"] = json.loads(entry["result"])
262
- except ValueError:
263
- pass
264
- entry.pop("resultEncoded", None)
265
- return entry
266
- except urllib.error.HTTPError as exc:
267
- if exc.code == 429:
268
- time.sleep(float(exc.headers.get("Retry-After") or 6))
269
- else:
270
- time.sleep(2)
271
- continue
272
- except Exception:
273
- attempt += 1
274
- time.sleep(min(15.0, 0.5 * (2 ** min(attempt, 5))))
275
- continue
276
- time.sleep(POLL_INTERVAL)
277
- return {
278
- "ok": False,
279
- "error": "timed out after %ds waiting for Studio to answer '%s'. Is Roblox Studio open with the Golem "
280
- "plugin connected to the relay?" % (timeout, op),
281
- }
282
-
283
-
284
- def status():
285
- """Read the relay channel's recent history and decode the plugin's liveness
286
- beacons. No Studio round-trip needed."""
287
- if str(CMD_URL).startswith("__") or str(RES_URL).startswith("__"): # no literal placeholders - see call()
288
- return {"ok": False, "error": "helper not configured (channel never stamped) - re-run: npx golem-bridge connect <channelId>"}
289
- try:
290
- beacon_body = _get(
291
- "%s/channels/%s/beacons.json?orderBy=%s&limitToLast=5"
292
- % (FIREBASE_DB, CHANNEL, urllib.parse.quote('"$key"')), timeout=30)
293
- beacon_data = json.loads(beacon_body) if beacon_body != "null" else {}
294
- if not isinstance(beacon_data, dict):
295
- beacon_data = {}
296
- res_body = _get(RES_URL + ".json?shallow=true", timeout=30)
297
- res_keys = json.loads(res_body) if res_body != "null" else {}
298
- if not isinstance(res_keys, dict):
299
- res_keys = {}
300
- results = sum(1 for v in res_keys.values() if v is True)
301
- except Exception as exc:
302
- return {"ok": False, "error": "cannot read the relay channel: %s" % exc}
303
- now = time.time()
304
- beacons = []
305
- for entry in beacon_data.values():
306
- if isinstance(entry, dict) and entry.get("op") in ("hello", "paused", "revoked"):
307
- beacons.append((float(entry.get("ts") or 0), entry))
308
- if not beacons:
309
- if results:
310
- verdict = ("no beacons, but %d cached result(s) - Studio is not running or runs an "
311
- "older plugin; ask the user to fully restart Studio with the current plugin" % results)
312
- else:
313
- verdict = ("result channel is empty - the plugin has never posted here: Studio is closed "
314
- "or was not restarted after a plugin update")
315
- return {"ok": True, "beacon": None, "ageSeconds": None, "recentResults": results, "verdict": verdict}
316
- ts, beacon = max(beacons, key=lambda b: b[0])
317
- age = max(0, int(now - ts))
318
- op = beacon.get("op")
319
- ver = beacon.get("v", "?")
320
- if age <= 900:
321
- verdict = "plugin is LIVE (v%s, hello beacon %ds ago) - it should answer commands" % (ver, age)
322
- else:
323
- verdict = ("last hello was %d min ago (v%s) - Studio may be closed or hung since then; ask the "
324
- "user to check the Golem window" % (age // 60, ver))
325
- return {"ok": True, "beacon": beacon, "ageSeconds": age, "recentResults": results, "verdict": verdict}
326
-
327
-
328
- def _turn_reminder(entry):
329
- if isinstance(entry, dict) and entry.get("turnOpen"):
330
- helper = (sys.argv and sys.argv[0]) or "golem.py"
331
- n = entry.get("turnTools")
332
- head = (">> TURN STILL OPEN (%d tools) - do NOT reply yet." % n
333
- if isinstance(n, int) else ">> TURN STILL OPEN - do NOT reply yet.")
334
- print(head + " When this task is done, close it with:\n"
335
- '>> python3 "%s" turn end --note "your reply"' % helper,
336
- file=sys.stderr)
337
-
338
-
339
- def _out(data, as_json=False, raw_field=None):
340
- if data.get("ok"):
341
- if raw_field and not as_json:
342
- result = data.get("result") or {}
343
- if isinstance(result.get(raw_field), str):
344
- sys.stdout.write(result[raw_field])
345
- if not result[raw_field].endswith("\n"):
346
- sys.stdout.write("\n")
347
- _turn_reminder(data)
348
- return 0
349
- print(json.dumps(data, indent=2))
350
- _turn_reminder(data)
351
- return 0
352
- print(json.dumps(data, indent=2), file=sys.stderr)
353
- _turn_reminder(data)
354
- return 1 if "error" in data else 2
355
-
356
-
357
- def main():
358
- ap = argparse.ArgumentParser(description="Golem helper for Roblox Studio", prog="golem.py")
359
- ap.add_argument("--timeout", type=float, default=120.0, help="seconds to wait for Studio (default 120)")
360
- sub = ap.add_subparsers(dest="cmd", required=True)
361
-
362
- sub.add_parser("ping", help="health check - is Studio connected?")
363
-
364
- sub.add_parser("debug", help="diagnostics: relay round-trip, marketplace reachability, HTTP state")
365
-
366
- sub.add_parser("status", help="is the plugin alive, paused, or the link revoked? (no Studio needed)")
367
-
368
- p = sub.add_parser("exec", help="send a raw op JSON: {\"op\":..., \"args\":...}")
369
- p.add_argument("json")
370
-
371
- p = sub.add_parser("lua", help="run Lua inside Studio (code arg, or - for stdin)")
372
- p.add_argument("code", nargs="?")
373
- p.add_argument("-", dest="from_stdin", action="store_true")
374
-
375
- p = sub.add_parser("list", help="children of a path")
376
- p.add_argument("path", nargs="?", default="game")
377
- p.add_argument("--recursive", action="store_true")
378
- p.add_argument("--max", type=int)
379
-
380
- p = sub.add_parser("tree", help="recursive tree of a path")
381
- p.add_argument("path", nargs="?", default="game")
382
- p.add_argument("--depth", type=int)
383
-
384
- p = sub.add_parser("read", help="read an instance (script source by default, --json for full record)")
385
- p.add_argument("path")
386
- p.add_argument("--json", action="store_true", dest="as_json")
387
- p.add_argument("--props", help="comma-separated extra properties")
388
-
389
- p = sub.add_parser("find", help="find instances by name or --tag")
390
- p.add_argument("query", nargs="?", default="")
391
- p.add_argument("--class", dest="cls")
392
- p.add_argument("--scope", default="game")
393
- p.add_argument("--max", type=int)
394
- p.add_argument("--exact", action="store_true")
395
- p.add_argument("--tag", help="find by CollectionService tag instead of name")
396
-
397
- p = sub.add_parser("grep", help="search script sources")
398
- p.add_argument("pattern")
399
- p.add_argument("--scope", default="game")
400
- p.add_argument("--max", type=int)
401
- p.add_argument("-i", action="store_true", dest="icase", help="case-insensitive")
402
-
403
- p = sub.add_parser("script", help="create/update a script (source from stdin or --source)")
404
- p.add_argument("parent")
405
- p.add_argument("name")
406
- p.add_argument("--class", dest="cls", default="Script",
407
- choices=["Script", "LocalScript", "ModuleScript"])
408
- p.add_argument("--mode", choices=["create", "update", "replace"], default="create")
409
- p.add_argument("--source")
410
-
411
- p = sub.add_parser("delete", help="delete instances")
412
- p.add_argument("paths", nargs="+")
413
-
414
- p = sub.add_parser("move", help="reparent an instance")
415
- p.add_argument("path")
416
- p.add_argument("parent")
417
-
418
- p = sub.add_parser("selection", help="read or set the Studio selection")
419
- p.add_argument("--set", help="comma-separated paths")
420
- p.add_argument("--clear", action="store_true")
421
-
422
- p = sub.add_parser("waypoint", help="set an undo checkpoint")
423
- p.add_argument("label", nargs="?", default="bridge")
424
-
425
- p = sub.add_parser("say", help="post a message to the user's Studio chat (closes the turn)")
426
- p.add_argument("text")
427
-
428
- p = sub.add_parser("turn", help="mark work turns: begin ... tools ... end --note (required protocol)")
429
- p.add_argument("action", choices=["begin", "end"])
430
- p.add_argument("--note", help="the user-facing reply, posted when the turn ends")
431
-
432
- p = sub.add_parser("rotate", help="rotate: --axis y --degrees 90 (relative) or --set 0,90,0 (absolute)")
433
- p.add_argument("path")
434
- p.add_argument("--axis", help="x|y|z|up|right|forward (or x,y,z vector)")
435
- p.add_argument("--degrees", type=float)
436
- p.add_argument("--set", dest="absolute", help="absolute orientation x,y,z in degrees")
437
- p.add_argument("--space", choices=["world", "local"], default="world")
438
-
439
- p = sub.add_parser("face", help="aim an instance's axis at a world point (keeps position)")
440
- p.add_argument("path")
441
- p.add_argument("target", help="x,y,z world position to face")
442
- p.add_argument("--axis", default="forward", help="forward (default) | up | right")
443
-
444
- p = sub.add_parser("shift", help="move by an offset in studs (world or local)")
445
- p.add_argument("path")
446
- p.add_argument("offset", help="x,y,z studs")
447
- p.add_argument("--space", choices=["world", "local"], default="world")
448
-
449
- p = sub.add_parser("scale", help="scale a part/model by a relative factor")
450
- p.add_argument("path")
451
- p.add_argument("factor", type=float)
452
-
453
- p = sub.add_parser("duplicate", help="clone an instance (optionally N times with spacing)")
454
- p.add_argument("path")
455
- p.add_argument("--count", type=int, default=1)
456
- p.add_argument("--offset", help="x,y,z applied per copy (copy i gets offset*i)")
457
- p.add_argument("--parent")
458
- p.add_argument("--name")
459
-
460
- p = sub.add_parser("group", help="wrap instances into a Model")
461
- p.add_argument("paths", nargs="+")
462
- p.add_argument("--name", default="Group")
463
- p.add_argument("--parent")
464
-
465
- p = sub.add_parser("pivot", help="set a model/part pivot (what it rotates around)")
466
- p.add_argument("path")
467
- p.add_argument("--position", help="x,y,z world position")
468
- p.add_argument("--orientation", help="x,y,z degrees")
469
-
470
- p = sub.add_parser("place", help="absolute position via pivot (parts and models)")
471
- p.add_argument("path")
472
- p.add_argument("position", help="x,y,z world position")
473
- p.add_argument("--orientation", help="x,y,z degrees")
474
-
475
- p = sub.add_parser("paint", help="set color/material/transparency/reflectance on parts")
476
- p.add_argument("paths", nargs="+")
477
- p.add_argument("--color", help="#RRGGBB")
478
- p.add_argument("--material", help="Grass, Neon, WoodPlanks, ...")
479
- p.add_argument("--transparency", type=float)
480
- p.add_argument("--reflectance", type=float)
481
-
482
- p = sub.add_parser("rename", help="rename an instance")
483
- p.add_argument("path")
484
- p.add_argument("name")
485
-
486
- p = sub.add_parser("look", help="aim the Studio editor camera at a path")
487
- p.add_argument("path")
488
- p.add_argument("--distance", type=float, default=30)
489
-
490
- p = sub.add_parser("count", help="count instances (cheap, no payloads)")
491
- p.add_argument("scope", nargs="?", default="game")
492
- p.add_argument("--class", dest="cls")
493
-
494
- sub.add_parser("undo", help="one Studio undo step (Ctrl+Z)")
495
-
496
- p = sub.add_parser("anchor", help="anchor parts in place (models: all their parts)")
497
- p.add_argument("paths", nargs="+")
498
- p.add_argument("--off", action="store_true", help="unanchor instead of anchor")
499
-
500
- p = sub.add_parser("collide", help="enable part collision (models: all their parts)")
501
- p.add_argument("paths", nargs="+")
502
- p.add_argument("--off", action="store_true", help="disable collision instead of enabling")
503
-
504
- p = sub.add_parser("light", help="add or update a light inside a part")
505
- p.add_argument("path")
506
- p.add_argument("--type", dest="light_type", default="point", choices=["point", "spot", "surface"])
507
- p.add_argument("--color")
508
- p.add_argument("--range", type=float)
509
- p.add_argument("--brightness", type=float)
510
- p.add_argument("--shadows", action="store_true")
511
-
512
- p = sub.add_parser("sound", help="add a Sound to a parent, optionally play it")
513
- p.add_argument("parent")
514
- p.add_argument("id")
515
- p.add_argument("--volume", type=float)
516
- p.add_argument("--loop", action="store_true")
517
- p.add_argument("--play", action="store_true")
518
- p.add_argument("--name")
519
-
520
- p = sub.add_parser("weld", help="weld a model's parts together with WeldConstraints")
521
- p.add_argument("paths", nargs="+")
522
-
523
- p = sub.add_parser("hitbox", help="invisible hitbox part sized to a target")
524
- p.add_argument("path")
525
- p.add_argument("--padding", type=float, default=0.5)
526
- p.add_argument("--name")
527
- p.add_argument("--collide", action="store_true", help="make the hitbox collidable")
528
-
529
- p = sub.add_parser("prompt", help="add a ProximityPrompt to a part (Press E to ...)")
530
- p.add_argument("path")
531
- p.add_argument("action")
532
- p.add_argument("--object", help="title above the prompt (e.g. the object's name)")
533
- p.add_argument("--hold", type=float, default=0)
534
- p.add_argument("--distance", type=float, default=8)
535
-
536
- p = sub.add_parser("particles", help="attach a particle preset to a part")
537
- p.add_argument("path")
538
- p.add_argument("preset", choices=["leaves", "sparks", "smoke", "magic", "fire",
539
- "snow", "rain", "bubbles", "dust", "confetti", "fireflies"])
540
- p.add_argument("--rate", type=float)
541
- p.add_argument("--color")
542
-
543
- p = sub.add_parser("sign", help="place a readable wooden sign")
544
- p.add_argument("text")
545
- p.add_argument("--position")
546
- p.add_argument("--parent")
547
- p.add_argument("--size")
548
- p.add_argument("--name")
549
-
550
- p = sub.add_parser("beam", help="glowing beam between two parts")
551
- p.add_argument("from") # keyword name; read with getattr(ns, "from")
552
- p.add_argument("to")
553
- p.add_argument("--color")
554
- p.add_argument("--width", type=float)
555
- p.add_argument("--curve", type=float)
556
- p.add_argument("--name")
557
-
558
- p = sub.add_parser("trail", help="motion trail on a moving part")
559
- p.add_argument("path")
560
- p.add_argument("--color")
561
- p.add_argument("--lifetime", type=float)
562
- p.add_argument("--name")
563
-
564
- p = sub.add_parser("explosion", help="one-shot explosion (visual only)")
565
- p.add_argument("--position")
566
- p.add_argument("--radius", type=float)
567
-
568
- p = sub.add_parser("ui_screen", help="create a ScreenGui under StarterGui")
569
- p.add_argument("name")
570
- p.add_argument("--parent")
571
- p.add_argument("--order", type=int)
572
-
573
- p = sub.add_parser("ui_frame", help="rounded panel/frame")
574
- p.add_argument("parent")
575
- p.add_argument("name")
576
- p.add_argument("--position")
577
- p.add_argument("--size")
578
- p.add_argument("--anchor")
579
- p.add_argument("--color")
580
- p.add_argument("--transparency", type=float)
581
- p.add_argument("--radius", type=int)
582
- p.add_argument("--clip", action="store_true")
583
-
584
- p = sub.add_parser("ui_label", help="text label")
585
- p.add_argument("parent")
586
- p.add_argument("name")
587
- p.add_argument("--text")
588
- p.add_argument("--position")
589
- p.add_argument("--size")
590
- p.add_argument("--anchor")
591
- p.add_argument("--color")
592
- p.add_argument("--align")
593
- p.add_argument("--font")
594
- p.add_argument("--text-size", type=int)
595
- p.add_argument("--wrap", action="store_true")
596
-
597
- p = sub.add_parser("ui_button", help="text button")
598
- p.add_argument("parent")
599
- p.add_argument("name")
600
- p.add_argument("--text")
601
- p.add_argument("--position")
602
- p.add_argument("--size")
603
- p.add_argument("--anchor")
604
- p.add_argument("--color")
605
- p.add_argument("--text-color")
606
- p.add_argument("--radius", type=int)
607
- p.add_argument("--font")
608
- p.add_argument("--text-size", type=int)
609
-
610
- p = sub.add_parser("ui_input", help="TextBox the player can type into")
611
- p.add_argument("parent")
612
- p.add_argument("name")
613
- p.add_argument("--placeholder")
614
- p.add_argument("--text")
615
- p.add_argument("--position")
616
- p.add_argument("--size")
617
- p.add_argument("--color")
618
- p.add_argument("--background")
619
- p.add_argument("--radius", type=int)
620
- p.add_argument("--text-size", type=int)
621
-
622
- p = sub.add_parser("ui_image", help="ImageLabel from a Roblox asset id")
623
- p.add_argument("parent")
624
- p.add_argument("name")
625
- p.add_argument("--asset")
626
- p.add_argument("--position")
627
- p.add_argument("--size")
628
- p.add_argument("--scale")
629
-
630
- p = sub.add_parser("ui_list", help="UIListLayout that auto-arranges a container's children")
631
- p.add_argument("parent")
632
- p.add_argument("--direction")
633
- p.add_argument("--padding", type=int)
634
- p.add_argument("--halign")
635
- p.add_argument("--valign")
636
-
637
- p = sub.add_parser("play", help="start play-testing the game (client when possible, Run mode fallback)")
638
- p.add_argument("--mode", choices=["play", "run"])
639
-
640
- p = sub.add_parser("stop", help="stop the running play test")
641
-
642
- p = sub.add_parser("logs", help="read Studio output - errors and warnings from the play test")
643
- p.add_argument("--all", action="store_true", help="include info/print messages too")
644
- p.add_argument("--limit", type=int)
645
- p.add_argument("--since", type=float, help="unix timestamp floor (default: when the test started)")
646
-
647
- p = sub.add_parser("attr", help="read/set/clear Studio attributes on an instance")
648
- p.add_argument("path")
649
- p.add_argument("--set", action="append", metavar="K=V")
650
- p.add_argument("--clear", action="append", metavar="K")
651
-
652
- p = sub.add_parser("tag", help="add/remove CollectionService tags")
653
- p.add_argument("paths", nargs="+")
654
- p.add_argument("--add", action="append", metavar="TAG")
655
- p.add_argument("--remove", action="append", metavar="TAG")
656
-
657
- p = sub.add_parser("match", help="copy color/material/transparency from one part onto targets")
658
- p.add_argument("from_path")
659
- p.add_argument("to", nargs="+")
660
-
661
- p = sub.add_parser("scatter", help="scatter N copies of a template in a disc around it")
662
- p.add_argument("path")
663
- p.add_argument("--count", type=int, default=10)
664
- p.add_argument("--radius", type=float, default=20)
665
- p.add_argument("--y-jitter", dest="y_jitter", type=float, default=0)
666
- p.add_argument("--parent")
667
- p.add_argument("--name")
668
-
669
- p = sub.add_parser("terrain", help="fill or clear terrain (block or ball)")
670
- p.add_argument("--action", choices=["fill", "clear"], default="fill")
671
- p.add_argument("--shape", choices=["block", "ball"], default="block")
672
- p.add_argument("--position", required=True, help="x,y,z center")
673
- p.add_argument("--size", help="x,y,z (block)")
674
- p.add_argument("--radius", type=float, help="ball radius")
675
- p.add_argument("--material", default="Grass")
676
-
677
- p = sub.add_parser("search", help="search the Roblox Creator Store (models, meshes, images, audio)")
678
- p.add_argument("query")
679
- p.add_argument("--category", default="model", help="model|mesh|image|audio|video|plugin")
680
- p.add_argument("--limit", type=int)
681
- p.add_argument("--cursor", help="nextPageCursor from a previous search")
682
-
683
- p = sub.add_parser("info", help="details + thumbnail for one marketplace asset")
684
-
685
- p.add_argument("id")
686
-
687
- p = sub.add_parser("insert", help="insert a marketplace asset into the place")
688
- p.add_argument("id")
689
- p.add_argument("parent", nargs="?", default="Workspace")
690
- p.add_argument("--name")
691
-
692
- p = sub.add_parser("apply", help="apply an asset id to a property (Image, Texture, SoundId, MeshId...)")
693
- p.add_argument("id")
694
- p.add_argument("path")
695
- p.add_argument("prop")
696
-
697
- ns = ap.parse_args()
698
- cmd = ns.cmd
699
- timeout = ns.timeout
700
-
701
- if cmd == "ping":
702
- sys.exit(_out(call("ping", {}, timeout=max(timeout, 60))))
703
- if cmd == "debug":
704
- sys.exit(_out(call("debug", {}, timeout=max(timeout, 90))))
705
- if cmd == "status":
706
- sys.exit(_out(status()))
707
- if cmd == "exec":
708
- try:
709
- payload = json.loads(ns.json)
710
- except ValueError as exc:
711
- print("invalid JSON: %s" % exc, file=sys.stderr)
712
- sys.exit(2)
713
- if not isinstance(payload, dict) or "op" not in payload:
714
- print('JSON must be an object with an "op" key', file=sys.stderr)
715
- sys.exit(2)
716
- sys.exit(_out(call(payload["op"], payload.get("args") or {}, timeout)))
717
- if cmd == "lua":
718
- code = ns.code
719
- if ns.from_stdin or (code is None and not sys.stdin.isatty()):
720
- code = sys.stdin.read()
721
- if not code:
722
- print("no Lua code given (pass it as an argument or pipe it in)", file=sys.stderr)
723
- sys.exit(2)
724
- sys.exit(_out(call("run", {"code": code}, timeout=timeout)))
725
- if cmd == "list":
726
- args = {"path": ns.path}
727
- if ns.recursive:
728
- args["recursive"] = True
729
- if ns.max:
730
- args["max"] = ns.max
731
- sys.exit(_out(call("list", args, timeout)))
732
- if cmd == "tree":
733
- args = {"path": ns.path, "depth": ns.depth or 2}
734
- sys.exit(_out(call("tree", args, timeout)))
735
- if cmd == "read":
736
- args = {"path": ns.path}
737
- if ns.props:
738
- args["props"] = [p.strip() for p in ns.props.split(",") if p.strip()]
739
- sys.exit(_out(call("read", args, timeout), as_json=ns.as_json, raw_field="source"))
740
- if cmd == "find":
741
- args = {"scope": ns.scope}
742
- if ns.tag:
743
- args["tag"] = ns.tag
744
- else:
745
- args["query"] = ns.query
746
- if ns.cls:
747
- args["class"] = ns.cls
748
- if ns.max:
749
- args["max"] = ns.max
750
- if ns.exact:
751
- args["exact"] = True
752
- sys.exit(_out(call("find", args, timeout)))
753
- if cmd == "grep":
754
- args = {"pattern": ns.pattern, "scope": ns.scope}
755
- if ns.max:
756
- args["max"] = ns.max
757
- if ns.icase:
758
- args["caseSensitive"] = False
759
- sys.exit(_out(call("grep", args, timeout)))
760
- if cmd == "script":
761
- source = ns.source
762
- if source is None and not sys.stdin.isatty():
763
- source = sys.stdin.read()
764
- if source is None:
765
- print("pass the script source via stdin (heredoc/pipe) or --source", file=sys.stderr)
766
- sys.exit(2)
767
- sys.exit(_out(call("script", {
768
- "parent": ns.parent, "name": ns.name, "class": ns.cls, "mode": ns.mode, "source": source,
769
- }, timeout)))
770
- if cmd == "delete":
771
- sys.exit(_out(call("delete", {"paths": ns.paths}, timeout)))
772
- if cmd == "move":
773
- sys.exit(_out(call("move", {"path": ns.path, "parent": ns.parent}, timeout)))
774
- if cmd == "selection":
775
- args = {}
776
- if ns.clear:
777
- args["clear"] = True
778
- elif ns.set:
779
- args["set"] = [p.strip() for p in ns.set.split(",") if p.strip()]
780
- sys.exit(_out(call("selection", args, timeout)))
781
- if cmd == "search":
782
- if not _marketplace_via_plugin():
783
- err = None
784
- try:
785
- result = marketplace_search(ns.query, ns.category, ns.limit, ns.cursor)
786
- except Exception as exc:
787
- result, err = None, "marketplace search failed: %s" % exc
788
- if err is None:
789
- sys.exit(_out({"ok": True, "op": "search", "result": result}))
790
- sys.exit(_out({"ok": False, "op": "search", "error": err}))
791
- args = {"query": ns.query, "category": ns.category}
792
- if ns.limit:
793
- args["limit"] = ns.limit
794
- if ns.cursor:
795
- args["cursor"] = ns.cursor
796
- sys.exit(_out(call("search_assets", args, timeout=max(timeout, 180))))
797
- if cmd == "info":
798
- if not _marketplace_via_plugin():
799
- err = None
800
- try:
801
- result = marketplace_info(ns.id)
802
- except Exception as exc:
803
- result, err = None, "asset info failed: %s" % exc
804
- if err is None:
805
- sys.exit(_out({"ok": True, "op": "info", "result": result}))
806
- sys.exit(_out({"ok": False, "op": "info", "error": err}))
807
- sys.exit(_out(call("asset_info", {"id": ns.id}, timeout=max(timeout, 180))))
808
- if cmd == "insert":
809
- args = {"id": ns.id, "parent": ns.parent}
810
- if ns.name:
811
- args["name"] = ns.name
812
- sys.exit(_out(call("insert_asset", args, timeout=max(timeout, 300))))
813
- if cmd == "apply":
814
- sys.exit(_out(call("apply_asset", {"id": ns.id, "path": ns.path, "prop": ns.prop}, timeout=timeout)))
815
- if cmd == "say":
816
- sys.exit(_out(call("say", {"text": ns.text}, timeout)))
817
- if cmd == "turn":
818
- if ns.action == "begin":
819
- sys.exit(_out(call("turn_begin", {}, timeout)))
820
- args = {}
821
- if ns.note:
822
- args["note"] = ns.note
823
- sys.exit(_out(call("turn_end", args, timeout)))
824
- if cmd == "rotate":
825
- args = {"path": ns.path, "space": ns.space}
826
- if ns.absolute is not None:
827
- args["orientation"] = vec3(ns.absolute)
828
- else:
829
- args["axis"] = ns.axis
830
- args["degrees"] = ns.degrees
831
- sys.exit(_out(call("rotate", args, timeout)))
832
- if cmd == "face":
833
- sys.exit(_out(call("face", {"path": ns.path, "target": vec3(ns.target), "axis": ns.axis}, timeout)))
834
- if cmd == "shift":
835
- sys.exit(_out(call("shift", {"path": ns.path, "offset": vec3(ns.offset), "space": ns.space}, timeout)))
836
- if cmd == "scale":
837
- sys.exit(_out(call("scale", {"path": ns.path, "factor": ns.factor}, timeout)))
838
- if cmd == "duplicate":
839
- args = {"path": ns.path, "count": ns.count}
840
- if ns.offset:
841
- args["offset"] = vec3(ns.offset)
842
- if ns.parent:
843
- args["parent"] = ns.parent
844
- if ns.name:
845
- args["name"] = ns.name
846
- sys.exit(_out(call("duplicate", args, timeout)))
847
- if cmd == "group":
848
- args = {"paths": ns.paths, "name": ns.name}
849
- if ns.parent:
850
- args["parent"] = ns.parent
851
- sys.exit(_out(call("group", args, timeout)))
852
- if cmd == "pivot":
853
- args = {"path": ns.path}
854
- if ns.position:
855
- args["position"] = vec3(ns.position)
856
- if ns.orientation:
857
- args["orientation"] = vec3(ns.orientation)
858
- sys.exit(_out(call("set_pivot", args, timeout)))
859
- if cmd == "terrain":
860
- args = {"action": ns.action, "shape": ns.shape,
861
- "position": vec3(ns.position), "material": ns.material}
862
- if ns.shape == "block":
863
- if not ns.size:
864
- print("--size x,y,z is required for block", file=sys.stderr)
865
- sys.exit(2)
866
- args["size"] = vec3(ns.size)
867
- else:
868
- if ns.radius is None:
869
- print("--radius is required for ball", file=sys.stderr)
870
- sys.exit(2)
871
- args["radius"] = ns.radius
872
- sys.exit(_out(call("terrain", args, timeout)))
873
- if cmd == "place":
874
- args = {"path": ns.path, "position": vec3(ns.position)}
875
- if ns.orientation:
876
- args["orientation"] = vec3(ns.orientation)
877
- sys.exit(_out(call("place", args, timeout)))
878
- if cmd == "paint":
879
- args = {"paths": ns.paths}
880
- if ns.color:
881
- args["color"] = ns.color
882
- if ns.material:
883
- args["material"] = ns.material
884
- if ns.transparency is not None:
885
- args["transparency"] = ns.transparency
886
- if ns.reflectance is not None:
887
- args["reflectance"] = ns.reflectance
888
- sys.exit(_out(call("paint", args, timeout)))
889
- if cmd == "rename":
890
- sys.exit(_out(call("rename", {"path": ns.path, "name": ns.name}, timeout)))
891
- if cmd == "look":
892
- sys.exit(_out(call("look", {"path": ns.path, "distance": ns.distance}, timeout)))
893
- if cmd == "count":
894
- args = {"scope": ns.scope}
895
- if ns.cls:
896
- args["class"] = ns.cls
897
- sys.exit(_out(call("count", args, timeout)))
898
- if cmd == "undo":
899
- sys.exit(_out(call("undo", {}, timeout)))
900
- if cmd == "anchor":
901
- sys.exit(_out(call("anchor", {"paths": ns.paths, "anchored": not ns.off}, timeout)))
902
- if cmd == "collide":
903
- sys.exit(_out(call("collide", {"paths": ns.paths, "canCollide": not ns.off}, timeout)))
904
- if cmd == "light":
905
- args = {"path": ns.path, "type": ns.light_type}
906
- if ns.color:
907
- args["color"] = ns.color
908
- if ns.range is not None:
909
- args["range"] = ns.range
910
- if ns.brightness is not None:
911
- args["brightness"] = ns.brightness
912
- if ns.shadows:
913
- args["shadows"] = True
914
- sys.exit(_out(call("light", args, timeout)))
915
- if cmd == "sound":
916
- args = {"parent": ns.parent, "id": ns.id}
917
- if ns.volume is not None:
918
- args["volume"] = ns.volume
919
- if ns.loop:
920
- args["looped"] = True
921
- if ns.play:
922
- args["play"] = True
923
- if ns.name:
924
- args["name"] = ns.name
925
- sys.exit(_out(call("sound", args, timeout)))
926
- if cmd == "weld":
927
- sys.exit(_out(call("weld", {"paths": ns.paths}, timeout)))
928
- if cmd == "hitbox":
929
- args = {"path": ns.path, "padding": ns.padding}
930
- if ns.name:
931
- args["name"] = ns.name
932
- if ns.collide:
933
- args["canCollide"] = True
934
- sys.exit(_out(call("hitbox", args, timeout)))
935
- if cmd == "prompt":
936
- args = {"path": ns.path, "action": ns.action, "hold": ns.hold, "distance": ns.distance}
937
- if ns.object:
938
- args["object"] = ns.object
939
- sys.exit(_out(call("prompt", args, timeout)))
940
- if cmd == "particles":
941
- args = {"path": ns.path, "preset": ns.preset}
942
- if ns.rate is not None:
943
- args["rate"] = ns.rate
944
- if ns.color:
945
- args["color"] = ns.color
946
- sys.exit(_out(call("particles", args, timeout)))
947
- if cmd == "sign":
948
- args = {"text": ns.text}
949
- if ns.position:
950
- args["position"] = vec3(ns.position)
951
- if ns.parent:
952
- args["parent"] = ns.parent
953
- if ns.size:
954
- args["size"] = vec3(ns.size)
955
- if ns.name:
956
- args["name"] = ns.name
957
- sys.exit(_out(call("sign", args, timeout)))
958
- if cmd == "beam":
959
- args = {"from": getattr(ns, "from"), "to": ns.to}
960
- for k in ("color", "width", "curve", "name"):
961
- v = getattr(ns, k, None)
962
- if v is not None:
963
- args[k] = v
964
- sys.exit(_out(call("beam", args, timeout)))
965
- if cmd == "trail":
966
- args = {"path": ns.path}
967
- for k in ("color", "lifetime", "name"):
968
- v = getattr(ns, k, None)
969
- if v is not None:
970
- args[k] = v
971
- sys.exit(_out(call("trail", args, timeout)))
972
- if cmd == "explosion":
973
- args = {}
974
- if ns.position:
975
- args["position"] = vec3(ns.position)
976
- if ns.radius is not None:
977
- args["radius"] = ns.radius
978
- sys.exit(_out(call("explosion", args, timeout)))
979
- if cmd == "ui_screen":
980
- args = {"name": ns.name}
981
- if ns.parent:
982
- args["parent"] = ns.parent
983
- if ns.order is not None:
984
- args["order"] = ns.order
985
- sys.exit(_out(call("ui_screen", args, timeout)))
986
- if cmd == "ui_frame":
987
- args = {"parent": ns.parent, "name": ns.name}
988
- for k in ("position", "size", "anchor", "color", "transparency", "radius"):
989
- v = getattr(ns, k, None)
990
- if v is not None:
991
- args[k] = v
992
- if ns.clip:
993
- args["clip"] = True
994
- sys.exit(_out(call("ui_frame", args, timeout)))
995
- if cmd == "ui_label":
996
- args = {"parent": ns.parent, "name": ns.name}
997
- for k in ("text", "position", "size", "anchor", "color", "align", "font", "text_size"):
998
- v = getattr(ns, k, None)
999
- if v is not None:
1000
- args[k] = v
1001
- if ns.wrap:
1002
- args["wrap"] = True
1003
- sys.exit(_out(call("ui_label", args, timeout)))
1004
- if cmd == "ui_button":
1005
- args = {"parent": ns.parent, "name": ns.name}
1006
- for k in ("text", "position", "size", "anchor", "color", "text_color", "radius", "font", "text_size"):
1007
- v = getattr(ns, k, None)
1008
- if v is not None:
1009
- args[k] = v
1010
- sys.exit(_out(call("ui_button", args, timeout)))
1011
- if cmd == "ui_input":
1012
- args = {"parent": ns.parent, "name": ns.name}
1013
- for k in ("placeholder", "text", "position", "size", "color", "background", "radius", "text_size"):
1014
- v = getattr(ns, k, None)
1015
- if v is not None:
1016
- args[k] = v
1017
- sys.exit(_out(call("ui_input", args, timeout)))
1018
- if cmd == "ui_image":
1019
- args = {"parent": ns.parent, "name": ns.name}
1020
- if ns.asset:
1021
- args["asset"] = ns.asset
1022
- for k in ("position", "size", "scale"):
1023
- v = getattr(ns, k, None)
1024
- if v is not None:
1025
- args[k] = v
1026
- sys.exit(_out(call("ui_image", args, timeout)))
1027
- if cmd == "ui_list":
1028
- args = {"parent": ns.parent}
1029
- for k in ("direction", "padding", "halign", "valign"):
1030
- v = getattr(ns, k, None)
1031
- if v is not None:
1032
- args[k] = v
1033
- sys.exit(_out(call("ui_list", args, timeout)))
1034
- if cmd == "play":
1035
- args = {}
1036
- if ns.mode:
1037
- args["mode"] = ns.mode
1038
- sys.exit(_out(call("play", args, timeout)))
1039
- if cmd == "stop":
1040
- sys.exit(_out(call("stop", {}, timeout)))
1041
- if cmd == "logs":
1042
- args = {"filter": "all"} if ns.all else {}
1043
- if ns.limit is not None:
1044
- args["limit"] = ns.limit
1045
- if ns.since is not None:
1046
- args["since"] = ns.since
1047
- sys.exit(_out(call("logs", args, timeout)))
1048
- if cmd == "attr":
1049
- args = {"path": ns.path}
1050
- if ns.set:
1051
- parsed = {}
1052
- for kv in ns.set:
1053
- if "=" not in kv:
1054
- print("--set expects K=V", file=sys.stderr)
1055
- sys.exit(2)
1056
- k, v = kv.split("=", 1)
1057
- try:
1058
- v = int(v)
1059
- except ValueError:
1060
- try:
1061
- v = float(v)
1062
- except ValueError:
1063
- if v == "true":
1064
- v = True
1065
- elif v == "false":
1066
- v = False
1067
- parsed[k] = v
1068
- args["set"] = parsed
1069
- if ns.clear:
1070
- args["clear"] = ns.clear
1071
- sys.exit(_out(call("attributes", args, timeout)))
1072
- if cmd == "tag":
1073
- args = {"paths": ns.paths}
1074
- if ns.add:
1075
- args["add"] = ns.add
1076
- if ns.remove:
1077
- args["remove"] = ns.remove
1078
- sys.exit(_out(call("tag", args, timeout)))
1079
- if cmd == "match":
1080
- sys.exit(_out(call("match", {"from": ns.from_path, "paths": ns.to}, timeout)))
1081
- if cmd == "scatter":
1082
- args = {"path": ns.path, "count": ns.count, "radius": ns.radius, "yJitter": ns.y_jitter}
1083
- if ns.parent:
1084
- args["parent"] = ns.parent
1085
- if ns.name:
1086
- args["name"] = ns.name
1087
- sys.exit(_out(call("scatter", args, timeout)))
1088
- if cmd == "waypoint":
1089
- sys.exit(_out(call("waypoint", {"label": ns.label}, timeout)))
1090
-
1091
-
1092
- if __name__ == "__main__":
1093
- main()