@el4cteo/rbx-studio-mcp 0.8.0 → 0.8.3
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 +5 -3
- package/dist/bridge/api.js +2 -2
- package/dist/bridge/api.js.map +1 -1
- package/dist/bridge/failover.js +12 -4
- package/dist/bridge/failover.js.map +1 -1
- package/dist/bridge/harness.js +48 -6
- package/dist/bridge/harness.js.map +1 -1
- package/dist/bridge/remote.js +4 -2
- package/dist/bridge/remote.js.map +1 -1
- package/dist/bridge/rpc.js +43 -5
- package/dist/bridge/rpc.js.map +1 -1
- package/dist/bridge/server.js +26 -1
- package/dist/bridge/server.js.map +1 -1
- package/dist/lib/apidump.js +82 -15
- package/dist/lib/apidump.js.map +1 -1
- package/dist/lib/cloudassets.js +2 -2
- package/dist/lib/cloudassets.js.map +1 -1
- package/dist/lib/livedata.js +24 -1
- package/dist/lib/livedata.js.map +1 -1
- package/dist/lib/liveops.js +79 -0
- package/dist/lib/liveops.js.map +1 -1
- package/dist/lib/monetization.js +119 -0
- package/dist/lib/monetization.js.map +1 -0
- package/dist/lib/opencloud.js +16 -6
- package/dist/lib/opencloud.js.map +1 -1
- package/dist/lib/png.js +19 -31
- package/dist/lib/png.js.map +1 -1
- package/dist/tools/audio.js +1 -1
- package/dist/tools/audio.js.map +1 -1
- package/dist/tools/device.js +8 -7
- package/dist/tools/device.js.map +1 -1
- package/dist/tools/discover.js +86 -5
- package/dist/tools/discover.js.map +1 -1
- package/dist/tools/instances.js +3 -1
- package/dist/tools/instances.js.map +1 -1
- package/dist/tools/perf.js +27 -5
- package/dist/tools/perf.js.map +1 -1
- package/dist/tools/playtest.js +35 -10
- package/dist/tools/playtest.js.map +1 -1
- package/dist/tools/session.js +11 -3
- package/dist/tools/session.js.map +1 -1
- package/dist/tools/terrain.js +2 -2
- package/dist/tools/terrain.js.map +1 -1
- package/dist/tools/universe.js +136 -11
- package/dist/tools/universe.js.map +1 -1
- package/dist/tools/world.js +30 -7
- package/dist/tools/world.js.map +1 -1
- package/package.json +4 -4
- package/plugin/src/ClientRelay.luau +20 -2
- package/plugin/src/Config.luau +1 -1
- package/plugin/src/Emulation.luau +44 -16
- package/plugin/src/ExecRuntime.luau +168 -161
- package/plugin/src/LogBuffer.luau +58 -3
- package/plugin/src/Phrase.luau +2 -0
- package/plugin/src/handlers/Assets.luau +27 -18
- package/plugin/src/handlers/Audio.luau +36 -15
- package/plugin/src/handlers/Character.luau +38 -23
- package/plugin/src/handlers/Data.luau +12 -1
- package/plugin/src/handlers/Debug.luau +42 -0
- package/plugin/src/handlers/Device.luau +38 -27
- package/plugin/src/handlers/Exec.luau +5 -1
- package/plugin/src/handlers/Generate.luau +8 -5
- package/plugin/src/handlers/Geometry.luau +7 -5
- package/plugin/src/handlers/Perf.luau +1 -0
- package/plugin/src/handlers/Playtest.luau +40 -3
- package/plugin/src/handlers/Spatial.luau +34 -0
- package/plugin/src/handlers/Terrain.luau +19 -24
- package/plugin/src/handlers/Viewport.luau +37 -5
- package/plugin/src/handlers/World.luau +38 -2
- package/plugin/src/init.server.luau +11 -0
- package/scripts/check-plugin.mjs +54 -0
- package/scripts/test-apidump.mjs +170 -0
- package/scripts/test-bridge.mjs +111 -4
- package/scripts/test-live-tools.mjs +382 -0
- package/scripts/test-tools.mjs +222 -0
|
@@ -75,34 +75,62 @@ local memoryCap: number? = nil
|
|
|
75
75
|
local NETWORK_FIELDS = {
|
|
76
76
|
{ key = "inLatency", property = "InboundNetworkMinDelayMs", scale = 1 },
|
|
77
77
|
{ key = "inJitter", property = "InboundNetworkJitterMs", scale = 1 },
|
|
78
|
-
{ key = "inLoss", property = "InboundNetworkLossPercent", scale =
|
|
78
|
+
{ key = "inLoss", property = "InboundNetworkLossPercent", scale = 1 },
|
|
79
79
|
{ key = "outLatency", property = "OutboundNetworkMinDelayMs", scale = 1 },
|
|
80
80
|
{ key = "outJitter", property = "OutboundNetworkJitterMs", scale = 1 },
|
|
81
|
-
{ key = "outLoss", property = "OutboundNetworkLossPercent", scale =
|
|
81
|
+
{ key = "outLoss", property = "OutboundNetworkLossPercent", scale = 1 },
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
--[[
|
|
85
85
|
What the engine will actually accept, measured rather than assumed.
|
|
86
86
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
reporting 0.5 back and the only way to notice was to read the value again --
|
|
90
|
-
which is how this was found.
|
|
87
|
+
Writes outside these ranges SUCCEED and are silently clamped, so the only
|
|
88
|
+
way to notice is to read the value again.
|
|
91
89
|
|
|
92
|
-
* Loss is a
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
90
|
+
* Loss is a PERCENT, as the name says, and Roblox caps it at 0.5% "to
|
|
91
|
+
prevent interfering with standard congestion control" (the Network
|
|
92
|
+
Simulation announcement; its own "Poor" profile is 0.5%). This file once
|
|
93
|
+
read the 0.5 ceiling as a fraction -- half of all packets -- and divided
|
|
94
|
+
every request by 100, so "0.5% loss" applied 0.005% while the reply,
|
|
95
|
+
scaled back up, still said 0.5.
|
|
97
96
|
* Delay and jitter both stop at 1000ms. A second of latency is already past
|
|
98
97
|
anything a real connection does, so this costs nothing but honesty.
|
|
99
98
|
]]
|
|
100
|
-
local
|
|
99
|
+
local LOSS_LIMIT_PERCENT = 0.5
|
|
101
100
|
local DELAY_LIMIT = 1000
|
|
102
101
|
|
|
103
|
-
Emulation.LOSS_LIMIT_PERCENT =
|
|
102
|
+
Emulation.LOSS_LIMIT_PERCENT = LOSS_LIMIT_PERCENT
|
|
104
103
|
Emulation.DELAY_LIMIT = DELAY_LIMIT
|
|
105
104
|
|
|
105
|
+
--[[
|
|
106
|
+
The machine's real memory, read before this plugin could have capped it.
|
|
107
|
+
|
|
108
|
+
`EmulatedTotalMemoryInMB` defaults to the physical memory and 0 is a real
|
|
109
|
+
value, not "off": clearing a cap by writing 0 left Studio emulating a
|
|
110
|
+
device with no memory at all. Removing a cap therefore means writing this
|
|
111
|
+
back. Read once at load, which is before anything here writes it.
|
|
112
|
+
]]
|
|
113
|
+
local machineMemory: number? = nil
|
|
114
|
+
do
|
|
115
|
+
local service = networkSettings()
|
|
116
|
+
if service ~= nil then
|
|
117
|
+
local ok, value = pcall(function()
|
|
118
|
+
return service.EmulatedTotalMemoryInMB
|
|
119
|
+
end)
|
|
120
|
+
if ok and tonumber(value) and (tonumber(value) :: number) > 0 then
|
|
121
|
+
machineMemory = tonumber(value)
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
function Emulation.machineMemory(): number?
|
|
127
|
+
return machineMemory
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
function Emulation.memoryCap(): number?
|
|
131
|
+
return memoryCap
|
|
132
|
+
end
|
|
133
|
+
|
|
106
134
|
--[[
|
|
107
135
|
What Studio is currently doing to network traffic.
|
|
108
136
|
|
|
@@ -125,9 +153,9 @@ function Emulation.network(): { [string]: any }
|
|
|
125
153
|
local number = if ok then tonumber(value) or 0 else 0
|
|
126
154
|
--[[
|
|
127
155
|
Scaled into the caller's units, then rounded to two places. These are
|
|
128
|
-
32-bit floats:
|
|
129
|
-
|
|
130
|
-
|
|
156
|
+
32-bit floats: 0.3% loss reads back as 0.30000001192092896, and
|
|
157
|
+
reporting that instead of 0.3 makes a perfectly applied setting look
|
|
158
|
+
like a failed one.
|
|
131
159
|
]]
|
|
132
160
|
number *= field.scale
|
|
133
161
|
state[field.key] = math.round(number * 100) / 100
|
|
@@ -1,161 +1,168 @@
|
|
|
1
|
-
--!strict
|
|
2
|
-
-- Shared bounded execution results in the VM that calls run.
|
|
3
|
-
local LogService = game:GetService("LogService")
|
|
4
|
-
local
|
|
5
|
-
|
|
6
|
-
--
|
|
7
|
-
|
|
8
|
-
local
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
--
|
|
12
|
-
--
|
|
13
|
-
|
|
14
|
-
local
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
local
|
|
19
|
-
local
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
--
|
|
31
|
-
--
|
|
32
|
-
--
|
|
33
|
-
--
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
--
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
--
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
local
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
Runtime
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
--
|
|
83
|
-
--
|
|
84
|
-
|
|
85
|
-
local
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
return
|
|
1
|
+
--!strict
|
|
2
|
+
-- Shared bounded execution results in the VM that calls run.
|
|
3
|
+
local LogService = game:GetService("LogService")
|
|
4
|
+
local LogBuffer = require(script.Parent.LogBuffer)
|
|
5
|
+
local Serialize = require(script.Parent.Serialize)
|
|
6
|
+
-- A chunk that prints in a loop would otherwise return a response no context
|
|
7
|
+
-- window can hold.
|
|
8
|
+
local MAX_OUTPUT_LINES = 200
|
|
9
|
+
local MAX_RETURN_VALUES = 10
|
|
10
|
+
|
|
11
|
+
-- Returned tables are walked rather than counted, but not without limit: a
|
|
12
|
+
-- chunk that returns a whole config tree, or one with a cycle in it, must not be
|
|
13
|
+
-- able to produce a response nothing can read.
|
|
14
|
+
local MAX_TABLE_ENTRIES = 50
|
|
15
|
+
local MAX_TABLE_DEPTH = 4
|
|
16
|
+
|
|
17
|
+
local function describe(value: any, depth: number?, seen: { [any]: boolean }?): any
|
|
18
|
+
local level = depth or 0
|
|
19
|
+
local visited = seen or {}
|
|
20
|
+
local kind = typeof(value)
|
|
21
|
+
|
|
22
|
+
if kind == "function" or kind == "thread" then
|
|
23
|
+
return string.format("<%s>", kind)
|
|
24
|
+
end
|
|
25
|
+
if kind ~= "table" then
|
|
26
|
+
return Serialize.value(value)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
local source = value :: { [any]: any }
|
|
30
|
+
-- Named separately from the depth cap, because they mean opposite things to
|
|
31
|
+
-- a reader. The depth cap says "there is more below this"; a cycle says
|
|
32
|
+
-- "this is the table you are already inside". Reporting the second as the
|
|
33
|
+
-- first printed `t.self.self.self` as three distinct nested tables, which
|
|
34
|
+
-- reads as real structure that does not exist.
|
|
35
|
+
if visited[source] then
|
|
36
|
+
return "<circular reference>"
|
|
37
|
+
end
|
|
38
|
+
if level >= MAX_TABLE_DEPTH then
|
|
39
|
+
return "<nested table>"
|
|
40
|
+
end
|
|
41
|
+
visited[source] = true
|
|
42
|
+
|
|
43
|
+
local count = 0
|
|
44
|
+
for _ in source do
|
|
45
|
+
count += 1
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
-- Arrays keep their shape so they read as lists on the other side; anything
|
|
49
|
+
-- with non-sequential keys becomes a map with stringified keys.
|
|
50
|
+
if count == #source then
|
|
51
|
+
local list: { any } = {}
|
|
52
|
+
for index, item in ipairs(source) do
|
|
53
|
+
if index > MAX_TABLE_ENTRIES then
|
|
54
|
+
table.insert(list, string.format("<%d more>", count - MAX_TABLE_ENTRIES))
|
|
55
|
+
break
|
|
56
|
+
end
|
|
57
|
+
table.insert(list, describe(item, level + 1, visited))
|
|
58
|
+
end
|
|
59
|
+
-- Cleared on the way out so the same table appearing twice side by side
|
|
60
|
+
-- still renders twice; only a table containing itself is a cycle.
|
|
61
|
+
visited[source] = nil
|
|
62
|
+
return list
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
local map: { [string]: any } = {}
|
|
66
|
+
local shown = 0
|
|
67
|
+
for key, item in source do
|
|
68
|
+
shown += 1
|
|
69
|
+
if shown > MAX_TABLE_ENTRIES then
|
|
70
|
+
map["..."] = string.format("<%d more>", count - MAX_TABLE_ENTRIES)
|
|
71
|
+
break
|
|
72
|
+
end
|
|
73
|
+
map[tostring(key)] = describe(item, level + 1, visited)
|
|
74
|
+
end
|
|
75
|
+
visited[source] = nil
|
|
76
|
+
return map
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
local Runtime = {}
|
|
80
|
+
Runtime.describe = describe
|
|
81
|
+
function Runtime.run(chunk: () -> ...any, timeout: number?): { [string]: any }
|
|
82
|
+
-- Captured rather than inferred: a chunk's prints are usually the point, and
|
|
83
|
+
-- reading them back from the log afterwards would also pick up whatever else
|
|
84
|
+
-- the place logged in the meantime.
|
|
85
|
+
local output: { { [string]: any } } = {}
|
|
86
|
+
local connection = LogService.MessageOut:Connect(function(message, messageType, context)
|
|
87
|
+
if #output < MAX_OUTPUT_LINES then
|
|
88
|
+
-- A structured log's context rides on the line, as it does in `console`.
|
|
89
|
+
local encoded = LogBuffer.encodeContext(context)
|
|
90
|
+
if encoded then
|
|
91
|
+
message ..= " " .. encoded
|
|
92
|
+
end
|
|
93
|
+
table.insert(output, {
|
|
94
|
+
message = message,
|
|
95
|
+
level = if messageType == Enum.MessageType.MessageError
|
|
96
|
+
then "error"
|
|
97
|
+
elseif messageType == Enum.MessageType.MessageWarning then "warning"
|
|
98
|
+
elseif messageType == Enum.MessageType.MessageInfo then "info"
|
|
99
|
+
else "print",
|
|
100
|
+
})
|
|
101
|
+
end
|
|
102
|
+
end)
|
|
103
|
+
|
|
104
|
+
local started = os.clock()
|
|
105
|
+
local results: any = nil
|
|
106
|
+
if timeout then
|
|
107
|
+
local worker = task.spawn(function() results = table.pack(pcall(chunk :: any)) end)
|
|
108
|
+
local deadline = started + timeout
|
|
109
|
+
while results == nil and os.clock() < deadline do task.wait() end
|
|
110
|
+
if results == nil then
|
|
111
|
+
task.cancel(worker)
|
|
112
|
+
results = table.pack(false, "CLIENT_TIMEOUT: execution exceeded " .. tostring(timeout) .. "s")
|
|
113
|
+
end
|
|
114
|
+
else
|
|
115
|
+
results = table.pack(pcall(chunk :: () -> ...any))
|
|
116
|
+
end
|
|
117
|
+
local elapsed = os.clock() - started
|
|
118
|
+
|
|
119
|
+
--[[
|
|
120
|
+
MessageOut is deferred: `print` returns before the event fires, so
|
|
121
|
+
disconnecting straight after the chunk finished captured nothing at all.
|
|
122
|
+
A short settle picks the lines up.
|
|
123
|
+
|
|
124
|
+
It is bounded and stops as soon as the flow stops, rather than waiting a
|
|
125
|
+
fixed period, so a chunk that logged nothing costs almost no time and one
|
|
126
|
+
that logged plenty is not truncated.
|
|
127
|
+
]]
|
|
128
|
+
local settleDeadline = os.clock() + 0.5
|
|
129
|
+
local seen = #output
|
|
130
|
+
local quietFrames = 0
|
|
131
|
+
while os.clock() < settleDeadline and quietFrames < 3 do
|
|
132
|
+
task.wait()
|
|
133
|
+
if #output == seen then
|
|
134
|
+
quietFrames += 1
|
|
135
|
+
else
|
|
136
|
+
seen = #output
|
|
137
|
+
quietFrames = 0
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
connection:Disconnect()
|
|
142
|
+
|
|
143
|
+
if not results[1] then
|
|
144
|
+
return {
|
|
145
|
+
ok = false,
|
|
146
|
+
error = tostring(results[2]),
|
|
147
|
+
output = output,
|
|
148
|
+
milliseconds = math.floor(elapsed * 1000 + 0.5),
|
|
149
|
+
}
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
local returned: { any } = {}
|
|
153
|
+
for index = 2, math.min(results.n, MAX_RETURN_VALUES + 1) do
|
|
154
|
+
-- A nil return is written out rather than dropped. `table.insert` of nil
|
|
155
|
+
-- does nothing, so `return a, nil, b` came back as two values and every
|
|
156
|
+
-- one after the nil shifted into the wrong position.
|
|
157
|
+
local value = results[index]
|
|
158
|
+
table.insert(returned, if value == nil then "nil" else describe(value))
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
ok = true,
|
|
163
|
+
returned = returned,
|
|
164
|
+
output = output,
|
|
165
|
+
milliseconds = math.floor(elapsed * 1000 + 0.5),
|
|
166
|
+
}
|
|
167
|
+
end
|
|
168
|
+
return Runtime
|
|
@@ -35,6 +35,10 @@ local SLACK = 500
|
|
|
35
35
|
-- handful of entries is a mismatch rather than a late delivery.
|
|
36
36
|
local CORRELATION_WINDOW = 20
|
|
37
37
|
|
|
38
|
+
-- A structured log's context, as JSON, is cut here: Roblox's own server logs
|
|
39
|
+
-- drop context past 1KiB, so nothing longer is worth carrying.
|
|
40
|
+
local MAX_CONTEXT = 1000
|
|
41
|
+
|
|
38
42
|
export type Entry = {
|
|
39
43
|
sequence: number?,
|
|
40
44
|
level: string,
|
|
@@ -43,6 +47,8 @@ export type Entry = {
|
|
|
43
47
|
-- Present on errors once the matching stack trace has been correlated.
|
|
44
48
|
stack: string?,
|
|
45
49
|
source: string?,
|
|
50
|
+
-- The table a structured log (`LogService:Info(msg, context)`) carried, as JSON.
|
|
51
|
+
context: string?,
|
|
46
52
|
}
|
|
47
53
|
|
|
48
54
|
local LEVELS: { [number]: string } = {
|
|
@@ -153,6 +159,7 @@ function LogBuffer.pushClient(player: Player, payload: any)
|
|
|
153
159
|
timestamp = if typeof(payload.timestamp) == "number" and payload.timestamp >= 0 and payload.timestamp < 100000000000 then payload.timestamp else os.time(),
|
|
154
160
|
stack = if typeof(payload.stack) == "string" then string.sub(payload.stack, 1, 4000) else nil,
|
|
155
161
|
source = if typeof(payload.source) == "string" then string.sub(payload.source, 1, 500) else nil,
|
|
162
|
+
context = if typeof(payload.context) == "string" then string.sub(payload.context, 1, MAX_CONTEXT) else nil,
|
|
156
163
|
}
|
|
157
164
|
local waiting = buffer.orphan[entry.message]
|
|
158
165
|
if level == "error" and waiting then
|
|
@@ -183,12 +190,60 @@ function LogBuffer.stream(player: Player?): ({ Entry }, number, string, number,
|
|
|
183
190
|
return table.move(entries, 1, #entries, 1, {}), dropped, studioToken, nextSequence, LogBuffer.recordingSeconds()
|
|
184
191
|
end
|
|
185
192
|
|
|
186
|
-
|
|
193
|
+
--[[
|
|
194
|
+
The context table of a structured log, as compact JSON.
|
|
195
|
+
|
|
196
|
+
`LogService:Info("{player} leveled up", { player = "Bob" })` prints only the
|
|
197
|
+
filled-in message; the table is the part that says which player and what
|
|
198
|
+
state, and MessageOut hands it over as a third argument. Plain prints pass an
|
|
199
|
+
empty table, which is dropped. Capped at 1000 characters -- the ceiling
|
|
200
|
+
Roblox's own server logs apply -- and a table JSON cannot hold (a Vector3, an
|
|
201
|
+
Instance) falls back to text rather than being lost.
|
|
202
|
+
]]
|
|
203
|
+
-- JSONEncode writes a Vector3 or an Instance as `null` without complaint, so
|
|
204
|
+
-- anything JSON has no shape for is turned into its text first.
|
|
205
|
+
local function plain(value: any, depth: number): any
|
|
206
|
+
local kind = typeof(value)
|
|
207
|
+
if kind == "string" or kind == "number" or kind == "boolean" then
|
|
208
|
+
return value
|
|
209
|
+
elseif kind == "table" then
|
|
210
|
+
if depth >= 4 then
|
|
211
|
+
return "<table>"
|
|
212
|
+
end
|
|
213
|
+
local copy = {}
|
|
214
|
+
for key, item in value do
|
|
215
|
+
copy[if typeof(key) == "number" then key else tostring(key)] = plain(item, depth + 1)
|
|
216
|
+
end
|
|
217
|
+
return copy
|
|
218
|
+
elseif kind == "Instance" then
|
|
219
|
+
local ok, full = pcall(function()
|
|
220
|
+
return value:GetFullName()
|
|
221
|
+
end)
|
|
222
|
+
return if ok then full else value.Name
|
|
223
|
+
end
|
|
224
|
+
return tostring(value)
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
function LogBuffer.encodeContext(context: any): string?
|
|
228
|
+
if typeof(context) ~= "table" or next(context) == nil then
|
|
229
|
+
return nil
|
|
230
|
+
end
|
|
231
|
+
local ok, encoded = pcall(function()
|
|
232
|
+
return HttpService:JSONEncode(plain(context, 0))
|
|
233
|
+
end)
|
|
234
|
+
if not ok then
|
|
235
|
+
return nil
|
|
236
|
+
end
|
|
237
|
+
return string.sub(encoded :: string, 1, MAX_CONTEXT)
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
local function push(message: string, messageType: Enum.MessageType, timestamp: number?, context: any?)
|
|
187
241
|
local level = LEVELS[messageType.Value] or "print"
|
|
188
242
|
local entry: Entry = {
|
|
189
243
|
level = level,
|
|
190
244
|
message = message,
|
|
191
245
|
timestamp = timestamp or os.time(),
|
|
246
|
+
context = LogBuffer.encodeContext(context),
|
|
192
247
|
}
|
|
193
248
|
|
|
194
249
|
-- Claim a trace that beat its own error line here.
|
|
@@ -261,7 +316,7 @@ function LogBuffer.start()
|
|
|
261
316
|
end
|
|
262
317
|
end
|
|
263
318
|
|
|
264
|
-
LogService.MessageOut:Connect(function(message: string, messageType: Enum.MessageType)
|
|
319
|
+
LogService.MessageOut:Connect(function(message: string, messageType: Enum.MessageType, context: any?)
|
|
265
320
|
local buffered = collecting
|
|
266
321
|
|
|
267
322
|
if buffered then
|
|
@@ -305,7 +360,7 @@ function LogBuffer.start()
|
|
|
305
360
|
return
|
|
306
361
|
end
|
|
307
362
|
|
|
308
|
-
push(message, messageType)
|
|
363
|
+
push(message, messageType, nil, context)
|
|
309
364
|
end)
|
|
310
365
|
|
|
311
366
|
--[[
|
package/plugin/src/Phrase.luau
CHANGED
|
@@ -341,6 +341,8 @@ local DESCRIBERS: { [string]: Describer } = {
|
|
|
341
341
|
return "Run the place"
|
|
342
342
|
elseif op == "multiplayer" then
|
|
343
343
|
return string.format("Start a %s test", plural(tonumber(params.players) or 2, "player"))
|
|
344
|
+
elseif op == "addPlayers" then
|
|
345
|
+
return string.format("Add %s to the test", plural(tonumber(params.players) or 1, "player"))
|
|
344
346
|
elseif op == "stop" then
|
|
345
347
|
return "Stop the playtest"
|
|
346
348
|
elseif op == "endTest" then
|
|
@@ -17,6 +17,7 @@ local InsertService = game:GetService("InsertService")
|
|
|
17
17
|
|
|
18
18
|
local Dispatch = require(script.Parent.Parent.Dispatch)
|
|
19
19
|
local Paths = require(script.Parent.Parent.Paths)
|
|
20
|
+
local Serialize = require(script.Parent.Parent.Serialize)
|
|
20
21
|
local Undo = require(script.Parent.Parent.Undo)
|
|
21
22
|
|
|
22
23
|
local Assets = {}
|
|
@@ -53,6 +54,17 @@ function Assets.insert(params: { [string]: any }): { [string]: any }
|
|
|
53
54
|
then Paths.resolve(params.parent)
|
|
54
55
|
else workspace
|
|
55
56
|
|
|
57
|
+
-- Parsed before anything loads. A position that did not parse used to be
|
|
58
|
+
-- skipped in silence, and the model landed wherever it was saved.
|
|
59
|
+
local position: Vector3? = nil
|
|
60
|
+
if typeof(params.position) == "string" and params.position ~= "" then
|
|
61
|
+
local ok, value, why = Serialize.parse(params.position, "Vector3")
|
|
62
|
+
if not ok then
|
|
63
|
+
Dispatch.fail("BAD_PARAMS", string.format("`position` is not a Vector3: %s", tostring(why)), 'Write it as "x, y, z".')
|
|
64
|
+
end
|
|
65
|
+
position = value :: Vector3
|
|
66
|
+
end
|
|
67
|
+
|
|
56
68
|
--[[
|
|
57
69
|
`game:GetObjects`, not `InsertService:LoadAsset`.
|
|
58
70
|
|
|
@@ -135,10 +147,8 @@ function Assets.insert(params: { [string]: any }): { [string]: any }
|
|
|
135
147
|
-- nothing silently is worse than saying so.
|
|
136
148
|
local remaining: { Instance } = {}
|
|
137
149
|
for _, child in children do
|
|
138
|
-
if
|
|
139
|
-
|
|
140
|
-
table.insert(remaining, child)
|
|
141
|
-
end
|
|
150
|
+
if not child:IsA("LuaSourceContainer") then
|
|
151
|
+
table.insert(remaining, child)
|
|
142
152
|
end
|
|
143
153
|
end
|
|
144
154
|
children = remaining
|
|
@@ -160,20 +170,19 @@ function Assets.insert(params: { [string]: any }): { [string]: any }
|
|
|
160
170
|
table.insert(inserted, Paths.of(child))
|
|
161
171
|
end
|
|
162
172
|
|
|
163
|
-
-- Position after parenting, so PrimaryPart and pivot are settled.
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
end
|
|
173
|
+
-- Position after parenting, so PrimaryPart and pivot are settled. Only
|
|
174
|
+
-- the position moves: the asset keeps the rotation it was saved with.
|
|
175
|
+
if position then
|
|
176
|
+
for _, child in children do
|
|
177
|
+
pcall(function()
|
|
178
|
+
if child:IsA("Model") then
|
|
179
|
+
local model = child :: Model
|
|
180
|
+
model:PivotTo(model:GetPivot().Rotation + position)
|
|
181
|
+
elseif child:IsA("BasePart") then
|
|
182
|
+
local part = child :: BasePart
|
|
183
|
+
part.CFrame = part.CFrame.Rotation + position
|
|
184
|
+
end
|
|
185
|
+
end)
|
|
177
186
|
end
|
|
178
187
|
end
|
|
179
188
|
end)
|