Skip to content

Instantly share code, notes, and snippets.

@jbrown123
Last active February 13, 2025 01:22
Show Gist options
  • Save jbrown123/052c6399a4ed0a0f5477a6124c841f41 to your computer and use it in GitHub Desktop.
Save jbrown123/052c6399a4ed0a0f5477a6124c841f41 to your computer and use it in GitHub Desktop.
RPTools Maptool - create tokens with multiple images

RPTools Maptool multi-image tokens

The process for generating a token with multiple images is somewhat complex, especially if you want a way to access and use those images (like changing your token image on the map). This could be useful for simple cases like having a unique token for each weapon your character wields. It's incredibly useful if you have a shapeshifter or other type of token that needs to be radically different depending on some circumstance.

Resources

I developed this by "standing on the backs of giants" which is to say, I did lots of looking around on the internet, in forums, and in the RPTools documentation. Here are some of the sources of information I gathered from:

Some of these sources have slightly inaccurate information (maybe outdated??) so don't be surprised if what you see here doesn't exactly match what is in the above links. For example, the spreadsheet linked from the RPTools wiki just creates random GUIDs and that doesn't work. It must be the MD5 checksum of the file.

What you'll need

To run this on your machine you'll need to have the following installed:

  • a copy of perl (Strawberry Perl for Windows works)
  • a copy of md5sum (my script expects output like that from the GNUWin32 version of md5sum which is 1 line per file with the checksum a space an asterisk and then the filename)
  • a zip file manipulation tool (e.g. 7-Zip, unzip, etc.)
  • the ability to run commands in a terminal / command prompt window

This process should work on any platform (Windows, Linux, Mac, etc.) that has those tools available. I developed and tested this on Windows so your mileage may vary. If you experience problems, please let me know.

Process

At it's core, the process isn't all that complicated, it's just tedious. That's exactly the sort of thing that computers are supposed to be good at, so "why not make the computer do it?" That's my thought process anyway.

Basically we want to add a collection of PNG files (you could use other types, but this tool only supports PNG because of transparency and that's super useful for tokens) to an existing token. You can change that by adjusting the $extension variable near the top of the file. I use an existing token because it's easier (and safer) than creating the entire token file from scratch.

Token files (extension .rptok) are just zip files that are renamed. That makes them easy to work with directly.

  1. Open Maptool and create a token using the base image you want for your token.
  2. Right click the token and select impersonate from the menu.
  3. In the chat window (select Windows | Chat if it's not visible) execute the following [setProperty("AttachedImageMap", json.set("{}", "##imageName", "##imageID"))]. This will create an extra property attached to the token even though it generates no output.
  4. Right click on the token and Save the token to a file.
  5. Open the token file (.rptok) with a zip tool. I use 7-Zip but anything that will open a zip file will work. Note that you might have to rename it from x.rptok to x.zip for your zip program to accept it.
  6. Extract the assets folder and the content.xml file. You can extract the rest if you want, but we wont be modifying anything else so there's really no need to extract them.
  7. Rename the original PNG file (it will be named a long string of numbers and letters) in the assets directory to whatever you want the default view to be named such as "Default.png" or "Generic.png", etc.
  8. Copy all the additional images (.png files) you want to use into the assets folder. Note that files named 'portrait' and 'handout' will be handled specially if they exist (see below).
  9. IMPORTANT NOTE make sure you name the image files whatever you want the button label to be for the macro that changes to that image (including upper or lower case). I typically label them after the weapon or position of the character. So, you might have "Crossbow.png", "Short Sword.png" or "Prone.png", etc.
  10. Open a command prompt / terminal window and navigate into the assets folder you extracted. Run the attached perl script IN THE ASSETS FOLDER to rename the files and make the neccessary edits in content.xml (which is expected to be in the folder above 'assets').
  11. Using your Zip tool, copy the assets folder and the (now updated) content.xml file back into the .rptok file. Do NOT copy the backup file, content-xml.bak
  12. Save the .rptok file
  13. Now drag the updated .rptok file onto a map in MapTool.
  14. In MapTool open Window | Selected to show the Selected token pane.
  15. Now click on the new token. A macro button for each of the tokens images should appear in the "Selected" pane (again, Windows | Selected to turn this on if it isn't already). Clicking on one of the named macro buttons should change the appearance of the token. You can also activate the macros by right-clicking on the token and looking under "Macros" in the token's pop-up menu.

I've recently enhanced the tool to recognize the special file names 'portrait.png' and 'handout.png'. If image files with these names exist they will be removed from the list of macros created and added to the token as the portrait and handout images respectively. You can include one, both, or neither and the tool will do the right thing.

The tool now supports appending macros to tokens with existing macros. It also supports replacing existing portrait and handout images.

Hopefully this helps make the process of creating multi-image tokens easier.

P.S. You can easily create multiple consistent top-down tokens for free using Hero Forge. Create your character with whatever clothes, weapons, pose, etc. Then, under "Stage" select the "Base" then pick the "Slot Base" (scroll to the bottom of the bases). Then rotate the image so it's a top-down view. Do a screen capture of the character (I use GreenShot). If you still have some of the slot base showing, it's easy to remove (along with the background) using online tools like this one Remove Background from Image for Free – remove.bg. Save a few of these with different weapons and/or poses and you'll have a great collection of images to use with the above tool to create a multi-view character token.

#!/usr/bin/env perl
#
# rename files and generate XML, JSON & macros to support multiple images for tokens
#
# Based on info from this page https://wiki.rptools.info/index.php/Adding_extra_image_files_to_tokens
#
our $VERSION = '1.13.20250212';
# == STEPS ==
# drag default image into maptool to create a token
# set name and make the token a "PC" token
#
# Impersonate token & execute the following code
# [setProperty("AttachedImageMap", json.set("{}", "##imageName", "##imageID"))]
#
# save token to .rptok file
# open with 7zip
#
# extract assets folder & content.xml
# rename existing image to 'Default' or 'Generic' or similar in assets folder
# copy images to assets folder
# image named 'portrait' will become the portrait
# image named 'handout' will become the handout
#
# run this tool IN THE ASSET FOLDER
# copy the assets and content.xml file back to the .rptok file via 7zip
# drag the token into maptool
# impersonate the token and run the following to see all the images
# [r, foreach(vImg, getProperty("AttachedImageMap"), ""): vImg+":<img src='asset://" + json.get(getProperty("AttachedImageMap"), vImg) + "' width=50>; "]
$extension = 'png';
$assetXMLTemplate=<<'EOS';
<net.rptools.maptool.model.Asset>
<id>
<id>{md5sum}</id>
</id>
<name>{name}</name>
<extension>{extension}</extension>
<type>IMAGE</type>
</net.rptools.maptool.model.Asset>
EOS
$assetEntryXMLTemplate=<<'EOS';
<entry>
<string>{name}</string>
<net.rptools.lib.MD5Key>
<id>{md5sum}</id>
</net.rptools.lib.MD5Key>
</entry>
EOS
$jsonTemplate='"{name}":"{md5sum}"';
$macroXMLTemplate=<<'EOS';
<entry>
<int>{index}</int>
<net.rptools.maptool.model.MacroButtonProperties>
<macroUUID>{uuid}</macroUUID>
<saveLocation>Token</saveLocation>
<index>{index}</index>
<colorKey>default</colorKey>
<hotKey>None</hotKey>
<command>[h: setTokenImage(json.get(getProperty(&quot;AttachedImageMap&quot;), &apos;{name}&apos;))]</command>
<label>{name}</label>
<group>Token</group>
<sortby></sortby>
<autoExecute>true</autoExecute>
<includeLabel>false</includeLabel>
<applyToTokens>false</applyToTokens>
<fontColorKey>default</fontColorKey>
<fontSize>1.00em</fontSize>
<minWidth></minWidth>
<maxWidth></maxWidth>
<allowPlayerEdits>true</allowPlayerEdits>
<toolTip></toolTip>
<displayHotKey>true</displayHotKey>
<commonMacro>false</commonMacro>
<compareGroup>true</compareGroup>
<compareSortPrefix>true</compareSortPrefix>
<compareCommand>true</compareCommand>
<compareIncludeLabel>true</compareIncludeLabel>
<compareAutoExecute>true</compareAutoExecute>
<compareApplyToSelectedTokens>true</compareApplyToSelectedTokens>
</net.rptools.maptool.model.MacroButtonProperties>
</entry>
EOS
$handoutXMLTemplate=<<'EOS';
<charsheetImage>
<id>{md5sum}</id>
</charsheetImage>
EOS
$portraitXMLTemplate=<<'EOS';
<portraitImage>
<id>{md5sum}</id>
</portraitImage>
EOS
my (@json, @entry, @macro);
my $index = 1;
foreach $_ (`md5sum *.$extension`)
{
s/[\r\n]//g;
if (/(\S+) \*(.+)\.$extension/i)
{
$md5sum = $1;
$name = $2;
# skip ones that are already done
next if ($name eq $md5sum);
print "renaming $name.$extension to $md5sum.$extension\n";
rename("$name.$extension", "$md5sum.$extension") || die "Can't rename $name.$extension to $md5sum.$extension: $!\n";
open(FH, '>', $md5sum) or die "Can't write $md5sum: $!\n";
print FH macroReplace($assetXMLTemplate, {name=>$name, extension=>$extension, md5sum=>$md5sum});
close(FH);
push(@entry, macroReplace($assetEntryXMLTemplate, {name=>$name, extension=>$extension, md5sum=>$md5sum} ));
if (lc($name) eq 'portrait')
{
$portraitXML = macroReplace($portraitXMLTemplate, {md5sum=>$md5sum});
}
elsif (lc($name) eq 'handout')
{
$handoutXML = macroReplace($handoutXMLTemplate, {md5sum=>$md5sum});
}
else
{
push(@json, macroReplace($jsonTemplate, {name=>$name, extension=>$extension, md5sum=>$md5sum} ));
push(@macro, macroReplace($macroXMLTemplate, {name=>$name, extension=>$extension, index=>$index, uuid=>genUUID()} ));
}
$index++;
}
else
{
die "Can't interpret md5sum output $_\n";
}
}
print "\nAsset entries for content.xml:\n@entry\n\n";
print "JSON string for content.xml: {" . join(',', @json) . "}\n\n";
print "MACRO entries for content.xml:\n <macroPropertiesMap>\n@macro </macroPropertiesMap>\n";
print "Portrait entry for content.xml:\n$portraitXML\n" if ($portraitXML ne "");
print "Handout entry for content.xml:\n$handoutXML\n" if ($handoutXML ne "");
# do the edits
print "\n\nUpdating ../content.xml\n";
rename('../content.xml', '../content-xml.bak') || die "Can't rename content.xml: $!\n";
open IN, '<../content-xml.bak' || die "Can't open content-xml.bak for read: $!\n";
open OUT, '>../content.xml' || die "Can't open content.xml for write: $!\n";
while(<IN>)
{
# paste asset entries before closing tag
if (m(</imageAssetMap>) )
{
print OUT "@entry";
$didAssets = 1;
}
# replace JSON entry
if (m({&quot;##imageName&quot;:&quot;##imageID&quot;}) )
{
my $json = '{' . join(',', @json) . '}';
s/{&quot;##imageName&quot;:&quot;##imageID&quot;}/$json/;
$didJSON = 1;
}
# insert macros (if there aren't any)
if (m(<macroPropertiesMap/>) )
{
print OUT " <macroPropertiesMap>\n@macro";
# adjust the closing tag
$_ = '</macroPropertiesMap>' . "\n";
$didMacros = 1;
}
# insert macros (if there are already macros)
if (m(</macroPropertiesMap>) )
{
print OUT "@macro";
$didMacros = 1;
}
# remove previous portrait if we have a new one
$skipPrint = 1 if (m(<portraitImage>) && $portraitXML ne "");
# insert portrait
if (m(<lightSourceList/?>) && $portraitXML ne "")
{
print OUT $portraitXML;
$didPortrait = 1;
}
# remove previous handout if we have a new one
$skipPrint = 1 if (m(<charsheetImage>) && $handoutXML ne "");
# insert handout
if (m(<lightSourceList/?>) && $handoutXML ne "")
{
print OUT $handoutXML;
$didHandout = 1;
}
print OUT $_ unless($skipPrint);
# stop skip on ending charsheetImage or portraitImage tag
$skipPrint = 0 if (m[</(charsheet|portrait)Image>]);
}
close OUT;
close IN;
$didAssets || print "** Didn't find assets!!\n";
$didJSON || print "** Didn't update JSON!!\n";
$didMacros || print "** Didn't add macros!!\n";
$didPortrait || print "** Didn't add portrait!!\n" if ($portraitXML ne "");
$didHandout || print "** Didn't add handout!!\n" if ($handoutXML ne "");
print "Update of ../content.xml complete. Backup stored in ../content-xml.bak\n";
#
# expects a template string and a hashref of names & values to replace
#
sub macroReplace
{
my ($temp, $hashRef) = @_;
for $key (keys %$hashRef)
{
my $val = $hashRef->{$key};
$temp =~ s/\{$key\}/$val/g;
}
die "\n\n*** Unreplaced token $1 in $temp\n" if ($temp =~ /(\{.+\})/);
return $temp;
}
#
# lifted from https://stackoverflow.com/questions/18628244/how-we-can-create-a-unique-id-in-perl
#
sub genUUID {
my $uuid;
my @set = ('a'..'f',0..9);
my $num = $#set;
$uuid .= $set[rand($num)] for 1..8;
$uuid .= '-';
for (1..3) {
$uuid .= $set[rand($num)] for 1..4;
$uuid .= '-';
}
$uuid .= $set[rand($num)] for 1..12;
return $uuid;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment