@el4cteo/rbx-studio-mcp 0.3.6 → 0.3.7
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/dist/bridge/rpc.js +73 -0
- package/dist/bridge/rpc.js.map +1 -1
- package/package.json +1 -1
- package/plugin/src/Config.luau +1 -1
- package/plugin/src/Console.luau +1101 -1041
- package/plugin/src/History.luau +187 -0
- package/plugin/src/Mirror.luau +20 -1
- package/plugin/src/init.server.luau +636 -552
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
--!strict
|
|
2
|
+
--[[
|
|
3
|
+
The console log, carried across the boundaries that used to erase it.
|
|
4
|
+
|
|
5
|
+
Pressing Play does not pause this plugin, it loads a second and a third copy
|
|
6
|
+
of it into the playtest's DataModels, each with its own empty console. Studio
|
|
7
|
+
then shows you the play view's panel, so the session you were watching
|
|
8
|
+
appears to have been wiped; stopping the test shows the editor's again, and
|
|
9
|
+
everything that happened during the test is missing from it instead. Nothing
|
|
10
|
+
crashed and nothing was lost -- the log was simply written in a window that is
|
|
11
|
+
no longer the one on screen.
|
|
12
|
+
|
|
13
|
+
Two halves fix that, and they are deliberately different mechanisms because
|
|
14
|
+
the two directions have different problems.
|
|
15
|
+
|
|
16
|
+
Forwards (editor -> playtest) is a cold start: the playtest's copies begin
|
|
17
|
+
with nothing and need what was already on screen. That is persistence, and it
|
|
18
|
+
is what this file does -- the edit session writes its rows to plugin
|
|
19
|
+
settings, and every copy reads them once at load.
|
|
20
|
+
|
|
21
|
+
Backwards (playtest -> editor) is not persistence at all. The editor session
|
|
22
|
+
is still running the whole time; it just is not the one being asked to do the
|
|
23
|
+
work. So there is nothing to restore, only something to be told, and it is
|
|
24
|
+
told over the bridge -- see `announceToPeers` in the server's rpc.ts.
|
|
25
|
+
|
|
26
|
+
Only the EDIT session writes. It is the one that outlives every playtest and
|
|
27
|
+
receives the peer announcements, so its log is the superset; letting the
|
|
28
|
+
playtest copies write as well would store the same rows twice and replay both
|
|
29
|
+
on the next load.
|
|
30
|
+
]]
|
|
31
|
+
|
|
32
|
+
local History = {}
|
|
33
|
+
|
|
34
|
+
local SETTING = "consoleHistory"
|
|
35
|
+
|
|
36
|
+
--[[
|
|
37
|
+
Rows kept, well under the console's own 300.
|
|
38
|
+
|
|
39
|
+
This is a settings file, rewritten in full on every save, and the value of an
|
|
40
|
+
old row falls off quickly -- what you want back is the screenful you were
|
|
41
|
+
looking at when the playtest started, not yesterday afternoon.
|
|
42
|
+
]]
|
|
43
|
+
local MAX_ROWS = 150
|
|
44
|
+
|
|
45
|
+
--[[
|
|
46
|
+
Seconds between saves.
|
|
47
|
+
|
|
48
|
+
A burst of commands writes several rows a second and every one of them would
|
|
49
|
+
otherwise be a rewrite of the whole file. The loss window is the same two
|
|
50
|
+
seconds, and the only way to lose that window is to close Studio inside it --
|
|
51
|
+
which `flush` covers anyway.
|
|
52
|
+
]]
|
|
53
|
+
local SAVE_EVERY = 2
|
|
54
|
+
|
|
55
|
+
export type Row = {
|
|
56
|
+
level: string,
|
|
57
|
+
message: string,
|
|
58
|
+
detail: string?,
|
|
59
|
+
stamp: string,
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
local store: Plugin? = nil
|
|
63
|
+
local read: (() -> { Row })? = nil
|
|
64
|
+
local dirty = false
|
|
65
|
+
local running = false
|
|
66
|
+
|
|
67
|
+
--[[
|
|
68
|
+
Which place this log belongs to.
|
|
69
|
+
|
|
70
|
+
One settings file is shared by every Studio window on the machine, so without
|
|
71
|
+
a key a second place's session would read back the first's history and show
|
|
72
|
+
it as its own. An unsaved place has no id, so its name stands in -- imperfect
|
|
73
|
+
between two unsaved places, and better than merging them.
|
|
74
|
+
]]
|
|
75
|
+
local function placeKey(): string
|
|
76
|
+
if game.PlaceId ~= 0 then
|
|
77
|
+
return tostring(game.PlaceId)
|
|
78
|
+
end
|
|
79
|
+
return "local:" .. game.Name
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
--[[
|
|
83
|
+
Rows survive a round trip through JSON, so nothing that comes back is
|
|
84
|
+
trusted: a settings file is a file on disk, and a field that is missing or
|
|
85
|
+
the wrong type must skip the row rather than reach the renderer.
|
|
86
|
+
]]
|
|
87
|
+
local function sane(value: any): Row?
|
|
88
|
+
if typeof(value) ~= "table" then
|
|
89
|
+
return nil
|
|
90
|
+
end
|
|
91
|
+
local row = value :: { [string]: any }
|
|
92
|
+
if typeof(row.level) ~= "string" or typeof(row.message) ~= "string" then
|
|
93
|
+
return nil
|
|
94
|
+
end
|
|
95
|
+
if typeof(row.stamp) ~= "string" then
|
|
96
|
+
return nil
|
|
97
|
+
end
|
|
98
|
+
return {
|
|
99
|
+
level = row.level,
|
|
100
|
+
message = row.message,
|
|
101
|
+
detail = if typeof(row.detail) == "string" then row.detail else nil,
|
|
102
|
+
stamp = row.stamp,
|
|
103
|
+
}
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
--[[
|
|
107
|
+
`snapshot` is a function rather than rows, because saving is throttled: what
|
|
108
|
+
must be written is whatever the console holds when the timer fires, not what
|
|
109
|
+
it held when the row that triggered it was logged.
|
|
110
|
+
]]
|
|
111
|
+
function History.attach(pluginObject: Plugin, snapshot: () -> { Row })
|
|
112
|
+
store = pluginObject
|
|
113
|
+
read = snapshot
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
function History.restore(): { Row }
|
|
117
|
+
local pluginObject = store
|
|
118
|
+
if pluginObject == nil then
|
|
119
|
+
return {}
|
|
120
|
+
end
|
|
121
|
+
local saved = pluginObject:GetSetting(SETTING)
|
|
122
|
+
if typeof(saved) ~= "table" then
|
|
123
|
+
return {}
|
|
124
|
+
end
|
|
125
|
+
local record = saved :: { [string]: any }
|
|
126
|
+
if record.place ~= placeKey() or typeof(record.rows) ~= "table" then
|
|
127
|
+
return {}
|
|
128
|
+
end
|
|
129
|
+
local rows: { Row } = {}
|
|
130
|
+
for _, entry in record.rows :: { any } do
|
|
131
|
+
local row = sane(entry)
|
|
132
|
+
if row ~= nil then
|
|
133
|
+
table.insert(rows, row)
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
return rows
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
--[[
|
|
140
|
+
Writes now. Called from the save loop and from `plugin.Unloading`, where
|
|
141
|
+
there is no next tick to wait for.
|
|
142
|
+
]]
|
|
143
|
+
function History.flush()
|
|
144
|
+
local pluginObject, snapshot = store, read
|
|
145
|
+
if pluginObject == nil or snapshot == nil then
|
|
146
|
+
return
|
|
147
|
+
end
|
|
148
|
+
dirty = false
|
|
149
|
+
local rows = snapshot()
|
|
150
|
+
while #rows > MAX_ROWS do
|
|
151
|
+
table.remove(rows, 1)
|
|
152
|
+
end
|
|
153
|
+
-- Guarded because a settings write is disk I/O: a full disk or a locked file
|
|
154
|
+
-- must cost the console its history, not its session.
|
|
155
|
+
pcall(function()
|
|
156
|
+
pluginObject:SetSetting(SETTING, { place = placeKey(), rows = rows })
|
|
157
|
+
end)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
-- Marks the log as changed. Cheap on purpose: it runs on every logged row.
|
|
161
|
+
function History.touch()
|
|
162
|
+
dirty = true
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
--[[
|
|
166
|
+
Starts the save loop. Owned here rather than by the caller so the throttle
|
|
167
|
+
and the thing it throttles cannot drift apart.
|
|
168
|
+
]]
|
|
169
|
+
function History.start()
|
|
170
|
+
running = true
|
|
171
|
+
task.spawn(function()
|
|
172
|
+
while running do
|
|
173
|
+
task.wait(SAVE_EVERY)
|
|
174
|
+
if running and dirty then
|
|
175
|
+
History.flush()
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
end)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
-- Stops the loop on unload. A reloaded plugin is a fresh module, so without
|
|
182
|
+
-- this every reload would leave another timer running against a dead session.
|
|
183
|
+
function History.stop()
|
|
184
|
+
running = false
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
return History
|
package/plugin/src/Mirror.luau
CHANGED
|
@@ -40,6 +40,25 @@ local Mirror = {}
|
|
|
40
40
|
|
|
41
41
|
local remote: RemoteEvent? = nil
|
|
42
42
|
|
|
43
|
+
--[[
|
|
44
|
+
Held shut until the server session has finished introducing itself.
|
|
45
|
+
|
|
46
|
+
Both halves of a playtest log their own banner, place and status, so a
|
|
47
|
+
channel that is open from the start delivers the server's copy of all of it
|
|
48
|
+
to a client that has already written its own -- the panel comes up saying
|
|
49
|
+
"rbx-studio v0.3.6" and "place: Game" twice, a line apart, which reads as a
|
|
50
|
+
glitch because it is one.
|
|
51
|
+
|
|
52
|
+
Opened on the server's "connected" row rather than after a fixed wait: that
|
|
53
|
+
is the exact point where its startup ends and the session's real activity
|
|
54
|
+
begins, and everything after it is news the client does not already have.
|
|
55
|
+
]]
|
|
56
|
+
local open = false
|
|
57
|
+
|
|
58
|
+
function Mirror.open()
|
|
59
|
+
open = true
|
|
60
|
+
end
|
|
61
|
+
|
|
43
62
|
--[[
|
|
44
63
|
True only in the half of a playtest that can reach the bridge.
|
|
45
64
|
|
|
@@ -88,7 +107,7 @@ end
|
|
|
88
107
|
]]
|
|
89
108
|
function Mirror.send(kind: string, arguments: { any })
|
|
90
109
|
local event = remote
|
|
91
|
-
if event == nil then
|
|
110
|
+
if event == nil or not open then
|
|
92
111
|
return
|
|
93
112
|
end
|
|
94
113
|
pcall(function()
|