Ok, here's an enhanced version that has several options for fixing duplicate attributes: First off, I added the --fix-equal argument, which will automatically get rid of all duplicates if they all have the same current and max value: !find-dup-attrs --fix-equal It will retain the first one listed (which is very likely the oldest one). You can additionally specify one of --force-lower-case or --force-upper-case: !find-dup-attrs --fix-equal --force-lower-case !find-dup-attrs --fix-equal --force-upper-case And it will additionally change the case of the retained attribute to be either lower case or upper case as specified (if you specify both, it will lowercase). For fixing duplicates when they values vary, I added several buttons: It will not ask you for confirmation, it will just do what you tell it to, so be sure you mean it. It will give you output to tell you what it did: Hope that helps fix things! Code: on('ready',()=>{
const s = {
outer: 'margin: .1em;margin-top:.5em; border: 1px solid #999; padding: .1em; border-radius: .1em; background-color: #eee;',
outerLabel: 'color:#ff0000;font-weight:bold;text-align:center;background-color:#ffff00;',
char: 'font-weight: bold; font-size: 1.5em; border-bottom: 2px solid #933;',
id: 'font-weight: bold; font-size: .8em;',
val: 'display:inline-block; padding: .1em .2em; max-width: 13em; max-height:1.2em; text-overflow: ellipsis; white-space: nowrap; overflow: hidden; background-color: #eec; border: 1px solid #333;',
attr: 'padding-left: 3em; font-size: .8em;',
key: 'padding-left: 1.5em;',
dup: 'border-bottom: 1px dashed #666;margin-bottom: .1em;',
btn: 'border: 1px solid #ccc; padding: .25em; border-radius: 50%; background-color: #669966;min-width:1.5em;min-height:1.5em;text-align:center; font-weight:bold;',
btn_L: 'background-color: #5bc0de; border-color: #45b8da;',
btn_U: 'background-color: #5cb85c; border-color: #4cae4c;',
btn_O: 'background-color: #f0ad4e; border-color: #eea236;',
btn_X: 'background-color: #d9534f; border-color: #d43f3a;'
};
const stripHTML = (t) => t.replace(/<br\/?>/gi,'\n').replace(/<\/div>/gi,'\n').replace(/<[^>]*?>/g,'');
const HE = (() => {
const esRE = (s) => s.replace(/(\\|\/|\[|\]|\(|\)|\{|\}|\?|\+|\*|\||\.|\^|\$)/g,'\\$1');
const e = (s) => `&${s};`;
const entities = {
'<' : e('lt'),
'>' : e('gt'),
"'" : e('#39'),
'@' : e('#64'),
'{' : e('#123'),
'|' : e('#124'),
'}' : e('#125'),
'[' : e('#91'),
']' : e('#93'),
'"' : e('quot')
};
const re = new RegExp(`(${Object.keys(entities).map(esRE).join('|')})`,'g');
return (s) => s.replace(re, (c) => (entities[c] || c) );
})();
const f = {
outer: (o)=>`<div style='${s.outer}'>${o}</div>`,
outerLabel: (l,o)=>f.outer(`<div style='${s.outerLabel}'>${l}</div> ${o}`),
char: (c)=>`<div style='${s.char}'>${c.get('name')}</div>`,
val: (v)=>`<span style='${s.val}'>${HE(stripHTML(`${v}`).replace(/\r\n|\r|\n/g,' '))||'&nbsp;'}</span>`,
id: (id)=>`<span style='${s.id}'> ${f.lowerBtn(id)}${f.upperBtn(id)}${f.onlyBtn(id)}${f.deleteBtn(id)} [<code>${id}</code>]</span>`,
attr: (a)=>`<div style='${s.attr}'><b>${a.get('name')}</b> ${f.id(a.id)} ${f.val(a.get('current'))} <b>/</b> ${f.val(a.get('max'))}</div>`,
key: (k)=>`<div style='${s.key}'>${k}</div>`,
dup: (k,as)=>`<div style='${s.dup}'>${f.key(k)}${as.map(f.attr).join('')}</div>`,
dups: (das)=>Object.keys(das).map((k)=>f.dup(k,das[k])).join(''),
btn: (l,i)=>`<a style='${s.btn}${s[`btn_${l}`]}' href='${i}'>${l}</a>`,
lowerBtn: (id)=>f.btn('L',`!lower-case-attr-by-id ${id}`),
upperBtn: (id)=>f.btn('U',`!upper-case-attr-by-id ${id}`),
onlyBtn: (id)=>f.btn('O',`!only-this-attr-by-id ${id}`),
deleteBtn: (id)=>f.btn('X',`!delete-attr-by-id ${id}`)
};
on('chat:message', (msg)=>{
if('api'===msg.type && playerIsGM(msg.playerid) ) {
if(/!find-dup-attrs\b/i.test(msg.content)){
let args = msg.content.split(/\s+/).map(s=>s.toLowerCase());
let fixEqual = args.includes('--fix-equal');
let forceLowerCase = args.includes('--force-lower-case');
let forceUpperCase = args.includes('--force-upper-case');
let CharAttrMap = findObjs({
type:'attribute'
}).reduce((m,a)=>{
let cid = a.get('characterid');
let key = a.get('name').toLowerCase();
m[cid]=m[cid]||{};
m[cid][key]=m[cid][key]||{};
m[cid][key][a.id]=a;
return m;
},{});
let DupCharAttrMap = Object.keys(CharAttrMap).reduce( (m,cid)=>{
Object.keys(CharAttrMap[cid]).forEach(key=>{
if(Object.keys(CharAttrMap[cid][key]).length>1){
m[cid]=m[cid]||{};
m[cid][key]=Object.values(CharAttrMap[cid][key]);
}
});
return m;
},{});
let fixed = {};
if(fixEqual){
Object.keys(DupCharAttrMap).forEach(cid=>{
Object.keys(DupCharAttrMap[cid]).forEach(key=>{
let same = true;
let current = DupCharAttrMap[cid][key][0].get('current');
let max = DupCharAttrMap[cid][key][0].get('max');
DupCharAttrMap[cid][key].slice(1).forEach(a=>{
same = same && (current === a.get('current'));
same = same && (max === a.get('max'));
});
if(same){
DupCharAttrMap[cid][key].slice(1).forEach(a=>a.remove());
if(forceLowerCase){
DupCharAttrMap[cid][key][0].set({
name: DupCharAttrMap[cid][key][0].get('name').toLowerCase()
});
} else if( forceUpperCase){
DupCharAttrMap[cid][key][0].set({
name: DupCharAttrMap[cid][key][0].get('name').toUpperCase()
});
}
fixed[cid]=fixed[cid]||{};
fixed[cid][key]=[DupCharAttrMap[cid][key][0]];
delete DupCharAttrMap[cid][key];
}
});
});
}
if(Object.keys(DupCharAttrMap).length){
let output=[];
Object.keys(DupCharAttrMap).forEach( cid =>{
let c = getObj('character',cid);
if(c){
output.push(f.outer(`${f.char(c)}${f.dups(DupCharAttrMap[cid])}`));
}
});
sendChat('',`/w gm ${output.join('')}`);
} else {
sendChat('',`/w gm No duplicate attributes found.`);
}
if(Object.keys(fixed).length){
let output=[];
Object.keys(fixed).forEach( cid =>{
let c = getObj('character',cid);
if(c){
output.push(f.outerLabel('Fixed',`${f.char(c)}${f.dups(fixed[cid])}`));
}
});
sendChat('',`/w gm ${output.join('')}`);
}
}
if(/!lower-case-attr-by-id/i.test(msg.content)){
let args = msg.content.split(/\s+/);
let a = getObj('attribute', args[1]);
if(a){
a.set({
name: a.get('name').toLowerCase()
});
let c = getObj('character',a.get('characterid'));
if(c){
sendChat('',`/w gm ${ f.outerLabel('Lowercased',`${f.char(c)}${f.dup(a.get('name'),[a])}`) }`);
}
}
}
if(/!upper-case-attr-by-id/i.test(msg.content)){
let args = msg.content.split(/\s+/);
let a = getObj('attribute', args[1]);
if(a){
a.set({
name: a.get('name').toUpperCase()
});
let c = getObj('character',a.get('characterid'));
if(c){
sendChat('',`/w gm ${ f.outerLabel('Uppercased',`${f.char(c)}${f.dup(a.get('name').toLowerCase(),[a])}`) }`);
}
}
}
if(/!delete-attr-by-id/i.test(msg.content)){
let args = msg.content.split(/\s+/);
let a = getObj('attribute', args[1]);
if(a){
let c = getObj('character',a.get('characterid'));
if(c){
sendChat('',`/w gm ${ f.outerLabel('Deleted',`${f.char(c)}${f.dup(a.get('name').toLowerCase(),[a])}`) }`);
a.remove();
}
}
}
if(/!only-this-attr-by-id/i.test(msg.content)){
let args = msg.content.split(/\s+/);
let a = getObj('attribute', args[1]);
if(a){
let c = getObj('character',a.get('characterid'));
if(c){
let key = a.get('name').toLowerCase();
let attrs = findObjs({
type: 'attribute',
characterid: c.id
}).filter(a2=>a2.get('name').toLowerCase()===key && a2.id !==a.id);
sendChat('',`/w gm ${ f.outerLabel(`Only This Attr Saved, ${attrs.length} Deleted`,`${f.char(c)}${f.dup(a.get('name').toLowerCase(),[a])}`) }`);
attrs.forEach(a2=>a2.remove());
}
}
}
}
});
});