@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.
@@ -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 is sometimes "Place1" at plugin-load time and only settles to
824
- -- the real DataModel name (e.g. "Game" once playtest spawns the play DM)
825
- -- after Studio finishes wiring things up. Re-fire /ready when it changes so
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 ensureNameChangeWatcher(conn)
832
- if nameChangeConn then
833
- return nil
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
- local okSig, signal = pcall(function()
836
- return game:GetPropertyChangedSignal("Name")
837
- end)
838
- if not okSig or not signal then
839
- return nil
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 = assignedRole
914
- if _condition == nil then
915
- _condition = detectRole()
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
- local connectedRole = _condition
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 for game.Name updates so a stale "Place1" captured at first
1155
- -- /ready gets refreshed once Studio settles on the real DM name.
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.1" is replaced with the package version at package time
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.1`
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 computeInstanceId()
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
- return `place:{tostring(game.PlaceId)}`
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
- return `anon:{existing}`
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
- local fresh = HttpService:GenerateGUID(false)
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
- local key = settingKey(computeInstanceId())
7950
- pcall(function()
7951
- return pluginRef:SetSetting(key, serverUrl)
7952
- end)
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
- local key = settingKey(computeInstanceId())
7959
- local ok, value = pcall(function()
7960
- return pluginRef:GetSetting(key)
7961
- end)
7962
- if ok and type(value) == "string" and value ~= "" then
7963
- return value
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.1"
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 computeInstanceId()
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
- return `place:{tostring(game.PlaceId)}`
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
- return `anon:{existing}`
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
- local fresh = HttpService:GenerateGUID(false)
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
- local value = readSetting(myKey)
8255
- if value == true then
8256
- -- Legacy boolean requests are ambiguous and may be stale from
8257
- -- a prior crashed session. New stop requests use token payloads.
8258
- writeSetting(myKey, false)
8259
- else
8260
- local payload = decodePayload(value)
8261
- if payload then
8262
- handleStopRequest(myKey, payload)
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 ok = writePayload(myKey, {
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
- local payload = decodePayload(readSetting(myKey))
8299
- if payload and payload.kind == "result" and payload.id == requestId then
8300
- return {
8301
- ok = payload.ok == true,
8302
- consumed = true,
8303
- error = payload.error,
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
- local myKey = settingKey(computeInstanceId())
8319
- if requestId ~= nil then
8320
- local payload = decodePayload(readSetting(myKey))
8321
- if payload and payload.id ~= requestId then
8322
- return nil
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 is sometimes "Place1" at plugin-load time and only settles to
213
- // the real DataModel name (e.g. "Game" once playtest spawns the play DM)
214
- // after Studio finishes wiring things up. Re-fire /ready when it changes so
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
- function ensureNameChangeWatcher(conn: Connection): void {
220
- if (nameChangeConn) return;
221
- const [okSig, signal] = pcall(() => game.GetPropertyChangedSignal("Name"));
222
- if (!okSig || !signal) return;
223
- nameChangeConn = signal.Connect(() => {
224
- // sendReady has its own 2s throttle, so rapid burst changes coalesce.
225
- sendReady(conn);
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 for game.Name updates so a stale "Place1" captured at first
515
- // /ready gets refreshed once Studio settles on the real DM name.
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 computeInstanceId(): string {
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
- return `place:${tostring(game.PlaceId)}`;
20
+ addUnique(ids, `place:${tostring(game.PlaceId)}`);
14
21
  }
15
22
  const existing = ServerStorage.GetAttribute("__MCPPlaceId");
16
23
  if (typeIs(existing, "string") && existing !== "") {
17
- return `anon:${existing as string}`;
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
- const fresh = HttpService.GenerateGUID(false);
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 key = settingKey(computeInstanceId());
31
- pcall(() => pluginRef!.SetSetting(key, serverUrl));
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 key = settingKey(computeInstanceId());
37
- const [ok, value] = pcall(() => pluginRef!.GetSetting(key));
38
- if (ok && typeIs(value, "string") && value !== "") {
39
- return value as string;
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
  }