@chrrxs/robloxstudio-mcp 2.16.1 → 2.16.2
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/index.js +155 -23
- package/package.json +1 -1
- package/studio-plugin/MCPInspectorPlugin.rbxmx +159 -83
- package/studio-plugin/MCPPlugin.rbxmx +159 -83
- package/studio-plugin/src/modules/Communication.ts +41 -19
- package/studio-plugin/src/modules/ServerUrlSettings.ts +25 -12
- package/studio-plugin/src/modules/StopPlayMonitor.ts +54 -32
|
@@ -662,21 +662,24 @@ local function computeInstanceId()
|
|
|
662
662
|
end)
|
|
663
663
|
return `anon:{fresh}`
|
|
664
664
|
end
|
|
665
|
-
local instanceId = computeInstanceId()
|
|
666
665
|
local assignedRole
|
|
667
666
|
local duplicateInstanceRole = false
|
|
668
667
|
local hasVersionMismatch = false
|
|
669
668
|
local lastVersionMismatchWarningKey
|
|
669
|
+
local lastReadyInstanceId
|
|
670
670
|
local readyFailureLogKeys = {}
|
|
671
671
|
-- Cache the published place name from MarketplaceService:GetProductInfo so
|
|
672
672
|
-- /ready can carry a friendly identifier (e.g. "Natural Disasters") distinct
|
|
673
673
|
-- from game.Name (the DataModel name, often "Place1" in edit). We only fetch
|
|
674
674
|
-- once per plugin load; the published name doesn't change mid-session.
|
|
675
675
|
local cachedPlaceName
|
|
676
|
+
local cachedPlaceNamePlaceId
|
|
676
677
|
local function resolvePlaceName()
|
|
677
|
-
if cachedPlaceName ~= nil then
|
|
678
|
+
if cachedPlaceName ~= nil and cachedPlaceNamePlaceId == game.PlaceId then
|
|
678
679
|
return cachedPlaceName
|
|
679
680
|
end
|
|
681
|
+
cachedPlaceName = nil
|
|
682
|
+
cachedPlaceNamePlaceId = game.PlaceId
|
|
680
683
|
if game.PlaceId == 0 then
|
|
681
684
|
cachedPlaceName = game.Name
|
|
682
685
|
return cachedPlaceName
|
|
@@ -820,28 +823,36 @@ end
|
|
|
820
823
|
-- Without this, every poll during the brief window where the server has just
|
|
821
824
|
-- restarted but hasn't seen our re-ready yet would fire a duplicate /ready.
|
|
822
825
|
local lastReadyPostAt = 0
|
|
823
|
-
-- game.Name
|
|
824
|
-
--
|
|
825
|
-
--
|
|
826
|
-
-- get_connected_instances doesn't show a stale dataModelName forever. Set
|
|
827
|
-
-- up once per plugin load — the connection passed in is whichever was
|
|
828
|
-
-- active when activatePlugin was first called.
|
|
826
|
+
-- game.Name and game.PlaceId can both settle after plugin load. PlaceId also
|
|
827
|
+
-- changes when an unpublished file is published while MCP is already active.
|
|
828
|
+
-- Re-fire /ready so the bridge can migrate anon:<uuid> to place:<PlaceId>.
|
|
829
829
|
local nameChangeConn
|
|
830
|
+
local placeIdChangeConn
|
|
830
831
|
local sendReady
|
|
831
|
-
local function
|
|
832
|
-
if nameChangeConn then
|
|
833
|
-
|
|
832
|
+
local function ensureIdentityWatcher(conn)
|
|
833
|
+
if not nameChangeConn then
|
|
834
|
+
local okSig, signal = pcall(function()
|
|
835
|
+
return game:GetPropertyChangedSignal("Name")
|
|
836
|
+
end)
|
|
837
|
+
if okSig and signal then
|
|
838
|
+
nameChangeConn = signal:Connect(function()
|
|
839
|
+
-- sendReady has its own 2s throttle, so rapid burst changes coalesce.
|
|
840
|
+
sendReady(conn)
|
|
841
|
+
end)
|
|
842
|
+
end
|
|
834
843
|
end
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
844
|
+
if not placeIdChangeConn then
|
|
845
|
+
local okSig, signal = pcall(function()
|
|
846
|
+
return game:GetPropertyChangedSignal("PlaceId")
|
|
847
|
+
end)
|
|
848
|
+
if okSig and signal then
|
|
849
|
+
placeIdChangeConn = signal:Connect(function()
|
|
850
|
+
cachedPlaceName = nil
|
|
851
|
+
cachedPlaceNamePlaceId = nil
|
|
852
|
+
sendReady(conn)
|
|
853
|
+
end)
|
|
854
|
+
end
|
|
840
855
|
end
|
|
841
|
-
nameChangeConn = signal:Connect(function()
|
|
842
|
-
-- sendReady has its own 2s throttle, so rapid burst changes coalesce.
|
|
843
|
-
sendReady(conn)
|
|
844
|
-
end)
|
|
845
856
|
end
|
|
846
857
|
function sendReady(conn)
|
|
847
858
|
if duplicateInstanceRole then
|
|
@@ -852,6 +863,7 @@ function sendReady(conn)
|
|
|
852
863
|
return nil
|
|
853
864
|
end
|
|
854
865
|
lastReadyPostAt = now
|
|
866
|
+
local instanceId = computeInstanceId()
|
|
855
867
|
task.spawn(function()
|
|
856
868
|
local readyOk, readyResult = pcall(function()
|
|
857
869
|
return HttpService:RequestAsync({
|
|
@@ -910,11 +922,20 @@ function sendReady(conn)
|
|
|
910
922
|
if _value ~= "" and _value then
|
|
911
923
|
assignedRole = readyData.assignedRole
|
|
912
924
|
end
|
|
913
|
-
local _condition =
|
|
914
|
-
if _condition
|
|
915
|
-
|
|
925
|
+
local _condition = parseOk
|
|
926
|
+
if _condition then
|
|
927
|
+
local _instanceId = readyData.instanceId
|
|
928
|
+
_condition = type(_instanceId) == "string"
|
|
929
|
+
if _condition then
|
|
930
|
+
_condition = readyData.instanceId ~= ""
|
|
931
|
+
end
|
|
916
932
|
end
|
|
917
|
-
|
|
933
|
+
lastReadyInstanceId = if _condition then readyData.instanceId else instanceId
|
|
934
|
+
local _condition_1 = assignedRole
|
|
935
|
+
if _condition_1 == nil then
|
|
936
|
+
_condition_1 = detectRole()
|
|
937
|
+
end
|
|
938
|
+
local connectedRole = _condition_1
|
|
918
939
|
if readyFailureLogKeys[readyLogKey] ~= nil then
|
|
919
940
|
readyFailureLogKeys[readyLogKey] = nil
|
|
920
941
|
print(`[robloxstudio-mcp] /ready connected for {instanceId}/{connectedRole} via {conn.serverUrl}`)
|
|
@@ -1136,6 +1157,12 @@ local function activatePlugin(connIndex)
|
|
|
1136
1157
|
if not conn.heartbeatConnection then
|
|
1137
1158
|
conn.heartbeatConnection = RunService.Heartbeat:Connect(function()
|
|
1138
1159
|
local now = tick()
|
|
1160
|
+
local currentInstanceId = computeInstanceId()
|
|
1161
|
+
if lastReadyInstanceId ~= nil and currentInstanceId ~= lastReadyInstanceId then
|
|
1162
|
+
cachedPlaceName = nil
|
|
1163
|
+
cachedPlaceNamePlaceId = nil
|
|
1164
|
+
sendReady(conn)
|
|
1165
|
+
end
|
|
1139
1166
|
local currentInterval = if conn.consecutiveFailures > 5 then conn.currentRetryDelay else conn.pollInterval
|
|
1140
1167
|
if now - conn.lastPoll > currentInterval then
|
|
1141
1168
|
conn.lastPoll = now
|
|
@@ -1151,9 +1178,8 @@ local function activatePlugin(connIndex)
|
|
|
1151
1178
|
if not RunService:IsRunning() then
|
|
1152
1179
|
task.spawn(cleanupLegacyEditBridges)
|
|
1153
1180
|
end
|
|
1154
|
-
-- Watch
|
|
1155
|
-
|
|
1156
|
-
ensureNameChangeWatcher(conn)
|
|
1181
|
+
-- Watch identity fields so stale name or anon instance ids are refreshed.
|
|
1182
|
+
ensureIdentityWatcher(conn)
|
|
1157
1183
|
end
|
|
1158
1184
|
local function deactivatePlugin(connIndex)
|
|
1159
1185
|
local _condition = connIndex
|
|
@@ -1362,9 +1388,9 @@ local function computeBridgeStamp()
|
|
|
1362
1388
|
for i = 1, #combined do
|
|
1363
1389
|
h = (h * 33 + (string.byte(combined, i))) % 2147483647
|
|
1364
1390
|
end
|
|
1365
|
-
-- "2.16.
|
|
1391
|
+
-- "2.16.2" is replaced with the package version at package time
|
|
1366
1392
|
-- (scripts/build-plugin.mjs injectVersion), so a release bump also restamps.
|
|
1367
|
-
return `{tostring(h)}-2.16.
|
|
1393
|
+
return `{tostring(h)}-2.16.2`
|
|
1368
1394
|
end
|
|
1369
1395
|
local BRIDGE_STAMP = computeBridgeStamp()
|
|
1370
1396
|
local function setSource(scriptInst, source)
|
|
@@ -7925,19 +7951,31 @@ local pluginRef
|
|
|
7925
7951
|
local function init(p)
|
|
7926
7952
|
pluginRef = p
|
|
7927
7953
|
end
|
|
7928
|
-
local function
|
|
7954
|
+
local function addUnique(values, value)
|
|
7955
|
+
local _values = values
|
|
7956
|
+
local _value = value
|
|
7957
|
+
if not (table.find(_values, _value) ~= nil) then
|
|
7958
|
+
local _values_1 = values
|
|
7959
|
+
local _value_1 = value
|
|
7960
|
+
table.insert(_values_1, _value_1)
|
|
7961
|
+
end
|
|
7962
|
+
end
|
|
7963
|
+
local function computeInstanceIds()
|
|
7964
|
+
local ids = {}
|
|
7929
7965
|
if game.PlaceId ~= 0 then
|
|
7930
|
-
|
|
7966
|
+
addUnique(ids, `place:{tostring(game.PlaceId)}`)
|
|
7931
7967
|
end
|
|
7932
7968
|
local existing = ServerStorage:GetAttribute("__MCPPlaceId")
|
|
7933
7969
|
if type(existing) == "string" and existing ~= "" then
|
|
7934
|
-
|
|
7970
|
+
addUnique(ids, `anon:{existing}`)
|
|
7971
|
+
elseif game.PlaceId == 0 then
|
|
7972
|
+
local fresh = HttpService:GenerateGUID(false)
|
|
7973
|
+
pcall(function()
|
|
7974
|
+
return ServerStorage:SetAttribute("__MCPPlaceId", fresh)
|
|
7975
|
+
end)
|
|
7976
|
+
addUnique(ids, `anon:{fresh}`)
|
|
7935
7977
|
end
|
|
7936
|
-
|
|
7937
|
-
pcall(function()
|
|
7938
|
-
return ServerStorage:SetAttribute("__MCPPlaceId", fresh)
|
|
7939
|
-
end)
|
|
7940
|
-
return `anon:{fresh}`
|
|
7978
|
+
return ids
|
|
7941
7979
|
end
|
|
7942
7980
|
local function settingKey(instanceId)
|
|
7943
7981
|
return SETTING_KEY_PREFIX .. instanceId
|
|
@@ -7946,21 +7984,25 @@ local function rememberServerUrl(serverUrl)
|
|
|
7946
7984
|
if not pluginRef or serverUrl == "" then
|
|
7947
7985
|
return nil
|
|
7948
7986
|
end
|
|
7949
|
-
|
|
7950
|
-
|
|
7951
|
-
|
|
7952
|
-
|
|
7987
|
+
for _, instanceId in computeInstanceIds() do
|
|
7988
|
+
local key = settingKey(instanceId)
|
|
7989
|
+
pcall(function()
|
|
7990
|
+
return pluginRef:SetSetting(key, serverUrl)
|
|
7991
|
+
end)
|
|
7992
|
+
end
|
|
7953
7993
|
end
|
|
7954
7994
|
local function readServerUrl()
|
|
7955
7995
|
if not pluginRef then
|
|
7956
7996
|
return nil
|
|
7957
7997
|
end
|
|
7958
|
-
|
|
7959
|
-
|
|
7960
|
-
|
|
7961
|
-
|
|
7962
|
-
|
|
7963
|
-
|
|
7998
|
+
for _, instanceId in computeInstanceIds() do
|
|
7999
|
+
local key = settingKey(instanceId)
|
|
8000
|
+
local ok, value = pcall(function()
|
|
8001
|
+
return pluginRef:GetSetting(key)
|
|
8002
|
+
end)
|
|
8003
|
+
if ok and type(value) == "string" and value ~= "" then
|
|
8004
|
+
return value
|
|
8005
|
+
end
|
|
7964
8006
|
end
|
|
7965
8007
|
return nil
|
|
7966
8008
|
end
|
|
@@ -7976,7 +8018,7 @@ return {
|
|
|
7976
8018
|
<Properties>
|
|
7977
8019
|
<string name="Name">State</string>
|
|
7978
8020
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
7979
|
-
local CURRENT_VERSION = "2.16.
|
|
8021
|
+
local CURRENT_VERSION = "2.16.2"
|
|
7980
8022
|
local PLUGIN_VARIANT = "main"
|
|
7981
8023
|
local MAX_CONNECTIONS = 5
|
|
7982
8024
|
local BASE_PORT = 58741
|
|
@@ -8078,6 +8120,9 @@ local TS = require(script.Parent.Parent.include.RuntimeLib)
|
|
|
8078
8120
|
-- Cross-DM stop_playtest signaling via plugin:SetSetting, scoped by
|
|
8079
8121
|
-- per-instance setting key so the same Studio process can host playtests
|
|
8080
8122
|
-- for multiple places without one place's stop_playtest yanking another's.
|
|
8123
|
+
-- During publish-after-connect, both "anon:<uuid>" and "place:<PlaceId>"
|
|
8124
|
+
-- can refer to the same Studio place, so stop requests are mirrored across
|
|
8125
|
+
-- both keys while the monitor waits for a matching result on either key.
|
|
8081
8126
|
--
|
|
8082
8127
|
-- `plugin:SetSetting` / `plugin:GetSetting` is a per-plugin persistent store
|
|
8083
8128
|
-- shared across every DataModel the plugin runs in (edit DMs, play-server
|
|
@@ -8125,23 +8170,48 @@ end
|
|
|
8125
8170
|
-- agree on the place identifier (published places: placeId; unpublished:
|
|
8126
8171
|
-- UUID on ServerStorage's __MCPPlaceId attribute, travels with the .rbxl
|
|
8127
8172
|
-- into the play DM).
|
|
8128
|
-
local function
|
|
8173
|
+
local function addUnique(values, value)
|
|
8174
|
+
local _values = values
|
|
8175
|
+
local _value = value
|
|
8176
|
+
if not (table.find(_values, _value) ~= nil) then
|
|
8177
|
+
local _values_1 = values
|
|
8178
|
+
local _value_1 = value
|
|
8179
|
+
table.insert(_values_1, _value_1)
|
|
8180
|
+
end
|
|
8181
|
+
end
|
|
8182
|
+
local function computeInstanceIds()
|
|
8183
|
+
local ids = {}
|
|
8129
8184
|
if game.PlaceId ~= 0 then
|
|
8130
|
-
|
|
8185
|
+
addUnique(ids, `place:{tostring(game.PlaceId)}`)
|
|
8131
8186
|
end
|
|
8132
8187
|
local existing = ServerStorage:GetAttribute("__MCPPlaceId")
|
|
8133
8188
|
if type(existing) == "string" and existing ~= "" then
|
|
8134
|
-
|
|
8189
|
+
addUnique(ids, `anon:{existing}`)
|
|
8190
|
+
elseif game.PlaceId == 0 then
|
|
8191
|
+
local fresh = HttpService:GenerateGUID(false)
|
|
8192
|
+
pcall(function()
|
|
8193
|
+
return ServerStorage:SetAttribute("__MCPPlaceId", fresh)
|
|
8194
|
+
end)
|
|
8195
|
+
addUnique(ids, `anon:{fresh}`)
|
|
8135
8196
|
end
|
|
8136
|
-
|
|
8137
|
-
pcall(function()
|
|
8138
|
-
return ServerStorage:SetAttribute("__MCPPlaceId", fresh)
|
|
8139
|
-
end)
|
|
8140
|
-
return `anon:{fresh}`
|
|
8197
|
+
return ids
|
|
8141
8198
|
end
|
|
8142
8199
|
local function settingKey(instanceId)
|
|
8143
8200
|
return SETTING_KEY_PREFIX .. instanceId
|
|
8144
8201
|
end
|
|
8202
|
+
local function settingKeys()
|
|
8203
|
+
local _exp = computeInstanceIds()
|
|
8204
|
+
-- ▼ ReadonlyArray.map ▼
|
|
8205
|
+
local _newValue = table.create(#_exp)
|
|
8206
|
+
local _callback = function(instanceId)
|
|
8207
|
+
return settingKey(instanceId)
|
|
8208
|
+
end
|
|
8209
|
+
for _k, _v in _exp do
|
|
8210
|
+
_newValue[_k] = _callback(_v, _k - 1, _exp)
|
|
8211
|
+
end
|
|
8212
|
+
-- ▲ ReadonlyArray.map ▲
|
|
8213
|
+
return _newValue
|
|
8214
|
+
end
|
|
8145
8215
|
local function readSetting(key)
|
|
8146
8216
|
if not pluginRef then
|
|
8147
8217
|
return nil
|
|
@@ -8248,18 +8318,19 @@ local function startMonitor()
|
|
|
8248
8318
|
warn("[robloxstudio-mcp] StopPlayMonitor.startMonitor called before init; skipping")
|
|
8249
8319
|
return nil
|
|
8250
8320
|
end
|
|
8251
|
-
local myKey = settingKey(computeInstanceId())
|
|
8252
8321
|
task.spawn(function()
|
|
8253
8322
|
while true do
|
|
8254
|
-
|
|
8255
|
-
|
|
8256
|
-
|
|
8257
|
-
|
|
8258
|
-
|
|
8259
|
-
|
|
8260
|
-
|
|
8261
|
-
|
|
8262
|
-
|
|
8323
|
+
for _, myKey in settingKeys() do
|
|
8324
|
+
local value = readSetting(myKey)
|
|
8325
|
+
if value == true then
|
|
8326
|
+
-- Legacy boolean requests are ambiguous and may be stale from
|
|
8327
|
+
-- a prior crashed session. New stop requests use token payloads.
|
|
8328
|
+
writeSetting(myKey, false)
|
|
8329
|
+
else
|
|
8330
|
+
local payload = decodePayload(value)
|
|
8331
|
+
if payload then
|
|
8332
|
+
handleStopRequest(myKey, payload)
|
|
8333
|
+
end
|
|
8263
8334
|
end
|
|
8264
8335
|
end
|
|
8265
8336
|
task.wait(POLL_INTERVAL_SEC)
|
|
@@ -8272,13 +8343,16 @@ local function requestStop()
|
|
|
8272
8343
|
ok = false,
|
|
8273
8344
|
}
|
|
8274
8345
|
end
|
|
8275
|
-
local myKey = settingKey(computeInstanceId())
|
|
8276
8346
|
local requestId = HttpService:GenerateGUID(false)
|
|
8277
|
-
local
|
|
8347
|
+
local payload = {
|
|
8278
8348
|
kind = "request",
|
|
8279
8349
|
id = requestId,
|
|
8280
8350
|
requestedAt = tick(),
|
|
8281
|
-
}
|
|
8351
|
+
}
|
|
8352
|
+
local ok = false
|
|
8353
|
+
for _, myKey in settingKeys() do
|
|
8354
|
+
ok = writePayload(myKey, payload) or ok
|
|
8355
|
+
end
|
|
8282
8356
|
return {
|
|
8283
8357
|
ok = ok,
|
|
8284
8358
|
requestId = if ok then requestId else nil,
|
|
@@ -8292,16 +8366,17 @@ local function waitForConsumption(requestId)
|
|
|
8292
8366
|
error = "Plugin reference is not initialized.",
|
|
8293
8367
|
}
|
|
8294
8368
|
end
|
|
8295
|
-
local myKey = settingKey(computeInstanceId())
|
|
8296
8369
|
local start = tick()
|
|
8297
8370
|
while tick() - start < WAIT_FOR_CONSUMPTION_TIMEOUT_SEC do
|
|
8298
|
-
|
|
8299
|
-
|
|
8300
|
-
|
|
8301
|
-
|
|
8302
|
-
|
|
8303
|
-
|
|
8304
|
-
|
|
8371
|
+
for _, myKey in settingKeys() do
|
|
8372
|
+
local payload = decodePayload(readSetting(myKey))
|
|
8373
|
+
if payload and payload.kind == "result" and payload.id == requestId then
|
|
8374
|
+
return {
|
|
8375
|
+
ok = payload.ok == true,
|
|
8376
|
+
consumed = true,
|
|
8377
|
+
error = payload.error,
|
|
8378
|
+
}
|
|
8379
|
+
end
|
|
8305
8380
|
end
|
|
8306
8381
|
task.wait(WAIT_POLL_SEC)
|
|
8307
8382
|
end
|
|
@@ -8315,14 +8390,15 @@ local function clearPending(requestId)
|
|
|
8315
8390
|
if not pluginRef then
|
|
8316
8391
|
return nil
|
|
8317
8392
|
end
|
|
8318
|
-
|
|
8319
|
-
|
|
8320
|
-
|
|
8321
|
-
|
|
8322
|
-
|
|
8393
|
+
for _, myKey in settingKeys() do
|
|
8394
|
+
if requestId ~= nil then
|
|
8395
|
+
local payload = decodePayload(readSetting(myKey))
|
|
8396
|
+
if payload and payload.id ~= requestId then
|
|
8397
|
+
continue
|
|
8398
|
+
end
|
|
8323
8399
|
end
|
|
8400
|
+
writeSetting(myKey, false)
|
|
8324
8401
|
end
|
|
8325
|
-
writeSetting(myKey, false)
|
|
8326
8402
|
end
|
|
8327
8403
|
return {
|
|
8328
8404
|
init = init,
|
|
@@ -47,11 +47,11 @@ function computeInstanceId(): string {
|
|
|
47
47
|
return `anon:${fresh}`;
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
const instanceId = computeInstanceId();
|
|
51
50
|
let assignedRole: string | undefined;
|
|
52
51
|
let duplicateInstanceRole = false;
|
|
53
52
|
let hasVersionMismatch = false;
|
|
54
53
|
let lastVersionMismatchWarningKey: string | undefined;
|
|
54
|
+
let lastReadyInstanceId: string | undefined;
|
|
55
55
|
const readyFailureLogKeys = new Set<string>();
|
|
56
56
|
|
|
57
57
|
// Cache the published place name from MarketplaceService:GetProductInfo so
|
|
@@ -59,9 +59,12 @@ const readyFailureLogKeys = new Set<string>();
|
|
|
59
59
|
// from game.Name (the DataModel name, often "Place1" in edit). We only fetch
|
|
60
60
|
// once per plugin load; the published name doesn't change mid-session.
|
|
61
61
|
let cachedPlaceName: string | undefined;
|
|
62
|
+
let cachedPlaceNamePlaceId: number | undefined;
|
|
62
63
|
|
|
63
64
|
function resolvePlaceName(): string {
|
|
64
|
-
if (cachedPlaceName !== undefined) return cachedPlaceName;
|
|
65
|
+
if (cachedPlaceName !== undefined && cachedPlaceNamePlaceId === game.PlaceId) return cachedPlaceName;
|
|
66
|
+
cachedPlaceName = undefined;
|
|
67
|
+
cachedPlaceNamePlaceId = game.PlaceId;
|
|
65
68
|
if (game.PlaceId === 0) {
|
|
66
69
|
cachedPlaceName = game.Name;
|
|
67
70
|
return cachedPlaceName;
|
|
@@ -209,21 +212,31 @@ function getConnectionStatus(connIndex: number): string {
|
|
|
209
212
|
// restarted but hasn't seen our re-ready yet would fire a duplicate /ready.
|
|
210
213
|
let lastReadyPostAt = 0;
|
|
211
214
|
|
|
212
|
-
// game.Name
|
|
213
|
-
//
|
|
214
|
-
//
|
|
215
|
-
// get_connected_instances doesn't show a stale dataModelName forever. Set
|
|
216
|
-
// up once per plugin load — the connection passed in is whichever was
|
|
217
|
-
// active when activatePlugin was first called.
|
|
215
|
+
// game.Name and game.PlaceId can both settle after plugin load. PlaceId also
|
|
216
|
+
// changes when an unpublished file is published while MCP is already active.
|
|
217
|
+
// Re-fire /ready so the bridge can migrate anon:<uuid> to place:<PlaceId>.
|
|
218
218
|
let nameChangeConn: RBXScriptConnection | undefined;
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
219
|
+
let placeIdChangeConn: RBXScriptConnection | undefined;
|
|
220
|
+
function ensureIdentityWatcher(conn: Connection): void {
|
|
221
|
+
if (!nameChangeConn) {
|
|
222
|
+
const [okSig, signal] = pcall(() => game.GetPropertyChangedSignal("Name"));
|
|
223
|
+
if (okSig && signal) {
|
|
224
|
+
nameChangeConn = signal.Connect(() => {
|
|
225
|
+
// sendReady has its own 2s throttle, so rapid burst changes coalesce.
|
|
226
|
+
sendReady(conn);
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
if (!placeIdChangeConn) {
|
|
231
|
+
const [okSig, signal] = pcall(() => game.GetPropertyChangedSignal("PlaceId"));
|
|
232
|
+
if (okSig && signal) {
|
|
233
|
+
placeIdChangeConn = signal.Connect(() => {
|
|
234
|
+
cachedPlaceName = undefined;
|
|
235
|
+
cachedPlaceNamePlaceId = undefined;
|
|
236
|
+
sendReady(conn);
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
}
|
|
227
240
|
}
|
|
228
241
|
|
|
229
242
|
function sendReady(conn: Connection): void {
|
|
@@ -231,6 +244,7 @@ function sendReady(conn: Connection): void {
|
|
|
231
244
|
const now = tick();
|
|
232
245
|
if (now - lastReadyPostAt < 2) return; // throttle to ≤1 /ready every 2s
|
|
233
246
|
lastReadyPostAt = now;
|
|
247
|
+
const instanceId = computeInstanceId();
|
|
234
248
|
task.spawn(() => {
|
|
235
249
|
const [readyOk, readyResult] = pcall(() => {
|
|
236
250
|
return HttpService.RequestAsync({
|
|
@@ -286,6 +300,9 @@ function sendReady(conn: Connection): void {
|
|
|
286
300
|
if (parseOk && readyData.assignedRole) {
|
|
287
301
|
assignedRole = readyData.assignedRole;
|
|
288
302
|
}
|
|
303
|
+
lastReadyInstanceId = parseOk && typeIs(readyData.instanceId, "string") && readyData.instanceId !== ""
|
|
304
|
+
? readyData.instanceId
|
|
305
|
+
: instanceId;
|
|
289
306
|
const connectedRole = assignedRole ?? detectRole();
|
|
290
307
|
if (readyFailureLogKeys.has(readyLogKey)) {
|
|
291
308
|
readyFailureLogKeys.delete(readyLogKey);
|
|
@@ -493,6 +510,12 @@ function activatePlugin(connIndex?: number) {
|
|
|
493
510
|
if (!conn.heartbeatConnection) {
|
|
494
511
|
conn.heartbeatConnection = RunService.Heartbeat.Connect(() => {
|
|
495
512
|
const now = tick();
|
|
513
|
+
const currentInstanceId = computeInstanceId();
|
|
514
|
+
if (lastReadyInstanceId !== undefined && currentInstanceId !== lastReadyInstanceId) {
|
|
515
|
+
cachedPlaceName = undefined;
|
|
516
|
+
cachedPlaceNamePlaceId = undefined;
|
|
517
|
+
sendReady(conn);
|
|
518
|
+
}
|
|
496
519
|
const currentInterval = conn.consecutiveFailures > 5 ? conn.currentRetryDelay : conn.pollInterval;
|
|
497
520
|
if (now - conn.lastPoll > currentInterval) {
|
|
498
521
|
conn.lastPoll = now;
|
|
@@ -511,9 +534,8 @@ function activatePlugin(connIndex?: number) {
|
|
|
511
534
|
task.spawn(cleanupLegacyEditBridges);
|
|
512
535
|
}
|
|
513
536
|
|
|
514
|
-
// Watch
|
|
515
|
-
|
|
516
|
-
ensureNameChangeWatcher(conn);
|
|
537
|
+
// Watch identity fields so stale name or anon instance ids are refreshed.
|
|
538
|
+
ensureIdentityWatcher(conn);
|
|
517
539
|
}
|
|
518
540
|
|
|
519
541
|
function deactivatePlugin(connIndex?: number) {
|
|
@@ -8,17 +8,26 @@ function init(p: Plugin): void {
|
|
|
8
8
|
pluginRef = p;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
function
|
|
11
|
+
function addUnique(values: string[], value: string): void {
|
|
12
|
+
if (!values.includes(value)) {
|
|
13
|
+
values.push(value);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function computeInstanceIds(): string[] {
|
|
18
|
+
const ids: string[] = [];
|
|
12
19
|
if (game.PlaceId !== 0) {
|
|
13
|
-
|
|
20
|
+
addUnique(ids, `place:${tostring(game.PlaceId)}`);
|
|
14
21
|
}
|
|
15
22
|
const existing = ServerStorage.GetAttribute("__MCPPlaceId");
|
|
16
23
|
if (typeIs(existing, "string") && existing !== "") {
|
|
17
|
-
|
|
24
|
+
addUnique(ids, `anon:${existing as string}`);
|
|
25
|
+
} else if (game.PlaceId === 0) {
|
|
26
|
+
const fresh = HttpService.GenerateGUID(false);
|
|
27
|
+
pcall(() => ServerStorage.SetAttribute("__MCPPlaceId", fresh));
|
|
28
|
+
addUnique(ids, `anon:${fresh}`);
|
|
18
29
|
}
|
|
19
|
-
|
|
20
|
-
pcall(() => ServerStorage.SetAttribute("__MCPPlaceId", fresh));
|
|
21
|
-
return `anon:${fresh}`;
|
|
30
|
+
return ids;
|
|
22
31
|
}
|
|
23
32
|
|
|
24
33
|
function settingKey(instanceId: string): string {
|
|
@@ -27,16 +36,20 @@ function settingKey(instanceId: string): string {
|
|
|
27
36
|
|
|
28
37
|
function rememberServerUrl(serverUrl: string): void {
|
|
29
38
|
if (!pluginRef || serverUrl === "") return;
|
|
30
|
-
const
|
|
31
|
-
|
|
39
|
+
for (const instanceId of computeInstanceIds()) {
|
|
40
|
+
const key = settingKey(instanceId);
|
|
41
|
+
pcall(() => pluginRef!.SetSetting(key, serverUrl));
|
|
42
|
+
}
|
|
32
43
|
}
|
|
33
44
|
|
|
34
45
|
function readServerUrl(): string | undefined {
|
|
35
46
|
if (!pluginRef) return undefined;
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
47
|
+
for (const instanceId of computeInstanceIds()) {
|
|
48
|
+
const key = settingKey(instanceId);
|
|
49
|
+
const [ok, value] = pcall(() => pluginRef!.GetSetting(key));
|
|
50
|
+
if (ok && typeIs(value, "string") && value !== "") {
|
|
51
|
+
return value as string;
|
|
52
|
+
}
|
|
40
53
|
}
|
|
41
54
|
return undefined;
|
|
42
55
|
}
|