For Marching order, could you do something like this? 1. Disable the existing MarchingOrder scripts, including any version installed through the Roll20 Mod library. Otherwise, the old code may continue producing the same error. 2. Create a new script and paste this in? /* * MarchingOrder - Proxy-Safe Edition * * Commands: * !follow Token, Leader * !unfollow Token * !march Leader, Token2, Token3 * !unmarch Leader * !clearFollow * * Tokens follow the previous positions of the * tokens directly ahead of them. * * No custom properties are written to Roll20 objects. */ var MarchingOrderSafe = MarchingOrderSafe || (function () { 'use strict'; var VERSION = '2.0'; var KEY = 'MarchingOrderSafe'; // Tracks movement generated by this script. // This is intentionally not persisted. var pendingMoves = {}; function data() { if (!state[KEY]) { state[KEY] = { parent: {}, child: {} }; } return state[KEY]; } function pageID() { return Campaign().get('playerpageid'); } function tokenByName(name) { var matches = findObjs({ _type: 'graphic', _subtype: 'token', _pageid: pageID(), layer: 'objects', name: name.trim() }); return matches.length ? matches[0] : null; } function tokenByID(id) { return id ? getObj('graphic', id) : null; } function whisper(msg, text) { var target = playerIsGM(msg.playerid) ? 'gm' : '"' + msg.who.replace(/\s+\(GM\)$/, '') + '"'; sendChat('MarchingOrder', '/w ' + target + ' ' + text); } function canControl(playerID, token) { if (playerIsGM(playerID)) { return true; } if (!token) { return false; } var controllers = []; var direct = token.get('controlledby') || ''; if (direct) { controllers = controllers.concat(direct.split(',')); } var characterID = token.get('represents'); var character = characterID ? getObj('character', characterID) : null; if (character) { var characterControllers = character.get('controlledby') || ''; if (characterControllers) { controllers = controllers.concat( characterControllers.split(',') ); } } return controllers.indexOf('all') !== -1 || controllers.indexOf(playerID) !== -1; } // Remove a token from a marching line, // connecting the tokens on either side. function unlink(id) { var d = data(); var ahead = d.parent[id]; var behind = d.child[id]; if (ahead) { if (behind) { d.child[ahead] = behind; } else { delete d.child[ahead]; } } if (behind) { if (ahead) { d.parent[behind] = ahead; } else { delete d.parent[behind]; } } delete d.parent[id]; delete d.child[id]; } // Insert follower immediately behind leader. function link(leaderID, followerID) { if (leaderID === followerID) { return false; } var d = data(); // Detach the follower from any previous line. unlink(followerID); var nextID = d.child[leaderID]; d.child[leaderID] = followerID; d.parent[followerID] = leaderID; if (nextID && nextID !== followerID) { d.child[followerID] = nextID; d.parent[nextID] = followerID; } return true; } function follow(msg, followerName, leaderName) { var follower = tokenByName(followerName); var leader = tokenByName(leaderName); if (!follower || !leader) { whisper(msg, 'One or both tokens were not found.'); return; } if (!canControl(msg.playerid, follower)) { whisper(msg, 'You do not control ' + followerName + '.'); return; } if (!link(leader.id, follower.id)) { whisper(msg, 'A token cannot follow itself.'); return; } sendChat('MarchingOrder', '/em ' + followerName + ' is following ' + leaderName + '.' ); } function unfollow(msg, name) { var token = tokenByName(name); if (!token) { whisper(msg, 'Token not found: ' + name); return; } if (!canControl(msg.playerid, token)) { whisper(msg, 'You do not control ' + name + '.'); return; } unlink(token.id); sendChat('MarchingOrder', '/em ' + name + ' stepped out of marching order.' ); } function march(msg, names) { var tokens = []; var seen = {}; // Validate the full order first. for (var i = 0; i < names.length; i++) { var name = names[i].trim(); var token = tokenByName(name); if (!token) { whisper(msg, 'Token not found: ' + name); return; } if (seen[token.id]) { whisper(msg, 'Duplicate token: ' + name); return; } seen[token.id] = true; tokens.push(token); } if (tokens.length < 2) { whisper(msg, 'Specify at least two tokens.'); return; } // Remove existing links for all participants. tokens.forEach(function (token) { unlink(token.id); }); // Build the new order from front to back. for (var j = 1; j < tokens.length; j++) { link(tokens[j - 1].id, tokens[j].id); } whisper(msg, 'Marching: ' + names.join(', ')); } function unmarch(msg, leaderName) { var leader = tokenByName(leaderName); if (!leader) { whisper(msg, 'Token not found: ' + leaderName); return; } var d = data(); var currentID = leader.id; var visited = {}; while (currentID && !visited[currentID]) { visited[currentID] = true; var nextID = d.child[currentID]; delete d.parent[currentID]; delete d.child[currentID]; currentID = nextID; } whisper(msg, 'Disbanded ' + leaderName + "'s marching line." ); } function clearFollow(msg) { var d = data(); var tokens = findObjs({ _type: 'graphic', _subtype: 'token', _pageid: pageID() }); tokens.forEach(function (token) { unlink(token.id); }); whisper(msg, 'Marching orders cleared on current page.'); } // Called only when a token actually changes position. function handleMovement(obj, prev) { if (obj.get('layer') !== 'objects') { return; } var id = obj.id; var oldLeft = prev.left; var oldTop = prev.top; var newLeft = obj.get('left'); var newTop = obj.get('top'); if (oldLeft === newLeft && oldTop === newTop) { return; } // Ignore movement generated by this script. // Followers should not independently break // formation when the API moves them. var pending = pendingMoves[id]; if (pending && pending.left === newLeft && pending.top === newTop) { delete pendingMoves[id]; return; } delete pendingMoves[id]; var d = data(); // Manual movement of a follower detaches it // from the person ahead, but its own followers // remain attached. var aheadID = d.parent[id]; if (aheadID) { delete d.child[aheadID]; delete d.parent[id]; } var currentID = id; var targetLeft = oldLeft; var targetTop = oldTop; var visited = {}; visited[id] = true; // Each follower moves to the previous position // of the token immediately ahead. while (d.child[currentID]) { var nextID = d.child[currentID]; // Safety against corrupted circular links. if (visited[nextID]) { log('MarchingOrder: Circular link detected.'); break; } visited[nextID] = true; var follower = tokenByID(nextID); if (!follower) { delete d.child[currentID]; delete d.parent[nextID]; break; } var followerOldLeft = follower.get('left'); var followerOldTop = follower.get('top'); // Record the generated movement before // modifying the Roll20 token. if (followerOldLeft !== targetLeft || followerOldTop !== targetTop) { pendingMoves[nextID] = { left: targetLeft, top: targetTop }; follower.set({ left: targetLeft, top: targetTop }); } targetLeft = followerOldLeft; targetTop = followerOldTop; currentID = nextID; } } function handleChat(msg) { if (msg.type !== 'api') { return; } var content = msg.content.trim(); var match = content.match(/^!(\S+)(?:\s+([\s\S]*))?$/); if (!match) { return; } var command = match[1].toLowerCase(); var args = (match[2] || '').trim(); if (command === 'follow') { var pair = args.split(','); if (pair.length !== 2) { whisper(msg, 'Usage: !follow Follower, Leader' ); return; } follow(msg, pair[0].trim(), pair[1].trim()); return; } if (command === 'unfollow') { if (!args) { whisper(msg, 'Usage: !unfollow Token'); return; } unfollow(msg, args); return; } // Remaining commands are GM-only. if (!playerIsGM(msg.playerid)) { return; } if (command === 'march') { march(msg, args.split(',')); } if (command === 'unmarch') { unmarch(msg, args); } if (command === 'clearfollow') { clearFollow(msg); } } function handleDestroy(obj) { unlink(obj.id); delete pendingMoves[obj.id]; } on('ready', function () { data(); log('MarchingOrder Safe v' + VERSION + ' ready.'); }); on('chat:message', handleChat); on('change:graphic', handleMovement); on('destroy:graphic', handleDestroy); return { version: VERSION }; }()); ---
Commands remain unchanged Command Action !follow Nighthawk, Hyperion Nighthawk follows Hyperion !unfollow Nighthawk Nighthawk steps out of line !march Hyperion, Nighthawk, Whizzer Establishes the entire marching order !unmarch Hyperion Disbands Hyperion's line !clearFollow Clears marching orders on the current page