golem-bridge 1.0.2 → 2.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/README.md +20 -12
- package/cli.js +100 -52
- package/golem.md +683 -0
- package/golem.py +1093 -0
- package/package.json +2 -2
package/golem.py
ADDED
|
@@ -0,0 +1,1093 @@
|
|
|
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()
|