Roll20 uses cookies to improve your experience on our site. Cookies enable you to enjoy certain features, social sharing functionality, and tailor message and display ads to your interests on our site and others. They also help us understand how our site is being used. By continuing to use our site, you consent to our use of cookies. Update your cookie preferences .
×

[Scriptlet] Duplicate Finder

1775758486

Edited 1775771495
keithcurtis
Forum Champion
Marketplace Creator
API Scripter
Just a little utility scriptlet. Macros and scripts can sometimes be confused or give unexpected results when there are duplicate items&nbsp; in the journal tab with the same name. These can be hard to find, especially if some are buried several folders layers deep, or archived. This scriptlet scans the characters and handouts in a game and provides a list of clickable links for allduplicates, so you can review which ones to delete. (No need to hunt them down, you can just edit the open journal and delete from the edit screen). Command is: !findDuplicates Sample output: on('ready', () =&gt; { log('Duplicate Name Finder script loaded.'); const CSS = { container: "background:#1f1f1f;border:2px solid #444;border-radius:8px;padding:10px;color:#eee;font-size:13px;", header: "font-size:16px;font-weight:bold;margin-bottom:8px;color:#fff;", section: "margin-top:8px;", sectionTitle: "font-weight:bold;color:#ffd700;margin-bottom:4px;", item: "margin-left:10px;margin-bottom:4px;", link: "display:inline-block;background:#2d6cdf;color:#fff;padding:2px 6px;margin:2px 2px 0 0;border-radius:4px;text-decoration:none;font-size:11px;font-weight:bold;border:1px solid #1f4ea3;", linkArchived: "display:inline-block;background:#555;color:#ccc;padding:2px 6px;margin:2px 2px 0 0;border-radius:4px;text-decoration:none;font-size:11px;font-weight:bold;border:1px solid #333;", count: "color:#aaa;", none: "color:#8fbc8f;font-weight:bold;" }; const makeLink = (type, id, name) =&gt; { const obj = getObj(type, id); const isArchived = obj &amp;&amp; obj.get('archived'); const style = isArchived ? CSS.linkArchived : CSS.link; const label = isArchived ? `${name} &lt;i&gt;(Archived)&lt;/i&gt;` : name; return `&lt;a href="<a href="http://journal.roll20.net/${type}/${id}" rel="nofollow">http://journal.roll20.net/${type}/${id}</a>" style="${style}" title="${isArchived ? 'Archived' : 'Open'}" onmouseover="this.style.background='${isArchived ? '#444' : '#1f4ea3'}'" onmouseout="this.style.background='${isArchived ? '#555' : '#2d6cdf'}'"&gt; ${label} &lt;/a&gt;`.replace(/\r\n|\r|\n/g, "").trim(); }; on('chat:message', (msg) =&gt; { if (msg.type === 'api' &amp;&amp; msg.content.startsWith('!findDuplicates')) { let duplicates = { characters: {}, handouts: {} }; const findDuplicates = (objects, type) =&gt; { let nameCounts = {}; objects.forEach(obj =&gt; { let name = obj.get('name'); if (!name) return; if (!nameCounts[name]) nameCounts[name] = []; nameCounts[name].push(obj.id); }); Object.keys(nameCounts).forEach(name =&gt; { if (nameCounts[name].length &gt; 1) { duplicates[type][name] = nameCounts[name]; } }); }; findDuplicates(findObjs({ type: 'character' }), 'characters'); findDuplicates(findObjs({ type: 'handout' }), 'handouts'); let html = `&lt;div style="${CSS.container}"&gt;`; html += `&lt;div style="${CSS.header}"&gt;Duplicate Name Finder&lt;/div&gt;`; let hasDuplicates = false; // Characters if (Object.keys(duplicates.characters).length &gt; 0) { html += `&lt;div style="${CSS.section}"&gt;`; html += `&lt;div style="${CSS.sectionTitle}"&gt;Character Sheets&lt;/div&gt;`; Object.entries(duplicates.characters).forEach(([name, ids]) =&gt; { html += `&lt;div style="${CSS.item}"&gt;• ${name} &lt;span style="${CSS.count}"&gt;(x${ids.length})&lt;/span&gt;&lt;br&gt;`; ids.forEach((id, i) =&gt; { html += `${makeLink('character', id, `Copy ${i+1}`)} `; }); html += `&lt;/div&gt;`; }); html += `&lt;/div&gt;`; hasDuplicates = true; } // Handouts if (Object.keys(duplicates.handouts).length &gt; 0) { html += `&lt;div style="${CSS.section}"&gt;`; html += `&lt;div style="${CSS.sectionTitle}"&gt;Handouts&lt;/div&gt;`; Object.entries(duplicates.handouts).forEach(([name, ids]) =&gt; { html += `&lt;div style="${CSS.item}"&gt;• ${name} &lt;span style="${CSS.count}"&gt;(x${ids.length})&lt;/span&gt;&lt;br&gt;`; ids.forEach((id, i) =&gt; { html += `${makeLink('handout', id, `Copy ${i+1}`)} `; }); html += `&lt;/div&gt;`; }); html += `&lt;/div&gt;`; hasDuplicates = true; } if (!hasDuplicates) { html += `&lt;div style="${CSS.none}"&gt;No duplicates found!&lt;/div&gt;`; } html += `&lt;/div&gt;`; sendChat('System', `/w gm ${html}`); } }); });
Hi Keith. This keeps crashing on me
1775771581

Edited 1775805704
keithcurtis
Forum Champion
Marketplace Creator
API Scripter
&lt;Head-slap&gt; Code is fixed. Longer explanation. I have a script called "Short Utilities" in my campaign. It's just a collection of short scripts and a chat menu interface. I did not grab the entirety of the script. I tested this on its own, and it should work now.
works now!! Thanks Keith!