@el4cteo/rbx-studio-mcp 0.1.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/LICENSE +21 -0
- package/README.md +203 -0
- package/dist/bridge/rpc.js +243 -0
- package/dist/bridge/rpc.js.map +1 -0
- package/dist/bridge/server.js +281 -0
- package/dist/bridge/server.js.map +1 -0
- package/dist/index.js +104 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/apidump.js +269 -0
- package/dist/lib/apidump.js.map +1 -0
- package/dist/lib/errors.js +38 -0
- package/dist/lib/errors.js.map +1 -0
- package/dist/lib/format.js +191 -0
- package/dist/lib/format.js.map +1 -0
- package/dist/lib/pluginbuild.js +83 -0
- package/dist/lib/pluginbuild.js.map +1 -0
- package/dist/lib/png.js +84 -0
- package/dist/lib/png.js.map +1 -0
- package/dist/lib/protocol.js +22 -0
- package/dist/lib/protocol.js.map +1 -0
- package/dist/lib/tool.js +27 -0
- package/dist/lib/tool.js.map +1 -0
- package/dist/resources.js +70 -0
- package/dist/resources.js.map +1 -0
- package/dist/tools/api.js +78 -0
- package/dist/tools/api.js.map +1 -0
- package/dist/tools/character.js +94 -0
- package/dist/tools/character.js.map +1 -0
- package/dist/tools/debug.js +211 -0
- package/dist/tools/debug.js.map +1 -0
- package/dist/tools/device.js +74 -0
- package/dist/tools/device.js.map +1 -0
- package/dist/tools/discover.js +217 -0
- package/dist/tools/discover.js.map +1 -0
- package/dist/tools/exec.js +191 -0
- package/dist/tools/exec.js.map +1 -0
- package/dist/tools/input.js +96 -0
- package/dist/tools/input.js.map +1 -0
- package/dist/tools/instances.js +261 -0
- package/dist/tools/instances.js.map +1 -0
- package/dist/tools/perf.js +367 -0
- package/dist/tools/perf.js.map +1 -0
- package/dist/tools/playtest.js +153 -0
- package/dist/tools/playtest.js.map +1 -0
- package/dist/tools/screenshot.js +75 -0
- package/dist/tools/screenshot.js.map +1 -0
- package/dist/tools/scripts.js +316 -0
- package/dist/tools/scripts.js.map +1 -0
- package/dist/tools/session.js +152 -0
- package/dist/tools/session.js.map +1 -0
- package/dist/tools/world.js +281 -0
- package/dist/tools/world.js.map +1 -0
- package/package.json +62 -0
- package/plugin/default.project.json +6 -0
- package/plugin/src/Config.luau +59 -0
- package/plugin/src/Console.luau +657 -0
- package/plugin/src/Context.luau +35 -0
- package/plugin/src/Dispatch.luau +90 -0
- package/plugin/src/Editor.luau +142 -0
- package/plugin/src/Emulation.luau +151 -0
- package/plugin/src/LogBuffer.luau +277 -0
- package/plugin/src/Net.luau +102 -0
- package/plugin/src/Paths.luau +255 -0
- package/plugin/src/Phrase.luau +465 -0
- package/plugin/src/Png.luau +238 -0
- package/plugin/src/Scope.luau +78 -0
- package/plugin/src/ScriptEdit.luau +100 -0
- package/plugin/src/Serialize.luau +287 -0
- package/plugin/src/TextEdit.luau +296 -0
- package/plugin/src/Transport.luau +328 -0
- package/plugin/src/Undo.luau +72 -0
- package/plugin/src/Visuals.luau +710 -0
- package/plugin/src/handlers/Api.luau +242 -0
- package/plugin/src/handlers/Assets.luau +145 -0
- package/plugin/src/handlers/Capture.luau +187 -0
- package/plugin/src/handlers/Character.luau +361 -0
- package/plugin/src/handlers/Debug.luau +391 -0
- package/plugin/src/handlers/Device.luau +119 -0
- package/plugin/src/handlers/Discover.luau +289 -0
- package/plugin/src/handlers/Exec.luau +270 -0
- package/plugin/src/handlers/Geometry.luau +261 -0
- package/plugin/src/handlers/Input.luau +287 -0
- package/plugin/src/handlers/Instances.luau +389 -0
- package/plugin/src/handlers/Perf.luau +645 -0
- package/plugin/src/handlers/Playtest.luau +205 -0
- package/plugin/src/handlers/Scripts.luau +387 -0
- package/plugin/src/handlers/Session.luau +168 -0
- package/plugin/src/handlers/Viewport.luau +302 -0
- package/plugin/src/handlers/World.luau +176 -0
- package/plugin/src/init.server.luau +317 -0
- package/scripts/build-plugin.mjs +157 -0
- package/scripts/check-plugin.mjs +97 -0
- package/scripts/install-plugin.mjs +39 -0
- package/scripts/latency.mjs +201 -0
- package/scripts/locate-luau.mjs +51 -0
- package/scripts/sourcemap.mjs +58 -0
- package/scripts/test-plugin.mjs +82 -0
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
--!strict
|
|
2
|
+
--[[
|
|
3
|
+
Creating, changing, moving and deleting instances.
|
|
4
|
+
|
|
5
|
+
Every operation here is a batch wrapped in one ChangeHistoryService recording.
|
|
6
|
+
Unlike script source -- where a recording turns out not to cover the change at
|
|
7
|
+
all -- instance edits are genuinely captured, so a failed batch really does
|
|
8
|
+
roll back and a successful one really is a single Ctrl+Z. The response says
|
|
9
|
+
which of those happened rather than assuming.
|
|
10
|
+
|
|
11
|
+
Property values arrive as text with the target type resolved from the live
|
|
12
|
+
API dump on the server. That split matters: the plugin has no dump, and
|
|
13
|
+
guessing a type from whatever the property currently holds fails on exactly
|
|
14
|
+
the properties worth setting -- a nil PrimaryPart, an unset Color3.
|
|
15
|
+
]]
|
|
16
|
+
|
|
17
|
+
local CollectionService = game:GetService("CollectionService")
|
|
18
|
+
|
|
19
|
+
local Dispatch = require(script.Parent.Parent.Dispatch)
|
|
20
|
+
local Paths = require(script.Parent.Parent.Paths)
|
|
21
|
+
local Serialize = require(script.Parent.Parent.Serialize)
|
|
22
|
+
local Undo = require(script.Parent.Parent.Undo)
|
|
23
|
+
|
|
24
|
+
-- Guards a runaway batch from locking Studio's main thread.
|
|
25
|
+
local MAX_INSTANCES = 500
|
|
26
|
+
|
|
27
|
+
local Instances = {}
|
|
28
|
+
|
|
29
|
+
type PropertySpec = { value: any, type: string }
|
|
30
|
+
|
|
31
|
+
--[[
|
|
32
|
+
Applies one property, returning a reason rather than raising so a batch can
|
|
33
|
+
report exactly which of twenty properties was wrong.
|
|
34
|
+
]]
|
|
35
|
+
local function applyProperty(target: Instance, name: string, spec: PropertySpec): string?
|
|
36
|
+
if typeof(spec) ~= "table" or spec.type == nil then
|
|
37
|
+
return string.format("%s: no type was resolved for this property", name)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
local ok, parsed, reason = Serialize.parse(spec.value, spec.type)
|
|
41
|
+
if not ok then
|
|
42
|
+
return string.format("%s: %s", name, reason or "could not be converted")
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
local assigned, err = pcall(function()
|
|
46
|
+
(target :: any)[name] = parsed
|
|
47
|
+
end)
|
|
48
|
+
if not assigned then
|
|
49
|
+
-- Read-only and context-gated properties fail here rather than at parse
|
|
50
|
+
-- time; the engine message names which.
|
|
51
|
+
return string.format("%s: %s", name, tostring(err))
|
|
52
|
+
end
|
|
53
|
+
return nil
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
local function applyAttributes(target: Instance, attributes: { [string]: any }): { string }
|
|
57
|
+
local failures: { string } = {}
|
|
58
|
+
for name, value in attributes do
|
|
59
|
+
-- A JSON null means "remove this attribute", which SetAttribute spells
|
|
60
|
+
-- as nil. There is no other way to express removal in the payload.
|
|
61
|
+
local ok, err = pcall(function()
|
|
62
|
+
target:SetAttribute(name, if value == "" then nil else value)
|
|
63
|
+
end)
|
|
64
|
+
if not ok then
|
|
65
|
+
table.insert(failures, string.format("attribute %s: %s", name, tostring(err)))
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
return failures
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
local function applyTags(target: Instance, tags: { [string]: any })
|
|
72
|
+
for _, tag in (tags.add or {}) :: { string } do
|
|
73
|
+
CollectionService:AddTag(target, tag)
|
|
74
|
+
end
|
|
75
|
+
for _, tag in (tags.remove or {}) :: { string } do
|
|
76
|
+
CollectionService:RemoveTag(target, tag)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
--[[
|
|
81
|
+
Builds one instance and its descendants.
|
|
82
|
+
|
|
83
|
+
Children are created before the parent is attached to the data model, so a
|
|
84
|
+
model appears in the Explorer complete rather than assembling itself piece by
|
|
85
|
+
piece in front of the user.
|
|
86
|
+
]]
|
|
87
|
+
local function build(spec: { [string]: any }, parent: Instance, created: { Instance })
|
|
88
|
+
local ok, instance = pcall(Instance.new, spec.className)
|
|
89
|
+
if not ok or typeof(instance) ~= "Instance" then
|
|
90
|
+
Dispatch.fail(
|
|
91
|
+
"BAD_CLASS",
|
|
92
|
+
string.format('"%s" is not a creatable class.', tostring(spec.className)),
|
|
93
|
+
"Check the spelling and capitalisation. Abstract classes like BasePart "
|
|
94
|
+
.. "cannot be created -- use a concrete one such as Part."
|
|
95
|
+
)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
local target = instance :: Instance
|
|
99
|
+
if typeof(spec.name) == "string" and spec.name ~= "" then
|
|
100
|
+
target.Name = spec.name
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
local failures: { string } = {}
|
|
104
|
+
for name, property in (spec.properties or {}) :: { [string]: PropertySpec } do
|
|
105
|
+
local failure = applyProperty(target, name, property)
|
|
106
|
+
if failure then
|
|
107
|
+
table.insert(failures, failure)
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
for _, failure in applyAttributes(target, (spec.attributes or {}) :: { [string]: any }) do
|
|
111
|
+
table.insert(failures, failure)
|
|
112
|
+
end
|
|
113
|
+
if spec.tags then
|
|
114
|
+
applyTags(target, spec.tags)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
if #failures > 0 then
|
|
118
|
+
target:Destroy()
|
|
119
|
+
Dispatch.fail(
|
|
120
|
+
"BAD_PROPERTY",
|
|
121
|
+
string.format("Could not set %d value(s) on the new %s.", #failures, spec.className),
|
|
122
|
+
table.concat(failures, "; ")
|
|
123
|
+
)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
-- Recorded parent-first, so the response reads top-down like the Explorer.
|
|
127
|
+
table.insert(created, target)
|
|
128
|
+
for _, child in (spec.children or {}) :: { { [string]: any } } do
|
|
129
|
+
build(child, target, created)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
target.Parent = parent
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
function Instances.create(params: { [string]: any }): { [string]: any }
|
|
136
|
+
local specs = params.instances
|
|
137
|
+
if typeof(specs) ~= "table" or #specs == 0 then
|
|
138
|
+
Dispatch.fail(
|
|
139
|
+
"BAD_PARAMS",
|
|
140
|
+
"create requires a non-empty `instances` array.",
|
|
141
|
+
"Each entry needs `parent` and `className`."
|
|
142
|
+
)
|
|
143
|
+
end
|
|
144
|
+
if #specs > MAX_INSTANCES then
|
|
145
|
+
Dispatch.fail(
|
|
146
|
+
"TOO_MANY",
|
|
147
|
+
string.format("%d instances in one call, over the %d limit.", #specs, MAX_INSTANCES),
|
|
148
|
+
"Split the batch, or create one parent and nest the rest under `children`."
|
|
149
|
+
)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
local created, recorded = Undo.record("StudioMCP.Create", "MCP create", function()
|
|
153
|
+
local built: { Instance } = {}
|
|
154
|
+
for _, spec in specs do
|
|
155
|
+
build(spec, Paths.resolve(spec.parent), built)
|
|
156
|
+
end
|
|
157
|
+
return built
|
|
158
|
+
end)
|
|
159
|
+
|
|
160
|
+
-- Paths are read only once the whole batch is attached. Formatting them as
|
|
161
|
+
-- each instance is made produces addresses that do not resolve: a child is
|
|
162
|
+
-- still detached from the data model while its parent is being assembled, and
|
|
163
|
+
-- a name shared with a sibling created later reads as unique because that
|
|
164
|
+
-- sibling does not exist yet.
|
|
165
|
+
local items: { { [string]: any } } = {}
|
|
166
|
+
local memo: Paths.NameIndex = {}
|
|
167
|
+
for _, instance in created do
|
|
168
|
+
table.insert(items, { path = Paths.of(instance, memo), className = instance.ClassName })
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
items = items,
|
|
173
|
+
undoStep = if recorded then "MCP create" else nil,
|
|
174
|
+
}
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
function Instances.modify(params: { [string]: any }): { [string]: any }
|
|
178
|
+
local targets = params.targets
|
|
179
|
+
if typeof(targets) ~= "table" or #targets == 0 then
|
|
180
|
+
Dispatch.fail(
|
|
181
|
+
"BAD_PARAMS",
|
|
182
|
+
"modify requires a non-empty `targets` array.",
|
|
183
|
+
"Each entry needs `paths` plus `properties`, `attributes` or `tags`."
|
|
184
|
+
)
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
-- Resolve everything before opening a recording, so a typo in the last path
|
|
188
|
+
-- costs nothing and leaves no undo entry behind.
|
|
189
|
+
local resolved: { { instance: Instance, spec: { [string]: any } } } = {}
|
|
190
|
+
for _, entry in targets do
|
|
191
|
+
local paths = entry.paths
|
|
192
|
+
if typeof(paths) ~= "table" or #paths == 0 then
|
|
193
|
+
Dispatch.fail("BAD_PARAMS", "Every modify target needs a non-empty `paths` array.")
|
|
194
|
+
end
|
|
195
|
+
for _, path in paths :: { string } do
|
|
196
|
+
table.insert(resolved, { instance = Paths.resolve(path), spec = entry })
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
local changed, recorded = Undo.record("StudioMCP.Modify", "MCP modify", function()
|
|
201
|
+
local items: { { [string]: any } } = {}
|
|
202
|
+
local memo: Paths.NameIndex = {}
|
|
203
|
+
|
|
204
|
+
for _, target in resolved do
|
|
205
|
+
local failures: { string } = {}
|
|
206
|
+
local applied = 0
|
|
207
|
+
|
|
208
|
+
for name, property in (target.spec.properties or {}) :: { [string]: PropertySpec } do
|
|
209
|
+
local failure = applyProperty(target.instance, name, property)
|
|
210
|
+
if failure then
|
|
211
|
+
table.insert(failures, failure)
|
|
212
|
+
else
|
|
213
|
+
applied += 1
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
local attributes = (target.spec.attributes or {}) :: { [string]: any }
|
|
218
|
+
for _, failure in applyAttributes(target.instance, attributes) do
|
|
219
|
+
table.insert(failures, failure)
|
|
220
|
+
end
|
|
221
|
+
for _ in attributes do
|
|
222
|
+
applied += 1
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
if target.spec.tags then
|
|
226
|
+
applyTags(target.instance, target.spec.tags)
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
-- One bad property fails the whole batch: the recording is cancelled
|
|
230
|
+
-- and every instance reverts, which is far easier to reason about
|
|
231
|
+
-- than a place half-way between two states.
|
|
232
|
+
if #failures > 0 then
|
|
233
|
+
Dispatch.fail(
|
|
234
|
+
"BAD_PROPERTY",
|
|
235
|
+
string.format(
|
|
236
|
+
"Could not set %d value(s) on %s.",
|
|
237
|
+
#failures,
|
|
238
|
+
target.instance:GetFullName()
|
|
239
|
+
),
|
|
240
|
+
table.concat(failures, "; ")
|
|
241
|
+
.. "\nNothing was changed: the whole batch was rolled back."
|
|
242
|
+
)
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
table.insert(items, {
|
|
246
|
+
path = Paths.of(target.instance, memo),
|
|
247
|
+
className = target.instance.ClassName,
|
|
248
|
+
changed = applied,
|
|
249
|
+
})
|
|
250
|
+
end
|
|
251
|
+
return items
|
|
252
|
+
end)
|
|
253
|
+
|
|
254
|
+
return {
|
|
255
|
+
items = changed,
|
|
256
|
+
undoStep = if recorded then "MCP modify" else nil,
|
|
257
|
+
}
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
function Instances.delete(params: { [string]: any }): { [string]: any }
|
|
261
|
+
local paths = params.paths
|
|
262
|
+
if typeof(paths) ~= "table" or #paths == 0 then
|
|
263
|
+
Dispatch.fail("BAD_PARAMS", "delete requires a non-empty `paths` array.")
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
local doomed: { Instance } = {}
|
|
267
|
+
for _, path in paths :: { string } do
|
|
268
|
+
local target = Paths.resolve(path)
|
|
269
|
+
-- Services cannot be destroyed and the attempt leaves Studio in a strange
|
|
270
|
+
-- state, so this is refused rather than passed to the engine.
|
|
271
|
+
if target == game or target.Parent == game then
|
|
272
|
+
Dispatch.fail(
|
|
273
|
+
"PROTECTED",
|
|
274
|
+
string.format('"%s" is a service and cannot be deleted.', path),
|
|
275
|
+
"Delete the contents instead, e.g. its children by path."
|
|
276
|
+
)
|
|
277
|
+
end
|
|
278
|
+
table.insert(doomed, target)
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
-- Paths are captured before anything is destroyed: an instance that has been
|
|
282
|
+
-- removed from the data model can no longer describe where it was.
|
|
283
|
+
local removed: { { [string]: any } } = {}
|
|
284
|
+
local memo: Paths.NameIndex = {}
|
|
285
|
+
for _, target in doomed do
|
|
286
|
+
table.insert(removed, {
|
|
287
|
+
path = Paths.of(target, memo),
|
|
288
|
+
className = target.ClassName,
|
|
289
|
+
descendants = #target:GetDescendants(),
|
|
290
|
+
})
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
local _, recorded = Undo.record("StudioMCP.Delete", "MCP delete", function()
|
|
294
|
+
for _, target in doomed do
|
|
295
|
+
target:Destroy()
|
|
296
|
+
end
|
|
297
|
+
return true
|
|
298
|
+
end)
|
|
299
|
+
|
|
300
|
+
return {
|
|
301
|
+
items = removed,
|
|
302
|
+
undoStep = if recorded then "MCP delete" else nil,
|
|
303
|
+
}
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
function Instances.move(params: { [string]: any }): { [string]: any }
|
|
307
|
+
local items = params.items
|
|
308
|
+
if typeof(items) ~= "table" or #items == 0 then
|
|
309
|
+
Dispatch.fail(
|
|
310
|
+
"BAD_PARAMS",
|
|
311
|
+
"move requires a non-empty `items` array.",
|
|
312
|
+
"Each entry needs `path` and `to`."
|
|
313
|
+
)
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
local planned: { { source: Instance, destination: Instance, spec: { [string]: any } } } = {}
|
|
317
|
+
for _, item in items do
|
|
318
|
+
local source = Paths.resolve(item.path)
|
|
319
|
+
local destination = Paths.resolve(item.to)
|
|
320
|
+
|
|
321
|
+
-- Reparenting something into itself detaches that whole branch from the
|
|
322
|
+
-- data model with no error, and it is not recoverable by undo.
|
|
323
|
+
if item.mode ~= "clone" and (destination == source or destination:IsDescendantOf(source)) then
|
|
324
|
+
Dispatch.fail(
|
|
325
|
+
"BAD_MOVE",
|
|
326
|
+
string.format('Cannot move "%s" into itself or its own descendant.', item.path),
|
|
327
|
+
"Pick a destination outside the branch being moved."
|
|
328
|
+
)
|
|
329
|
+
end
|
|
330
|
+
table.insert(planned, { source = source, destination = destination, spec = item })
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
local moved, recorded = Undo.record("StudioMCP.Move", "MCP move", function()
|
|
334
|
+
local results: { { instance: Instance, cloned: boolean } } = {}
|
|
335
|
+
for _, entry in planned do
|
|
336
|
+
local target = entry.source
|
|
337
|
+
if entry.spec.mode == "clone" then
|
|
338
|
+
-- Clone is typed as returning an Instance but hands back nil when
|
|
339
|
+
-- Archivable is false, which is how a locked or package-owned
|
|
340
|
+
-- instance refuses to be copied.
|
|
341
|
+
local copy = target:Clone()
|
|
342
|
+
if typeof(copy) ~= "Instance" then
|
|
343
|
+
Dispatch.fail(
|
|
344
|
+
"NOT_COPYABLE",
|
|
345
|
+
string.format('"%s" cannot be cloned.', target:GetFullName()),
|
|
346
|
+
"Archivable is false on this instance, which excludes it from copies."
|
|
347
|
+
)
|
|
348
|
+
end
|
|
349
|
+
target = copy
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
if typeof(entry.spec.name) == "string" and entry.spec.name ~= "" then
|
|
353
|
+
target.Name = entry.spec.name
|
|
354
|
+
end
|
|
355
|
+
target.Parent = entry.destination
|
|
356
|
+
|
|
357
|
+
table.insert(results, { instance = target, cloned = entry.spec.mode == "clone" })
|
|
358
|
+
end
|
|
359
|
+
return results
|
|
360
|
+
end)
|
|
361
|
+
|
|
362
|
+
-- As with create: a path formatted mid-batch can miss same-named siblings the
|
|
363
|
+
-- rest of the batch is about to add, so it is read once everything has landed.
|
|
364
|
+
local placed: { { [string]: any } } = {}
|
|
365
|
+
local memo: Paths.NameIndex = {}
|
|
366
|
+
for _, entry in moved do
|
|
367
|
+
table.insert(placed, {
|
|
368
|
+
path = Paths.of(entry.instance, memo),
|
|
369
|
+
className = entry.instance.ClassName,
|
|
370
|
+
cloned = entry.cloned,
|
|
371
|
+
})
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
return {
|
|
375
|
+
items = placed,
|
|
376
|
+
undoStep = if recorded then "MCP move" else nil,
|
|
377
|
+
}
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
function Instances.register()
|
|
381
|
+
Dispatch.registerAll("instances", {
|
|
382
|
+
create = Instances.create,
|
|
383
|
+
modify = Instances.modify,
|
|
384
|
+
delete = Instances.delete,
|
|
385
|
+
move = Instances.move,
|
|
386
|
+
})
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
return Instances
|