@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.
@@ -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
@@ -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()