@el4cteo/rbx-studio-mcp 0.4.2 → 0.4.6
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 +222 -211
- package/dist/bridge/api.js +3 -0
- package/dist/bridge/api.js.map +1 -1
- package/dist/bridge/failover.js +13 -0
- package/dist/bridge/failover.js.map +1 -1
- package/dist/bridge/remote.js +15 -1
- package/dist/bridge/remote.js.map +1 -1
- package/dist/bridge/rpc.js +35 -6
- package/dist/bridge/rpc.js.map +1 -1
- package/dist/bridge/server.js +55 -7
- package/dist/bridge/server.js.map +1 -1
- package/dist/doctor.js +160 -0
- package/dist/doctor.js.map +1 -0
- package/dist/index.js +29 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/protocol.js.map +1 -1
- package/dist/tools/exec.js +53 -3
- package/dist/tools/exec.js.map +1 -1
- package/dist/tools/generate.js +120 -0
- package/dist/tools/generate.js.map +1 -0
- package/dist/tools/scripts.js +20 -1
- package/dist/tools/scripts.js.map +1 -1
- package/dist/tools/world.js +203 -20
- package/dist/tools/world.js.map +1 -1
- package/package.json +2 -1
- package/plugin/src/Config.luau +1 -1
- package/plugin/src/Console.luau +233 -9
- package/plugin/src/Transport.luau +6 -1
- package/plugin/src/Visuals.luau +52 -1
- package/plugin/src/handlers/Assets.luau +352 -145
- package/plugin/src/handlers/Generate.luau +386 -0
- package/plugin/src/handlers/Geometry.luau +170 -0
- package/plugin/src/handlers/Scripts.luau +79 -1
- package/plugin/src/handlers/Viewport.luau +416 -302
- package/plugin/src/handlers/World.luau +15 -5
- package/plugin/src/init.server.luau +725 -674
- package/scripts/test-bridge.mjs +56 -0
- package/scripts/test-failover.mjs +136 -95
package/plugin/src/Console.luau
CHANGED
|
@@ -203,6 +203,21 @@ type Record = {
|
|
|
203
203
|
stamp: string,
|
|
204
204
|
}
|
|
205
205
|
|
|
206
|
+
--[[
|
|
207
|
+
One MCP client sharing this bridge, as the server describes it.
|
|
208
|
+
|
|
209
|
+
`name` is what the agent calls itself in the MCP handshake, so it is
|
|
210
|
+
"claude-code" or "codex" rather than anything the bridge guessed. A client
|
|
211
|
+
that never introduced itself arrives as "unknown", which is honest: it is
|
|
212
|
+
connected, we just do not know what it is.
|
|
213
|
+
]]
|
|
214
|
+
export type Client = {
|
|
215
|
+
name: string,
|
|
216
|
+
version: string,
|
|
217
|
+
pid: number,
|
|
218
|
+
connectedAt: number,
|
|
219
|
+
}
|
|
220
|
+
|
|
206
221
|
type State = {
|
|
207
222
|
records: { Record },
|
|
208
223
|
-- The last status reported, replayed after a theme switch. Without it the
|
|
@@ -227,7 +242,7 @@ type State = {
|
|
|
227
242
|
statusText: TextLabel?,
|
|
228
243
|
metaText: TextLabel?,
|
|
229
244
|
countersText: TextLabel?,
|
|
230
|
-
clientsChip:
|
|
245
|
+
clientsChip: TextButton?,
|
|
231
246
|
pinned: boolean,
|
|
232
247
|
calls: number,
|
|
233
248
|
errors: number,
|
|
@@ -244,6 +259,24 @@ type State = {
|
|
|
244
259
|
generation: number,
|
|
245
260
|
-- How many MCP clients share this bridge. Only ever displayed above one.
|
|
246
261
|
clients: number,
|
|
262
|
+
--[[
|
|
263
|
+
Whether the count above was learned from this connection or the last.
|
|
264
|
+
|
|
265
|
+
The bridge sends the roster the moment a stream opens, so the first
|
|
266
|
+
frame after every reconnect looks exactly like an agent arriving. It is
|
|
267
|
+
not: the same agents were there a second ago, and the connect itself has
|
|
268
|
+
already played its flourish. Cleared whenever the transport leaves
|
|
269
|
+
"connected", so only a genuine arrival mid-session celebrates.
|
|
270
|
+
]]
|
|
271
|
+
clientsKnown: boolean,
|
|
272
|
+
--[[
|
|
273
|
+
Who those clients are, newest last, as the bridge last reported them.
|
|
274
|
+
|
|
275
|
+
Kept even at one client, unlike the badge: the roster is what answers
|
|
276
|
+
"is this extra one a problem?", and the moment it becomes worth asking
|
|
277
|
+
is the moment a second appears -- too late to start recording.
|
|
278
|
+
]]
|
|
279
|
+
clientList: { Client },
|
|
247
280
|
-- Set while a repaint is already scheduled for the end of this frame.
|
|
248
281
|
dirty: boolean,
|
|
249
282
|
|
|
@@ -286,6 +319,8 @@ local state: State = {
|
|
|
286
319
|
running = nil,
|
|
287
320
|
generation = 0,
|
|
288
321
|
clients = 1,
|
|
322
|
+
clientsKnown = false,
|
|
323
|
+
clientList = {},
|
|
289
324
|
dirty = false,
|
|
290
325
|
crt = nil,
|
|
291
326
|
crtScreen = nil,
|
|
@@ -295,6 +330,11 @@ local state: State = {
|
|
|
295
330
|
crtGeneration = 0,
|
|
296
331
|
}
|
|
297
332
|
|
|
333
|
+
-- Declared ahead of its definition: `setClients` calls it and is written
|
|
334
|
+
-- above it, and a plain `function layoutHeader()` there would have quietly
|
|
335
|
+
-- become a global.
|
|
336
|
+
local layoutHeader: () -> ()
|
|
337
|
+
|
|
298
338
|
-- How long the session must be silent before the console says so. Long enough
|
|
299
339
|
-- that an agent pausing to think is not announced as having stopped.
|
|
300
340
|
local QUIET_SECONDS = 20
|
|
@@ -681,13 +721,30 @@ end
|
|
|
681
721
|
badge -- a permanent "1 client connected" is a label, not a signal, and the
|
|
682
722
|
header has better uses for the width.
|
|
683
723
|
]]
|
|
684
|
-
function Console.setClients(count: number)
|
|
724
|
+
function Console.setClients(count: number, list: { Client }?)
|
|
685
725
|
local previous = state.clients
|
|
726
|
+
local known = state.clientsKnown
|
|
686
727
|
state.clients = count
|
|
728
|
+
state.clientsKnown = true
|
|
729
|
+
if list ~= nil then
|
|
730
|
+
state.clientList = list
|
|
731
|
+
end
|
|
732
|
+
|
|
733
|
+
--[[
|
|
734
|
+
An agent joining gets the same flourish as a session landing.
|
|
735
|
+
|
|
736
|
+
It is the same kind of news -- something that can now drive this Studio
|
|
737
|
+
has arrived -- and the trace is where this panel says so. Guarded on
|
|
738
|
+
`known` because the bridge re-sends the roster on every reconnect, and
|
|
739
|
+
replaying the arrival of agents that never left would fire it several
|
|
740
|
+
times a session until it stopped meaning anything.
|
|
741
|
+
]]
|
|
742
|
+
if known and count > previous then
|
|
743
|
+
Visuals.celebrate()
|
|
744
|
+
end
|
|
687
745
|
|
|
688
746
|
local chip = state.clientsChip
|
|
689
747
|
if chip then
|
|
690
|
-
chip.Visible = count > 1
|
|
691
748
|
-- Pluralised even though the badge hides at one. A string that is only
|
|
692
749
|
-- ever correct because nobody can see it is a trap for whoever changes
|
|
693
750
|
-- the visibility rule later.
|
|
@@ -698,10 +755,7 @@ function Console.setClients(count: number)
|
|
|
698
755
|
)
|
|
699
756
|
end
|
|
700
757
|
|
|
701
|
-
|
|
702
|
-
if meta then
|
|
703
|
-
meta.Size = UDim2.new(1, if count > 1 then -486 else -394, 1, 0)
|
|
704
|
-
end
|
|
758
|
+
layoutHeader()
|
|
705
759
|
|
|
706
760
|
--[[
|
|
707
761
|
Only the arrival is worth a row.
|
|
@@ -713,8 +767,152 @@ function Console.setClients(count: number)
|
|
|
713
767
|
reports it; the count coming down is already covered twice over.
|
|
714
768
|
]]
|
|
715
769
|
if count > 1 and previous <= 1 then
|
|
716
|
-
|
|
770
|
+
--[[
|
|
771
|
+
Named, not just counted.
|
|
772
|
+
|
|
773
|
+
"3 MCP clients connected" is the line people bring to us asking
|
|
774
|
+
whether something is wrong, because a number cannot say whether the
|
|
775
|
+
extra ones are agents they started or processes they forgot. The
|
|
776
|
+
names can, so they are in the row that raises the question rather
|
|
777
|
+
than only in a panel the reader has to know to hover.
|
|
778
|
+
]]
|
|
779
|
+
Console.log(
|
|
780
|
+
"info",
|
|
781
|
+
string.format("%d MCP clients connected", count),
|
|
782
|
+
Console.clientSummary()
|
|
783
|
+
)
|
|
784
|
+
end
|
|
785
|
+
end
|
|
786
|
+
|
|
787
|
+
--[[
|
|
788
|
+
The roster on one line, for the caption and the arrival row.
|
|
789
|
+
|
|
790
|
+
Names only, deduplicated by name with a count where it repeats: two Codex
|
|
791
|
+
windows are "codex x2", not "codex, codex". The interesting fact is which
|
|
792
|
+
tools are attached, and a list that repeats a name reads as a mistake.
|
|
793
|
+
]]
|
|
794
|
+
--[[
|
|
795
|
+
Places the header's right-hand controls, and gives the meta line what is left.
|
|
796
|
+
|
|
797
|
+
One function rather than each control minding its own position, because the
|
|
798
|
+
clients badge comes and goes: it only appears above one client. With the
|
|
799
|
+
positions written at each call site, the meta line's width had to encode
|
|
800
|
+
every combination as a constant, and the two had to be kept in step by hand.
|
|
801
|
+
|
|
802
|
+
Laid out right to left from the buttons, each control claiming its width
|
|
803
|
+
plus a gap. The meta line then ends a fixed distance further left, which is
|
|
804
|
+
the rule it always followed -- it just no longer has to be told the answer.
|
|
805
|
+
]]
|
|
806
|
+
local BUTTONS_WIDTH = 244
|
|
807
|
+
local HEADER_GAP = 8
|
|
808
|
+
local CHIP_WIDTH = 84
|
|
809
|
+
local META_CLEARANCE = 150
|
|
810
|
+
|
|
811
|
+
layoutHeader = function()
|
|
812
|
+
local edge = BUTTONS_WIDTH
|
|
813
|
+
|
|
814
|
+
local chip = state.clientsChip
|
|
815
|
+
if chip then
|
|
816
|
+
chip.Visible = state.clients > 1
|
|
817
|
+
if chip.Visible then
|
|
818
|
+
edge += HEADER_GAP
|
|
819
|
+
chip.Position = UDim2.new(1, -edge, 0.5, 0)
|
|
820
|
+
edge += CHIP_WIDTH
|
|
821
|
+
end
|
|
822
|
+
end
|
|
823
|
+
|
|
824
|
+
local meta = state.metaText
|
|
825
|
+
if meta then
|
|
826
|
+
meta.Size = UDim2.new(1, -(edge + META_CLEARANCE), 1, 0)
|
|
827
|
+
end
|
|
828
|
+
end
|
|
829
|
+
|
|
830
|
+
|
|
831
|
+
|
|
832
|
+
|
|
833
|
+
function Console.clientSummary(): string
|
|
834
|
+
local list = state.clientList
|
|
835
|
+
if #list == 0 then
|
|
836
|
+
return ""
|
|
837
|
+
end
|
|
838
|
+
local order: { string } = {}
|
|
839
|
+
local seen: { [string]: number } = {}
|
|
840
|
+
for _, client in list do
|
|
841
|
+
local name = if client.name ~= "" then client.name else "unknown"
|
|
842
|
+
if seen[name] == nil then
|
|
843
|
+
seen[name] = 0
|
|
844
|
+
table.insert(order, name)
|
|
845
|
+
end
|
|
846
|
+
seen[name] += 1
|
|
717
847
|
end
|
|
848
|
+
local parts: { string } = {}
|
|
849
|
+
for _, name in order do
|
|
850
|
+
local total = seen[name]
|
|
851
|
+
table.insert(parts, if total > 1 then string.format("%s x%d", name, total) else name)
|
|
852
|
+
end
|
|
853
|
+
return table.concat(parts, ", ")
|
|
854
|
+
end
|
|
855
|
+
|
|
856
|
+
--[[
|
|
857
|
+
The roster in full, one row per client, written into the log.
|
|
858
|
+
|
|
859
|
+
In the log rather than a hover panel because that is where this console puts
|
|
860
|
+
facts it wants the user to be able to scroll back to -- and because a list
|
|
861
|
+
that only exists while the pointer is on it cannot be read and acted on at
|
|
862
|
+
the same time.
|
|
863
|
+
]]
|
|
864
|
+
function Console.reportClients()
|
|
865
|
+
local list = state.clientList
|
|
866
|
+
if #list == 0 then
|
|
867
|
+
--[[
|
|
868
|
+
An empty roster beside a count above one is not "nobody is here",
|
|
869
|
+
it is a server too old to say who. Worth distinguishing: the first
|
|
870
|
+
reading sends someone looking for a connection problem that does
|
|
871
|
+
not exist, and the fix -- restart the MCP server -- is not one
|
|
872
|
+
anybody guesses from "no client has introduced itself".
|
|
873
|
+
]]
|
|
874
|
+
if state.clients > 0 then
|
|
875
|
+
Console.log(
|
|
876
|
+
"dim",
|
|
877
|
+
string.format("%d connected, but this bridge did not say which", state.clients),
|
|
878
|
+
"restart the MCP server"
|
|
879
|
+
)
|
|
880
|
+
else
|
|
881
|
+
Console.log("dim", "no MCP client is connected")
|
|
882
|
+
end
|
|
883
|
+
return
|
|
884
|
+
end
|
|
885
|
+
Console.log("info", string.format("%d MCP client%s on this bridge", #list, if #list == 1 then "" else "s"))
|
|
886
|
+
local now = os.time()
|
|
887
|
+
for _, client in list do
|
|
888
|
+
local name = if client.name ~= "" then client.name else "unknown"
|
|
889
|
+
local version = if client.version ~= "" then " " .. client.version else ""
|
|
890
|
+
-- Seconds since the epoch on both sides, so this is a real elapsed time
|
|
891
|
+
-- and not a guess: the bridge and Studio are the same machine.
|
|
892
|
+
local since = math.max(0, now - math.floor(client.connectedAt / 1000))
|
|
893
|
+
Console.log(
|
|
894
|
+
"dim",
|
|
895
|
+
string.format(" %s%s", name, version),
|
|
896
|
+
string.format("pid %d %s", client.pid, Console.humanDuration(since))
|
|
897
|
+
)
|
|
898
|
+
end
|
|
899
|
+
end
|
|
900
|
+
|
|
901
|
+
--[[
|
|
902
|
+
A duration a person reads at a glance, not a precise one.
|
|
903
|
+
|
|
904
|
+
Rounded down deliberately: "2m" for anything in that minute is what someone
|
|
905
|
+
scanning a list wants, and a ticking "2m 47s" invites the reader to watch it
|
|
906
|
+
rather than to read past it.
|
|
907
|
+
]]
|
|
908
|
+
function Console.humanDuration(seconds: number): string
|
|
909
|
+
if seconds < 60 then
|
|
910
|
+
return string.format("%ds", seconds)
|
|
911
|
+
end
|
|
912
|
+
if seconds < 3600 then
|
|
913
|
+
return string.format("%dm", math.floor(seconds / 60))
|
|
914
|
+
end
|
|
915
|
+
return string.format("%dh %dm", math.floor(seconds / 3600), math.floor(seconds % 3600 / 60))
|
|
718
916
|
end
|
|
719
917
|
|
|
720
918
|
--[[
|
|
@@ -913,6 +1111,11 @@ function Console.setStatus(status: string, meta: string)
|
|
|
913
1111
|
if status == "connected" and previous ~= "connected" then
|
|
914
1112
|
Visuals.celebrate()
|
|
915
1113
|
end
|
|
1114
|
+
|
|
1115
|
+
-- A roster learned on the old connection says nothing about this one.
|
|
1116
|
+
if status ~= "connected" then
|
|
1117
|
+
state.clientsKnown = false
|
|
1118
|
+
end
|
|
916
1119
|
end
|
|
917
1120
|
|
|
918
1121
|
--[[
|
|
@@ -1054,7 +1257,7 @@ function Console.mount(parent: Instance, handlers: Handlers)
|
|
|
1054
1257
|
connection facts live. `meta` gives up the width when it appears; see
|
|
1055
1258
|
`Console.setClients`.
|
|
1056
1259
|
]]
|
|
1057
|
-
local clientsChip = Instance.new("
|
|
1260
|
+
local clientsChip = Instance.new("TextButton")
|
|
1058
1261
|
clientsChip.Name = "Clients"
|
|
1059
1262
|
clientsChip.AnchorPoint = Vector2.new(1, 0.5)
|
|
1060
1263
|
clientsChip.Position = UDim2.new(1, -252, 0.5, 0)
|
|
@@ -1065,10 +1268,31 @@ function Console.mount(parent: Instance, handlers: Handlers)
|
|
|
1065
1268
|
clientsChip.TextSize = 11
|
|
1066
1269
|
themed(clientsChip, "TextColor3", "cyan")
|
|
1067
1270
|
clientsChip.Text = ""
|
|
1271
|
+
clientsChip.AutoButtonColor = false
|
|
1068
1272
|
clientsChip.Visible = false
|
|
1069
1273
|
clientsChip.Parent = header
|
|
1070
1274
|
state.clientsChip = clientsChip
|
|
1071
1275
|
|
|
1276
|
+
--[[
|
|
1277
|
+
A count raises a question the count cannot answer.
|
|
1278
|
+
|
|
1279
|
+
"3 clients" is the exact thing users bring to us asking whether it is a
|
|
1280
|
+
problem, and it never is answerable from a number: three agents they
|
|
1281
|
+
started and three processes they forgot look identical. Hovering names
|
|
1282
|
+
them in the caption -- cheap, no click, no panel to dismiss -- and
|
|
1283
|
+
clicking writes the full roster into the log, where it can be scrolled
|
|
1284
|
+
back to and acted on.
|
|
1285
|
+
]]
|
|
1286
|
+
clientsChip.MouseEnter:Connect(function()
|
|
1287
|
+
Visuals.showNote(Console.clientSummary())
|
|
1288
|
+
end)
|
|
1289
|
+
clientsChip.MouseLeave:Connect(function()
|
|
1290
|
+
Visuals.clearNote()
|
|
1291
|
+
end)
|
|
1292
|
+
clientsChip.Activated:Connect(function()
|
|
1293
|
+
Console.reportClients()
|
|
1294
|
+
end)
|
|
1295
|
+
|
|
1072
1296
|
local chipCorner = Instance.new("UICorner")
|
|
1073
1297
|
chipCorner.CornerRadius = UDim.new(0, 3)
|
|
1074
1298
|
chipCorner.Parent = clientsChip
|
|
@@ -45,6 +45,7 @@ type Callbacks = {
|
|
|
45
45
|
|
|
46
46
|
local running = false
|
|
47
47
|
local studioId: string = ""
|
|
48
|
+
|
|
48
49
|
local activeStream: any = nil
|
|
49
50
|
local mode: "sse" | "poll" = "sse"
|
|
50
51
|
|
|
@@ -283,7 +284,11 @@ local function runPolling(callbacks: Callbacks): boolean
|
|
|
283
284
|
command would otherwise never hear about it.
|
|
284
285
|
]]
|
|
285
286
|
if typeof(payload.clients) == "number" then
|
|
286
|
-
callbacks.onEvent({
|
|
287
|
+
callbacks.onEvent({
|
|
288
|
+
event = "clients",
|
|
289
|
+
count = payload.clients,
|
|
290
|
+
list = payload.clientList,
|
|
291
|
+
})
|
|
287
292
|
end
|
|
288
293
|
end
|
|
289
294
|
end
|
package/plugin/src/Visuals.luau
CHANGED
|
@@ -232,6 +232,15 @@ type Runtime = {
|
|
|
232
232
|
highlight: Frame?,
|
|
233
233
|
readout: TextLabel?,
|
|
234
234
|
caption: TextLabel?,
|
|
235
|
+
--[[
|
|
236
|
+
What the caption said before a note took it over.
|
|
237
|
+
|
|
238
|
+
Nil means no note is showing, which is why this is the flag as well as
|
|
239
|
+
the storage: a second `showNote` while one is up must not overwrite the
|
|
240
|
+
text being held, or clearing would restore a note instead of the
|
|
241
|
+
caption.
|
|
242
|
+
]]
|
|
243
|
+
noted: string?,
|
|
235
244
|
bars: { Bar },
|
|
236
245
|
connection: RBXScriptConnection?,
|
|
237
246
|
|
|
@@ -289,6 +298,7 @@ local runtime: Runtime = {
|
|
|
289
298
|
highlight = nil,
|
|
290
299
|
readout = nil,
|
|
291
300
|
caption = nil,
|
|
301
|
+
noted = nil,
|
|
292
302
|
bars = {},
|
|
293
303
|
connection = nil,
|
|
294
304
|
spin = Vector3.new(0.18, 0.27, 0.11),
|
|
@@ -1012,11 +1022,46 @@ end
|
|
|
1012
1022
|
]]
|
|
1013
1023
|
function Visuals.setCaption(title: string)
|
|
1014
1024
|
runtime.lastTitle = title
|
|
1025
|
+
-- A note is holding the caption's real text for later. Update what will be
|
|
1026
|
+
-- restored, not what is on screen, or the note is wiped by the next command
|
|
1027
|
+
-- and clearing it would put back a line that is already out of date.
|
|
1028
|
+
if runtime.noted ~= nil then
|
|
1029
|
+
runtime.noted = title
|
|
1030
|
+
return
|
|
1031
|
+
end
|
|
1015
1032
|
if runtime.caption then
|
|
1016
1033
|
runtime.caption.Text = title
|
|
1017
1034
|
end
|
|
1018
1035
|
end
|
|
1019
1036
|
|
|
1037
|
+
--[[
|
|
1038
|
+
Borrows the caption to answer something the user is pointing at.
|
|
1039
|
+
|
|
1040
|
+
The caption is the band's one line of prose, so a note goes there rather
|
|
1041
|
+
than into a floating panel: there is exactly one place on this widget that
|
|
1042
|
+
explains what you are looking at, and two would be one too many. Whatever
|
|
1043
|
+
the caption was saying is put back by `clearNote`, so a note never costs the
|
|
1044
|
+
user the line they were reading.
|
|
1045
|
+
]]
|
|
1046
|
+
function Visuals.showNote(text: string)
|
|
1047
|
+
if runtime.caption == nil or text == "" then
|
|
1048
|
+
return
|
|
1049
|
+
end
|
|
1050
|
+
if runtime.noted == nil then
|
|
1051
|
+
runtime.noted = runtime.caption.Text
|
|
1052
|
+
end
|
|
1053
|
+
runtime.caption.Text = text
|
|
1054
|
+
end
|
|
1055
|
+
|
|
1056
|
+
function Visuals.clearNote()
|
|
1057
|
+
local held = runtime.noted
|
|
1058
|
+
if held == nil or runtime.caption == nil then
|
|
1059
|
+
return
|
|
1060
|
+
end
|
|
1061
|
+
runtime.noted = nil
|
|
1062
|
+
runtime.caption.Text = held
|
|
1063
|
+
end
|
|
1064
|
+
|
|
1020
1065
|
--[[
|
|
1021
1066
|
Falls back to the last thing that ran once a command finishes.
|
|
1022
1067
|
|
|
@@ -1028,9 +1073,15 @@ function Visuals.setIdle()
|
|
|
1028
1073
|
if runtime.caption == nil then
|
|
1029
1074
|
return
|
|
1030
1075
|
end
|
|
1031
|
-
|
|
1076
|
+
local text = if runtime.lastTitle ~= nil
|
|
1032
1077
|
then "last: " .. runtime.lastTitle
|
|
1033
1078
|
else "waiting for a command"
|
|
1079
|
+
-- Same rule as setCaption: a note owns the line until it is cleared.
|
|
1080
|
+
if runtime.noted ~= nil then
|
|
1081
|
+
runtime.noted = text
|
|
1082
|
+
return
|
|
1083
|
+
end
|
|
1084
|
+
runtime.caption.Text = text
|
|
1034
1085
|
end
|
|
1035
1086
|
|
|
1036
1087
|
--[[
|