From 189cc6671f7807f67fb563a8ce364a6598ccf3d8 Mon Sep 17 00:00:00 2001 From: Kenneth VanderLinde Date: Sat, 18 Oct 2025 01:58:50 -0700 Subject: [PATCH 1/2] Use Token#getTokenImageAssetId() as source of truth for image tables `ImageUtil#getTokenImage(Token, ImageObserver[])` is redundant and has been removed. Any callers of this method can instead just call `Token#getTokenImageAssetId()` followed by `ImageManager.getImage(MD5Key, ImageObserver[])`. The `TokenRenderer` no longer tries to cache token images per facing, and instead uses the above technique to get the image each time. This cache wasn't really functional since entries could never be ejected or updated. Being static, this also meant it was a memory leak as all the images ever cached would be kept in `TokenRenderer#imageTableMap`. --- .../java/net/rptools/lib/image/ImageUtil.java | 36 ------- .../client/ui/zone/renderer/ZoneRenderer.java | 4 +- .../renderer/tokenRender/TokenRenderer.java | 97 ++----------------- .../java/net/rptools/maptool/model/Token.java | 11 ++- 4 files changed, 20 insertions(+), 128 deletions(-) diff --git a/src/main/java/net/rptools/lib/image/ImageUtil.java b/src/main/java/net/rptools/lib/image/ImageUtil.java index 514ea73059..0e87cd1e91 100644 --- a/src/main/java/net/rptools/lib/image/ImageUtil.java +++ b/src/main/java/net/rptools/lib/image/ImageUtil.java @@ -30,10 +30,7 @@ import net.rptools.lib.MathUtil; import net.rptools.maptool.client.AppConstants; import net.rptools.maptool.client.AppPreferences; -import net.rptools.maptool.client.MapTool; import net.rptools.maptool.model.*; -import net.rptools.maptool.util.ImageManager; -import net.rptools.parser.ParserException; import org.apache.commons.io.FileUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -521,39 +518,6 @@ public static double getIsoFigureScaleFactor(Token token, Rectangle2D footprintB footprintBounds.getHeight() * 2 / token.getHeight()); } - /** - * Checks to see if token has an image table and references that if the token has a facing - * otherwise uses basic image - * - * @param token the token to get the image from. - * @return BufferedImage - */ - public static BufferedImage getTokenImage(Token token, ImageObserver... observers) { - BufferedImage image = null; - // Get the basic image - if (token.getHasImageTable() && token.hasFacing() && token.getImageTableName() != null) { - LookupTable lookupTable = - MapTool.getCampaign().getLookupTableMap().get(token.getImageTableName()); - if (lookupTable != null) { - try { - LookupTable.LookupEntry result = - lookupTable.getLookup(Integer.toString(token.getFacing())); - if (result != null) { - image = ImageManager.getImage(result.getImageId(), observers); - } - } catch (ParserException p) { - // do nothing - } - } - } - - if (image == null) { - // Adds zr as observer so we can repaint once the image is ready. Fixes #1700. - image = ImageManager.getImage(token.getImageAssetId(), observers); - } - return image; - } - public static BufferedImage flipIsometric(BufferedImage image, boolean toRhombus) { BufferedImage workImage; boolean isSquished = diff --git a/src/main/java/net/rptools/maptool/client/ui/zone/renderer/ZoneRenderer.java b/src/main/java/net/rptools/maptool/client/ui/zone/renderer/ZoneRenderer.java index c48e311438..56f10b5c05 100644 --- a/src/main/java/net/rptools/maptool/client/ui/zone/renderer/ZoneRenderer.java +++ b/src/main/java/net/rptools/maptool/client/ui/zone/renderer/ZoneRenderer.java @@ -38,7 +38,6 @@ import javax.swing.*; import net.rptools.lib.CodeTimer; import net.rptools.lib.MD5Key; -import net.rptools.lib.image.ImageUtil; import net.rptools.maptool.client.*; import net.rptools.maptool.client.functions.TokenMoveFunctions; import net.rptools.maptool.client.swing.ImageLabel; @@ -1794,7 +1793,8 @@ protected void renderTokens( timer.start("token-list-1b"); // get token image, using image table if present - BufferedImage image = ImageUtil.getTokenImage(token, this); + MD5Key tokenImageId = token.getTokenImageAssetId(); + BufferedImage image = ImageManager.getImage(tokenImageId, this); timer.stop("token-list-1b"); timer.start("token-list-5a"); diff --git a/src/main/java/net/rptools/maptool/client/ui/zone/renderer/tokenRender/TokenRenderer.java b/src/main/java/net/rptools/maptool/client/ui/zone/renderer/tokenRender/TokenRenderer.java index f1497a8a98..44c03ecaaf 100644 --- a/src/main/java/net/rptools/maptool/client/ui/zone/renderer/tokenRender/TokenRenderer.java +++ b/src/main/java/net/rptools/maptool/client/ui/zone/renderer/tokenRender/TokenRenderer.java @@ -15,17 +15,9 @@ package net.rptools.maptool.client.ui.zone.renderer.tokenRender; import java.awt.*; -import java.awt.geom.*; import java.awt.image.BufferedImage; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import javax.swing.*; import net.rptools.lib.CodeTimer; import net.rptools.lib.MD5Key; -import net.rptools.lib.image.ImageUtil; -import net.rptools.maptool.client.MapTool; import net.rptools.maptool.client.ui.zone.ZoneViewModel.TokenPosition; import net.rptools.maptool.client.ui.zone.renderer.RenderHelper; import net.rptools.maptool.model.*; @@ -36,8 +28,6 @@ public class TokenRenderer { private static final Logger log = LogManager.getLogger(TokenRenderer.class); - private static final Map> imageTableMap = - Collections.synchronizedMap(new HashMap<>()); private final RenderHelper renderHelper; private final Zone zone; @@ -51,50 +41,27 @@ public void renderToken(Token token, TokenPosition position, Graphics2D g2d, flo var timer = CodeTimer.get(); timer.increment("TokenRenderer-renderToken"); timer.start("TokenRenderer-renderToken"); - - timer.start("TokenRenderer-loadImageTable"); - if (token.getHasImageTable() && !imageTableMap.containsKey(token.getImageTableName())) { - (new CacheTableImagesWorker(token.getImageTableName())).execute(); - } - timer.stop("TokenRenderer-loadImageTable"); - - timer.start("TokenRenderer-paintTokenImage"); renderHelper.render( g2d, worldG -> paintTokenImage(worldG, position, extraOpacity * token.getTokenOpacity())); - timer.stop("TokenRenderer-paintTokenImage"); timer.stop("TokenRenderer-renderToken"); } + /** + * Checks to see if token has an image table and references that if the token has a facing + * otherwise uses basic image + * + * @param token the token to get the image from. + * @return The token's current image based on its facing. + */ private BufferedImage getRenderImage(Token token) { - var timer = CodeTimer.get(); - timer.start("TokenRenderer-getRenderImage"); - BufferedImage bi = ImageManager.BROKEN_IMAGE; - if (token.getHasImageTable() && imageTableMap.containsKey(token.getImageTableName())) { - Map imageTable = imageTableMap.get(token.getImageTableName()); - int max = imageTable.keySet().stream().max(Integer::compareTo).orElse(Integer.MAX_VALUE); - if (max != Integer.MAX_VALUE) { - int useValue = (360 + token.getFacingInDegrees()) % max; - bi = - imageTable.get( - imageTable.keySet().stream() - .sorted() - .filter(integer -> integer >= useValue) - .toList() - .getFirst()); - } - } else { - bi = ImageUtil.getTokenImage(token, renderHelper.getImageObserver()); - } - timer.stop("TokenRenderer-getRenderImage"); - return bi; + // get token image, using image table if present + MD5Key tokenImageId = token.getTokenImageAssetId(); + return ImageManager.getImage(tokenImageId, renderHelper.getImageObserver()); } private void paintTokenImage(Graphics2D g2d, TokenPosition position, float opacity) { var token = position.token(); var renderImage = getRenderImage(token); - if (renderImage == null) { - return; - } var imageTransform = TokenUtil.getRenderTransform( @@ -110,48 +77,4 @@ private void paintTokenImage(Graphics2D g2d, TokenPosition position, float opaci g2d.drawImage(renderImage, imageTransform, renderHelper.getImageObserver()); g2d.setStroke(new BasicStroke(1f)); } - - private static Map cacheImageTable(String tableName) { - LookupTable lookupTable = MapTool.getCampaign().getLookupTableMap().get(tableName); - if (lookupTable != null) { - BufferedImage broken = ImageManager.BROKEN_IMAGE; - Map tmp = new HashMap<>(); - List entries = lookupTable.getEntryList(); - for (LookupTable.LookupEntry entry : entries) { - MD5Key asset = entry.getImageId(); - if (asset != null) { - BufferedImage bi = ImageManager.getImageAndWait(asset); - if (!bi.equals(broken)) { - tmp.put(entry.getMax(), bi); - } - } - } - if (!tmp.isEmpty()) { - return tmp; - } - } - return null; - } - - private static class CacheTableImagesWorker - extends SwingWorker, String> { - String tableName; - - public CacheTableImagesWorker(String tableName) { - this.tableName = tableName; - } - - @Override - public Map doInBackground() { - return cacheImageTable(tableName); - } - - @Override - protected void done() { - try { - imageTableMap.put(tableName, get()); - } catch (Exception ignore) { - } - } - } } diff --git a/src/main/java/net/rptools/maptool/model/Token.java b/src/main/java/net/rptools/maptool/model/Token.java index 2f05a997b8..e3bd92042a 100644 --- a/src/main/java/net/rptools/maptool/model/Token.java +++ b/src/main/java/net/rptools/maptool/model/Token.java @@ -1134,15 +1134,20 @@ public MD5Key getImageAssetId() { } public MD5Key getTokenImageAssetId() { - if (!getHasImageTable() || !hasFacing() || getImageTableName() == null) + if (!getHasImageTable() || !hasFacing() || getImageTableName() == null) { return getImageAssetId(); + } LookupTable lookupTable = MapTool.getCampaign().getLookupTableMap().get(getImageTableName()); - if (lookupTable == null) return getImageAssetId(); + if (lookupTable == null) { + return getImageAssetId(); + } try { LookupTable.LookupEntry result = lookupTable.getLookup(String.valueOf(getFacing())); - if (result != null) return result.getImageId(); + if (result != null) { + return result.getImageId(); + } } catch (ParserException p) { /* do nothing */ From 1a7a5d8c3d9b52c39d9d2d2b3244f21a799b3975 Mon Sep 17 00:00:00 2001 From: Kenneth VanderLinde Date: Sat, 18 Oct 2025 02:30:29 -0700 Subject: [PATCH 2/2] Make Token#getTokenImageAssetId() more robust All `null` results from the image table lookup are now guaranteed to use the fallback to `Token#getImageAssetId()`. --- .../java/net/rptools/maptool/model/Token.java | 46 +++++++++++++++---- 1 file changed, 36 insertions(+), 10 deletions(-) diff --git a/src/main/java/net/rptools/maptool/model/Token.java b/src/main/java/net/rptools/maptool/model/Token.java index e3bd92042a..8b1c6f5265 100644 --- a/src/main/java/net/rptools/maptool/model/Token.java +++ b/src/main/java/net/rptools/maptool/model/Token.java @@ -1133,27 +1133,53 @@ public MD5Key getImageAssetId() { return assetId; } - public MD5Key getTokenImageAssetId() { + /** + * Looks up the token's facing in the token's image table. + * + *

If the token does not have an image table, or does not have its facing set, or otherwise + * cannot find an image ID from the lookup table, this method return {@code null}. + * + * @return The image ID from the image table, or {@code null} if none can be found. + */ + private @Nullable MD5Key lookupImageTableByFacing() { if (!getHasImageTable() || !hasFacing() || getImageTableName() == null) { - return getImageAssetId(); + return null; } LookupTable lookupTable = MapTool.getCampaign().getLookupTableMap().get(getImageTableName()); if (lookupTable == null) { - return getImageAssetId(); + return null; } + LookupTable.LookupEntry result; try { - LookupTable.LookupEntry result = lookupTable.getLookup(String.valueOf(getFacing())); - if (result != null) { - return result.getImageId(); - } - + result = lookupTable.getLookup(String.valueOf(getFacing())); } catch (ParserException p) { - /* do nothing */ + return null; + } + if (result == null) { + return null; + } + + MD5Key imageId = result.getImageId(); + if (imageId == null) { + return null; } - return getImageAssetId(); + return imageId; + } + + /** + * Looks up the token's facing in the token's image table. + * + *

If the token does not have an image table, or does not have its facing set, or otherwise + * cannot find an image ID from the lookup table, this method returns same result as {@link + * #getImageAssetId()}. + * + * @return The image ID from the image table. + */ + public MD5Key getTokenImageAssetId() { + return Objects.requireNonNullElseGet(lookupImageTableByFacing(), this::getImageAssetId); } /**