Skip to content

Commit 116a891

Browse files
authored
V1.04 timeout bugfix (Roll20#1093)
1 parent 54b1980 commit 116a891

File tree

2 files changed

+272
-2
lines changed

2 files changed

+272
-2
lines changed

DupCharToken/1.04/DupCharToken.js

+270
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,270 @@
1+
//
2+
// DupCharToken
3+
//
4+
// This script will duplicate a character sheet and token giving the new characters and tokens an identifying number and
5+
// linking each new token to it's own individual new character sheet.
6+
// This script is useful for those who want to create multiple copies of monsters because they prefer multiple character sheets to linking tokens as Mooks.
7+
// Using an argument of "clean" causes it to clean up (delete) numbered tokens and characters such as this script creates.
8+
//
9+
// Examples:
10+
// If you select a token named "Skel" that is linked to a character named "Skeleton", and in the chat window enter
11+
// !DupCharToken 5
12+
// It will make 5 new characters, identical to the original, named "Skeleton 1" through "Skeleton 5",
13+
// and make 5 new tokens, identical to the original and stacked in the exact same location, named "Skel 1" through "Skel 5".
14+
// Each token will be linked to its corresponding character sheet in the exact same manor as the originals were linked.
15+
//
16+
// If later on you select any token named "Skel" or "Skel (number)" and enter
17+
// !DupCharToken clean
18+
// It will delete all tokens where the name is "Skel (any number)" and all characters named "Skeleton (any number)".
19+
// The original token and character (without a number) will not be deleted.
20+
//
21+
//
22+
// Known bugs/issues:
23+
// 1 - If the avatar image of the character is not in a user library (i.e: it's a marketplace image or from the Monster Manual, or the like),
24+
// due to limits placed upon the API, it can't set an avatar image. You will have to do that yourself, or (better yet)
25+
// copy the image to your own library on roll20, and set the token to be copied to use the image in your library.
26+
// Two ways to copy the image to your own library on roll20 are as follows:
27+
// A) download an image and then upload it manually, either by downloading the set,
28+
// or placing a copy on the VTT, pressing "z" and right clicking the preview.
29+
// or
30+
// B) Right click the image in your marketplace collection (in the art library) and choosing "Copy to Library" or "Copy to Folder"
31+
// B is probably faster, unless you are working with a whole set.
32+
// After downloading the image to your own Roll20 library, make sure the Character and Token to be copied uses that image.
33+
// 2 - It takes a while to make copies, If making lots of copies of characters with lots of attributes, it might be several minutes between the
34+
// message that the copy has started, and the message that it has ended.
35+
// This is OK except that the character copy is not done until the ending msg comes.
36+
// 3 - Version 01.00 had unknown problems with the Roll20 script library, where the script could not be added or imported from the script library.
37+
// The script would work if it was gotten from GitHub and pasted into an API screen, but did not work from the script library.
38+
// It is hoped that version 1.01 will fix this, but if not, get it from the script library.
39+
//
40+
//
41+
// This script is based upon a script by "The Aaron".
42+
// https://app.roll20.net/forum/post/5687127/duplicate-character-sheet-plus-linked-token-script/?pageforid=5687127#post-5687127
43+
// It has been modified to it's current form by Chris Dickey (Roll20 user "Chris D").
44+
// Version 01.00 Dec 2018 Chris D. Original release.
45+
// Version 1.01 Nov 2019 Chris D. After all characters have been created, do loops to read attributes and abilities once and create for each new character.
46+
// Version 1.02 Jan 2020 Chris D. Fixed bug where script would crash API if clean was run with no tokens selected.
47+
// Version 1.03 Mar 2020 Chris D. Tokens dragged from the journal would not be correctly linked. Moved setDefaultTokenForCharacter to occur after attributes are copied.
48+
// Update 1.031 May 2020 Chris D. Minor bugfix with copying bio and gmnotes.
49+
// Update 1.04 Sep 2020 Chris D. Fixed the bug where it would timeout, killing api sandbox, if copying too many attributes.
50+
//
51+
//
52+
// Commands:
53+
// !DupCharToken (Number of tokens to create) (Starting number)
54+
// !DupCharToken clean
55+
// Number to create is the number of characters and tokens to copy/create. It defaults to 1.
56+
// Starting number is the number to start numbering the tokens and characters from. Defaults to 1.
57+
// So !dupCharacter 5 3 would start numbering at 3 ending at number 7.
58+
//
59+
// You can add the following text to a macro
60+
// !dupCharToken ?{How many Duplicates|1} ?{Starting Number|1}
61+
62+
63+
on('ready',()=>{
64+
'use strict';
65+
66+
on('chat:message',(msg)=>{
67+
'use strict';
68+
69+
if( 'api' === msg.type && /^!dupCharToken\b/i.test( msg.content ) && playerIsGM( msg.playerid )) {
70+
try {
71+
let start = new Date().getTime();
72+
function sChat( txt ) {
73+
sendChat('DupCharToken', '/w ' + msg.who.replace(" (GM)","") + ' <div style="color: #993333;font-weight:bold;">' + txt + '</div>', null, {noarchive:true});
74+
}
75+
76+
if( /\bclean\b/i.test( msg.content ) || /\bdelete\b/i.test( msg.content )) { // We are not duplicating, we are cleaning up characters and tokens that we created previously.
77+
let tnames = [],
78+
cnames = [];
79+
if( !msg.selected || msg.selected.length == 0 )
80+
sChat( "Please select exactly one token representing the character to duplicate. Script usage is !DupCharToken (Number of tokens to create) (Starting number) or !DupCharToken clean");
81+
else {
82+
_.each( msg.selected, function( sel ) { // for each token selected, find base token name and base character name. add unique ones to list.
83+
let tokenObj = getObj('graphic', sel._id);
84+
if ( _.isUndefined( tokenObj ) )
85+
sChat( "Token is invalid in some way." );
86+
else {
87+
let tn = tokenObj.get( "name" ).replace( /\s+\d+\s*$/, ""); // one or more whitespaces, one or more digits, zero or more whitespaces, then the end of line get trimmed off.
88+
if( tnames.indexOf( tn ) == -1 )
89+
tnames.push( tn );
90+
let charObj = getObj('character', tokenObj.get('represents'));
91+
if ( _.isUndefined( charObj ) )
92+
sChat( "Token is not correctly linked to a good character." );
93+
else {
94+
let cn = charObj.get( "name" ).replace( /\s+\d+\s*$/, ""); // one or more whitespaces, one or more digits, zero or more whitespaces, then the end of line get trimmed off.
95+
if( cnames.indexOf( cn ) == -1 )
96+
cnames.push( cn );
97+
}
98+
}
99+
}); // end each selected token
100+
101+
function getRegEx( arr ) { // skel, ork.
102+
let re = "^(";
103+
arr.forEach( function(nm ) { re += nm + "|"; });
104+
return new RegExp ( re.slice( 0, -1) + ")\\s\\d+\\s*$" );
105+
};
106+
if( tnames.length ) { // search for token names. for each found token, delete.
107+
let reg = getRegEx( tnames ),
108+
d = [];
109+
_.each(findObjs({type:"graphic", _subtype: "token" }),( tObj )=>{
110+
if( reg.test( tObj.get( 'name' ) ) ) {
111+
d.push( tObj.get( 'name' ) );
112+
tObj.remove();
113+
}
114+
});
115+
sChat( "Deleted tokens: " + d.join() + "." );
116+
}
117+
118+
if( cnames.length ) { // search for character names, for each, delete.
119+
let reg = getRegEx( cnames ),
120+
d = [];
121+
_.each(findObjs({type:"character" }),( cObj )=>{
122+
if( reg.test( cObj.get( 'name' ) ) ) {
123+
d.push( cObj.get( 'name' ) );
124+
cObj.remove();
125+
}
126+
});
127+
sChat( "Deleted characters: " + d.join() + "." );
128+
}
129+
}
130+
// End clean
131+
} else if( !msg.selected || msg.selected.length !== 1 )
132+
sChat( "Please select exactly one token representing the character to duplicate. Script usage is !DupCharToken (Number of tokens to create) (Starting number) or !DupCharToken clean");
133+
else { // Not cleaning up, presumably duplicating.
134+
let num = 1,
135+
startnum = 1;
136+
let cl = msg.content.split( /\s+/ );
137+
if( cl.length > 1 ) {
138+
if( parseInt( cl[ 1 ] ) )
139+
num = parseInt( cl[ 1 ] );
140+
else {
141+
sChat( "Script usage is !DupCharToken (Number of tokens to create) (Starting number) or !DupCharToken clean");
142+
return;
143+
}
144+
if( cl.length > 2 && parseInt( cl[ 2 ] ) )
145+
startnum = parseInt( cl[ 2 ] );
146+
}
147+
148+
let tokenObj = getObj('graphic', msg.selected[ 0 ]._id);
149+
if ( _.isUndefined( tokenObj ) )
150+
sChat( "Token is invalid in some way." );
151+
else { // Good command line.
152+
let charArray = [],
153+
tokenArray = [],
154+
links = [];
155+
156+
function getLink( n, lnk ) {
157+
let l = tokenObj.get( lnk );
158+
if( l && l != "" ) {
159+
let a = getObj("attribute", l);
160+
if( a && a.get( "name" ) != "" )
161+
links [ n ] = a.get( "name" );
162+
}
163+
}
164+
getLink( 0, "bar1_link" );
165+
getLink( 1, "bar2_link" );
166+
getLink( 2, "bar3_link" );
167+
168+
let charObj = getObj('character', tokenObj.get('represents'));
169+
if ( _.isUndefined( charObj ) )
170+
sChat( "Token is not correctly linked to a good character." );
171+
else { // Everything is good. We are duplicating.
172+
let oldCid = charObj.id;
173+
let tmpC = JSON.stringify( charObj ).replace ( /\,"bio"\:.*?\,/gi, ',"bio":"",').replace ( /\,"gmnotes"\:.*?\,/gi, ',"gmnotes":"",'),
174+
tmpT = JSON.stringify( tokenObj );
175+
176+
for( let i = startnum; i < (startnum + num); ++i) { // We are going to want to duplicate both the character and token this many times.
177+
let newC = JSON.parse( tmpC ); // Simple true copy of object.
178+
delete newC._id;
179+
newC.name= charObj.get( 'name' ) + " " + i;
180+
181+
let parts = newC.avatar.match(/(.*\/images\/.*)(thumb|med|original|max)([^?]*)(\?[^?]+)?$/);
182+
if(parts)
183+
newC.avatar = parts[1]+'thumb'+parts[3]+(parts[4]?parts[4]:`?${Math.round(Math.random()*9999999)}`);
184+
else
185+
newC.avatar = "";
186+
187+
let newT = JSON.parse( tmpT );
188+
delete newT._id;
189+
newT.name = tokenObj.get( 'name' ) + " " + i;
190+
parts = newT.imgsrc.match(/(.*\/images\/.*)(thumb|med|original|max)([^?]*)(\?[^?]+)?$/);
191+
if(parts)
192+
newT.imgsrc = parts[1]+'thumb'+parts[3]+(parts[4]?parts[4]:`?${Math.round(Math.random()*9999999)}`);
193+
else
194+
newT.imgsrc = "";
195+
196+
let newCObj = createObj('character', newC);
197+
let newTObj = createObj('graphic', newT);
198+
newTObj.set('represents', newCObj.id);
199+
charArray.push( newCObj );
200+
tokenArray.push( newTObj );
201+
} // End For each character to create.
202+
203+
let attQueue = findObjs({_type:'attribute', _characterid:oldCid}); // create the queue we'll be processing asynchronously
204+
const attBurndown = () => { // create the function that will process the next element of the queue
205+
if(attQueue.length){
206+
let a = attQueue.shift();
207+
let sa = JSON.parse(JSON.stringify(a));
208+
delete sa._id;
209+
delete sa._type;
210+
for( let i = 0; i < charArray.length; ++i ) {
211+
delete sa._characterid;
212+
sa._characterid = charArray[ i ].id;
213+
if( links.indexOf( sa.name ) > -1 ) {
214+
let newA = createObj('attribute', sa);
215+
tokenArray[ i ].set( "bar" + (links.indexOf( sa.name ) + 1).toString() + "_link", newA.id); // Link the new token to the new attribute.
216+
} else
217+
createObj('attribute', sa);
218+
};
219+
setTimeout(attBurndown,0);
220+
} else // Have finished the last attribute.
221+
sChat( "Done duplicating: " + charObj.get( "name" ) + " " + num + " times. It took "
222+
+ Math.round(((new Date().getTime()) - start) / 1000) + " seconds.");
223+
};
224+
// start the execution by doing the first element
225+
attBurndown();
226+
227+
let abQueue = findObjs({_type:'ability', _characterid:oldCid}); // create the queue we'll be processing asynchronously
228+
const abBurndown = () => { // create the function that will process the next element of the queue
229+
if(abQueue.length){
230+
let a = abQueue.shift();
231+
let sa = JSON.parse(JSON.stringify(a));
232+
delete sa._id;
233+
delete sa._type;
234+
for( let i = 0; i < charArray.length; ++i ) {
235+
delete sa._characterid;
236+
sa._characterid = charArray[ i ].id;
237+
createObj('ability', sa);
238+
};
239+
setTimeout(abBurndown,0);
240+
}
241+
};
242+
abBurndown(); // start the execution by doing the first element
243+
244+
for( let i = 0; i < charArray.length; ++i ) {
245+
setDefaultTokenForCharacter( charArray[ i ], tokenArray[ i ]);
246+
toFront( tokenArray[ i ] );
247+
}
248+
249+
charObj.get("bio", function(bio) {
250+
_.each( charArray, c => {
251+
if( !_.isNull( bio ) && !_.isUndefined( bio) && bio != "null" && bio != "undefined" && (typeof bio === 'string' && bio.trim() !== "") )
252+
c.set( 'bio', bio );
253+
}); });
254+
charObj.get("gmnotes", function(gmnotes) {
255+
_.each( charArray, c => {
256+
if( !_.isNull( gmnotes ) && !_.isUndefined( gmnotes) && gmnotes != "null" && gmnotes != "undefined" && (typeof gmnotes === 'string' && gmnotes.trim() !== "") )
257+
c.set( 'gmnotes', gmnotes );
258+
}); });
259+
sChat( "Working on duplicating: " + charObj.get( "name" ) + " " + num + " times." );
260+
}
261+
} // End tokenObj is OK.
262+
} // End have one token selected.
263+
} catch(err) {
264+
log( msg);
265+
log( "DupCharToken.js error caught: " + err );
266+
}
267+
} // End msg if for this script.
268+
}); // End on Chat.
269+
// End on Ready.
270+
});

DupCharToken/script.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
{
22
"name": "Duplicate a Character and Token",
33
"script": "DupCharToken.js",
4-
"version": "1.03",
5-
"previousversions": ["1.0", "1.01", "1.02"],
4+
"version": "1.04",
5+
"previousversions": ["1.0", "1.01", "1.02", "1.03"],
66
"description": "This script will duplicate a character sheet and tokens giving the new characters and tokens an identifying number and linking each new token to it's own individual new character sheet. This script is useful for those who want to create multiple copies of monsters because they prefer multiple character sheets to linking tokens as Mooks. Using an argument of 'clean' causes it to clean up (delete) numbered tokens and characters such as it creates.\r\r## Commands:\r\r```!DupCharToken <Number of tokens to create (defaults to 1)> <Start numbering from (defaults to 1)>```\r\r```!DupCharToken clean```\r\rYou can add the following text to a macro\r```!DupCharToken ?{How many Duplicates|1} ?{Starting Number|1}```\r\r## Examples:\r\rIf you select a token named 'Skel' that is linked to a character named 'Skeleton', and in the chat window enter```!DupCharToken 5```\r\rIt will make 5 new characters, identical to the original, named 'Skeleton 1' through 'Skeleton 5', and make 5 new tokens, identical to the original and stacked in the exact same location, named 'Skel 1' through 'Skel 5'. Each token will be linked to its corresponding character sheet in the exact same manor as the originals were linked.\r\rIf later on you select any token named 'Skel' or 'Skel (number)' and enter```!DupCharToken clean```\rIt will delete all tokens where the name is 'Skel (any number)' and all characters named 'Skeleton (any number)'. \rThe original token and character (without a number) will not be deleted.\r\r\r## Known bugs/issues:\r If the avatar image of the character is not in a user library (i.e: it's a marketplace image or from the Monster Manual, or the like), due to limits placed upon the API, it can't set an avatar image. You will have to do that yourself (or copy the image to the library).\r\r For more information please see [CharDupToken script wiki page](https://wiki.roll20.net/Script:DupCharToken)",
77
"authors": "Chris Dickey",
88
"roll20userid": "633707",

0 commit comments

Comments
 (0)