From b58cf07c0d1049964a32fd8ae57e4bd0915fa8fb Mon Sep 17 00:00:00 2001 From: Kenneth VanderLinde Date: Fri, 22 Nov 2024 11:37:42 -0800 Subject: [PATCH 1/5] Add support in the model for unique light sources - Methods on `Token` to add or delete unique light sources and look them up. - Support in `AttachedLightSource` to resolve against a `Token` in addition to a `Campaign`. - Support in `TokenDto` to transfer unique light sources. Attaching (turning on) a unique light source works exactly as for a campaign light source: the ID is added to the list. The only difference in behaviour between unique and campaign light sources is where the definition lives. --- .../ui/zone/LightSourceIconOverlay.java | 2 +- .../maptool/client/ui/zone/ZoneView.java | 7 +-- .../maptool/model/AttachedLightSource.java | 14 ++++-- .../java/net/rptools/maptool/model/Token.java | 47 ++++++++++++++++--- src/main/proto/data_transfer_objects.proto | 1 + 5 files changed, 57 insertions(+), 14 deletions(-) diff --git a/src/main/java/net/rptools/maptool/client/ui/zone/LightSourceIconOverlay.java b/src/main/java/net/rptools/maptool/client/ui/zone/LightSourceIconOverlay.java index db480f8ee8..bd5146f4b7 100644 --- a/src/main/java/net/rptools/maptool/client/ui/zone/LightSourceIconOverlay.java +++ b/src/main/java/net/rptools/maptool/client/ui/zone/LightSourceIconOverlay.java @@ -35,7 +35,7 @@ public void paintOverlay(ZoneRenderer renderer, Graphics2D g) { if (token.hasLightSources()) { boolean foundNormalLight = false; for (AttachedLightSource attachedLightSource : token.getLightSources()) { - LightSource lightSource = attachedLightSource.resolve(MapTool.getCampaign()); + LightSource lightSource = attachedLightSource.resolve(token, MapTool.getCampaign()); if (lightSource != null && lightSource.getType() == LightSource.Type.NORMAL) { foundNormalLight = true; break; diff --git a/src/main/java/net/rptools/maptool/client/ui/zone/ZoneView.java b/src/main/java/net/rptools/maptool/client/ui/zone/ZoneView.java index 00ff1882b7..8a3b7f0b66 100644 --- a/src/main/java/net/rptools/maptool/client/ui/zone/ZoneView.java +++ b/src/main/java/net/rptools/maptool/client/ui/zone/ZoneView.java @@ -78,7 +78,7 @@ private Set getLightSources(Player.Role role, LightSource.Type type) { private void addLightSourceToken(Token token, Set roles) { for (AttachedLightSource als : token.getLightSources()) { - LightSource lightSource = als.resolve(MapTool.getCampaign()); + LightSource lightSource = als.resolve(token, MapTool.getCampaign()); if (lightSource == null) { continue; } @@ -316,7 +316,8 @@ private List calculateLitAreas(Token lightSourceToken, double final var result = new ArrayList(); for (final var attachedLightSource : lightSourceToken.getLightSources()) { - LightSource lightSource = attachedLightSource.resolve(MapTool.getCampaign()); + LightSource lightSource = + attachedLightSource.resolve(lightSourceToken, MapTool.getCampaign()); if (lightSource == null) { continue; } @@ -669,7 +670,7 @@ public List getDrawableAuras(PlayerView view) { Point p = FogUtil.calculateVisionCenter(token, zone); for (AttachedLightSource als : token.getLightSources()) { - LightSource lightSource = als.resolve(MapTool.getCampaign()); + LightSource lightSource = als.resolve(token, MapTool.getCampaign()); if (lightSource == null) { continue; } diff --git a/src/main/java/net/rptools/maptool/model/AttachedLightSource.java b/src/main/java/net/rptools/maptool/model/AttachedLightSource.java index da9e46a48b..5ca79f80a9 100644 --- a/src/main/java/net/rptools/maptool/model/AttachedLightSource.java +++ b/src/main/java/net/rptools/maptool/model/AttachedLightSource.java @@ -31,8 +31,8 @@ public AttachedLightSource(@Nonnull GUID lightSourceId) { * Get the ID of the attached light source. * *

If you're trying to use this to look up a {@link net.rptools.maptool.model.LightSource}, - * consider using {@link #resolve(Campaign)} instead. If you're trying to compare to another - * {@code GUID}, consider using {@link #matches(GUID)}. + * consider using {@link #resolve(Token, Campaign)} instead. If you're trying to compare to + * another {@code GUID}, consider using {@link #matches(GUID)}. * * @return The ID of the attached light source. */ @@ -41,13 +41,19 @@ public GUID getId() { } /** - * Obtain the attached {@code LightSource} from the campaign. + * Obtain the attached {@code LightSource} from the token or campaign. * + * @param token The token in which to look up light source IDs. * @param campaign The campaign in which to look up light source IDs. * @return The {@code LightSource} referenced by this {@code AttachedLightSource}, or {@code null} * if no such light source exists. */ - public @Nullable LightSource resolve(Campaign campaign) { + public @Nullable LightSource resolve(Token token, Campaign campaign) { + final var uniqueLightSource = token.getUniqueLightSource(lightSourceId); + if (uniqueLightSource != null) { + return uniqueLightSource; + } + for (Map map : campaign.getLightSourcesMap().values()) { if (map.containsKey(lightSourceId)) { return map.get(lightSourceId); diff --git a/src/main/java/net/rptools/maptool/model/Token.java b/src/main/java/net/rptools/maptool/model/Token.java index fbb392e64d..0395e011e9 100644 --- a/src/main/java/net/rptools/maptool/model/Token.java +++ b/src/main/java/net/rptools/maptool/model/Token.java @@ -333,6 +333,8 @@ public String toString() { private MD5Key charsheetImage; private MD5Key portraitImage; + private Map uniqueLightSources = new LinkedHashMap<>(); + /** * All light sources attached to the token. * @@ -478,6 +480,7 @@ public Token(Token token) { ownerType = token.ownerType; ownerList.addAll(token.ownerList); + uniqueLightSources.putAll(token.uniqueLightSources); lightSourceList.addAll(token.lightSourceList); state.putAll(token.state); @@ -900,6 +903,26 @@ public String getImageTableName() { return imageTableName; } + public @Nonnull Collection getUniqueLightSources() { + return uniqueLightSources.values(); + } + + public @Nullable LightSource getUniqueLightSource(GUID lightSourceId) { + return uniqueLightSources.getOrDefault(lightSourceId, null); + } + + public void addUniqueLightSource(LightSource source) { + uniqueLightSources.put(source.getId(), source); + } + + public void removeUniqueLightSource(GUID lightSourceId) { + uniqueLightSources.remove(lightSourceId); + } + + public void removeAllUniqueLightsources() { + uniqueLightSources.clear(); + } + public void addLightSource(GUID lightSourceId) { if (lightSourceList.stream().anyMatch(source -> source.matches(lightSourceId))) { // Avoid duplicates. @@ -911,7 +934,7 @@ public void addLightSource(GUID lightSourceId) { public void removeLightSourceType(LightSource.Type lightType) { for (ListIterator i = lightSourceList.listIterator(); i.hasNext(); ) { AttachedLightSource als = i.next(); - LightSource lightSource = als.resolve(MapTool.getCampaign()); + LightSource lightSource = als.resolve(this, MapTool.getCampaign()); if (lightSource != null && lightSource.getType() == lightType) { i.remove(); } @@ -921,7 +944,7 @@ public void removeLightSourceType(LightSource.Type lightType) { public void removeGMAuras() { for (ListIterator i = lightSourceList.listIterator(); i.hasNext(); ) { AttachedLightSource als = i.next(); - LightSource lightSource = als.resolve(MapTool.getCampaign()); + LightSource lightSource = als.resolve(this, MapTool.getCampaign()); if (lightSource != null) { List lights = lightSource.getLightList(); for (Light light : lights) { @@ -936,7 +959,7 @@ public void removeGMAuras() { public void removeOwnerOnlyAuras() { for (ListIterator i = lightSourceList.listIterator(); i.hasNext(); ) { AttachedLightSource als = i.next(); - LightSource lightSource = als.resolve(MapTool.getCampaign()); + LightSource lightSource = als.resolve(this, MapTool.getCampaign()); if (lightSource != null) { List lights = lightSource.getLightList(); for (Light light : lights) { @@ -950,7 +973,7 @@ public void removeOwnerOnlyAuras() { public boolean hasOwnerOnlyAuras() { for (AttachedLightSource als : lightSourceList) { - LightSource lightSource = als.resolve(MapTool.getCampaign()); + LightSource lightSource = als.resolve(this, MapTool.getCampaign()); if (lightSource != null) { List lights = lightSource.getLightList(); for (Light light : lights) { @@ -965,7 +988,7 @@ public boolean hasOwnerOnlyAuras() { public boolean hasGMAuras() { for (AttachedLightSource als : lightSourceList) { - LightSource lightSource = als.resolve(MapTool.getCampaign()); + LightSource lightSource = als.resolve(this, MapTool.getCampaign()); if (lightSource != null) { List lights = lightSource.getLightList(); for (Light light : lights) { @@ -980,7 +1003,7 @@ public boolean hasGMAuras() { public boolean hasLightSourceType(LightSource.Type lightType) { for (AttachedLightSource als : lightSourceList) { - LightSource lightSource = als.resolve(MapTool.getCampaign()); + LightSource lightSource = als.resolve(this, MapTool.getCampaign()); if (lightSource != null && lightSource.getType() == lightType) { return true; } @@ -2532,6 +2555,12 @@ protected Object readResolve() { if (ownerList == null) { ownerList = new HashSet<>(); } + if (uniqueLightSources == null) { + uniqueLightSources = new LinkedHashMap<>(); + } else { + // Whatever type of map is present, we want an order-preserving linked hash map. + uniqueLightSources = new LinkedHashMap<>(uniqueLightSources); + } // Remove null and duplicate attached light sources. List lightSources = @@ -2985,6 +3014,10 @@ public static Token fromDto(TokenDto dto) { dto.hasCharsheetImage() ? new MD5Key(dto.getCharsheetImage().getValue()) : null; token.portraitImage = dto.hasPortraitImage() ? new MD5Key(dto.getPortraitImage().getValue()) : null; + + dto.getUniqueLightSourcesList().stream() + .map(LightSource::fromDto) + .forEach(source -> token.uniqueLightSources.put(source.getId(), source)); token.lightSourceList.addAll( dto.getLightSourcesList().stream() .map(AttachedLightSource::fromDto) @@ -3112,6 +3145,8 @@ public TokenDto toDto() { if (portraitImage != null) { dto.setPortraitImage(StringValue.of(portraitImage.toString())); } + dto.addAllUniqueLightSources( + uniqueLightSources.values().stream().map(LightSource::toDto).collect(Collectors.toList())); dto.addAllLightSources( lightSourceList.stream().map(AttachedLightSource::toDto).collect(Collectors.toList())); if (sightType != null) { diff --git a/src/main/proto/data_transfer_objects.proto b/src/main/proto/data_transfer_objects.proto index 895209702e..7f8941be33 100644 --- a/src/main/proto/data_transfer_objects.proto +++ b/src/main/proto/data_transfer_objects.proto @@ -347,6 +347,7 @@ message TokenDto { bool is_flipped_iso = 47; google.protobuf.StringValue charsheet_image = 48; google.protobuf.StringValue portrait_image = 49; + repeated LightSourceDto unique_light_sources = 72; repeated AttachedLightSourceDto light_sources = 50; google.protobuf.StringValue sight_type = 51; bool has_sight = 52; From edf869c251b6deed3031f48b6dbcaa8bafdb985f Mon Sep 17 00:00:00 2001 From: Kenneth VanderLinde Date: Fri, 22 Nov 2024 11:39:35 -0800 Subject: [PATCH 2/5] Tweak method in ServerCommand for attaching light sources The new version is not an overload of `updateTokenProperty()` but has a more descriptive name. It also does not rely on the caller deciding which `Token.Update` value to use. Instead, a `boolean` is accepted to toggle the light source on or off, and the method will pick one of the two valid `Token.Update` value based on that. --- .../client/ServerCommandClientImpl.java | 20 +++++++++++-------- .../client/functions/TokenLightFunctions.java | 5 ++--- .../rptools/maptool/server/ServerCommand.java | 12 +++++++++-- 3 files changed, 24 insertions(+), 13 deletions(-) diff --git a/src/main/java/net/rptools/maptool/client/ServerCommandClientImpl.java b/src/main/java/net/rptools/maptool/client/ServerCommandClientImpl.java index 03b37ab24a..0ecbb2f4dd 100644 --- a/src/main/java/net/rptools/maptool/client/ServerCommandClientImpl.java +++ b/src/main/java/net/rptools/maptool/client/ServerCommandClientImpl.java @@ -640,6 +640,18 @@ public void removeData(String type, String namespace, String name) { makeServerCall(Message.newBuilder().setRemoveDataMsg(msg).build()); } + @Override + public void toggleLightSourceOnToken(Token token, boolean toggleOn, LightSource lightSource) { + var update = toggleOn ? Token.Update.addLightSource : Token.Update.removeLightSource; + // We only need to send the ID of the light source. + updateTokenProperty( + token, + update, + TokenPropertyValueDto.newBuilder() + .setLightSourceId(lightSource.getId().toString()) + .build()); + } + @Override public void setTokenTopology(Token token, @Nullable Area area, Zone.TopologyType topologyType) { if (area == null) { @@ -708,14 +720,6 @@ public void updateTokenProperty(Token token, Token.Update update, String value) token, update, TokenPropertyValueDto.newBuilder().setStringValue(value).build()); } - @Override - public void updateTokenProperty(Token token, Token.Update update, LightSource value) { - updateTokenProperty( - token, - update, - TokenPropertyValueDto.newBuilder().setLightSourceId(value.getId().toString()).build()); - } - @Override public void updateTokenProperty(Token token, Token.Update update, int value1, int value2) { updateTokenProperty( diff --git a/src/main/java/net/rptools/maptool/client/functions/TokenLightFunctions.java b/src/main/java/net/rptools/maptool/client/functions/TokenLightFunctions.java index 7a3d2d79de..229b7ebcc4 100644 --- a/src/main/java/net/rptools/maptool/client/functions/TokenLightFunctions.java +++ b/src/main/java/net/rptools/maptool/client/functions/TokenLightFunctions.java @@ -147,12 +147,11 @@ private static BigDecimal setLight(Token token, String category, String name, Bi I18N.getText("macro.function.tokenLight.unknownLightType", "setLights", category)); } - final var updateAction = - BigDecimal.ZERO.equals(val) ? Token.Update.removeLightSource : Token.Update.addLightSource; + final var add = !BigDecimal.ZERO.equals(val); for (LightSource ls : sources) { if (name.equals(ls.getName())) { found = true; - MapTool.serverCommand().updateTokenProperty(token, updateAction, ls); + MapTool.serverCommand().toggleLightSourceOnToken(token, add, ls); } } diff --git a/src/main/java/net/rptools/maptool/server/ServerCommand.java b/src/main/java/net/rptools/maptool/server/ServerCommand.java index 18218fd5aa..3d79005e1b 100644 --- a/src/main/java/net/rptools/maptool/server/ServerCommand.java +++ b/src/main/java/net/rptools/maptool/server/ServerCommand.java @@ -185,6 +185,16 @@ default void updateTopology( void removeData(String type, String namespace, String name); + /** + * Adds or removes a light source on {@code token}. + * + * @param token The token to modify + * @param toggleOn If {@code true}, the light source is turned on for the token. Otherwise, it is + * turned off. + * @param lightSource The light source to add. + */ + void toggleLightSourceOnToken(Token token, boolean toggleOn, LightSource lightSource); + void setTokenTopology(Token token, @Nullable Area area, Zone.TopologyType topologyType); void updateTokenProperty(Token token, Token.Update update, int value); @@ -200,8 +210,6 @@ void updateTokenProperty( void updateTokenProperty(Token token, Token.Update update, String value); - void updateTokenProperty(Token token, Token.Update update, LightSource value); - void updateTokenProperty(Token token, Token.Update update, int value1, int value2); void updateTokenProperty(Token token, Token.Update update, boolean value); From f7847dbce7d725596297ae024e8924ec3594386d Mon Sep 17 00:00:00 2001 From: Kenneth VanderLinde Date: Fri, 22 Nov 2024 11:41:38 -0800 Subject: [PATCH 3/5] Add support in existing macros for unique light sources This only adds the ability to write a `"$token"` category in `getLights()`, `setLight()`, and `hasLightSource()` to restrict the operation to unique light sources on the given token, rather than restricting it to a campaign category. It also extends the wildcard (`"*"`) support in these functions to also look for unique light sources on the token. --- .../client/functions/TokenLightFunctions.java | 40 ++++++++++++++++--- 1 file changed, 35 insertions(+), 5 deletions(-) diff --git a/src/main/java/net/rptools/maptool/client/functions/TokenLightFunctions.java b/src/main/java/net/rptools/maptool/client/functions/TokenLightFunctions.java index 229b7ebcc4..6b7791b4ad 100644 --- a/src/main/java/net/rptools/maptool/client/functions/TokenLightFunctions.java +++ b/src/main/java/net/rptools/maptool/client/functions/TokenLightFunctions.java @@ -32,6 +32,8 @@ public class TokenLightFunctions extends AbstractFunction { private static final TokenLightFunctions instance = new TokenLightFunctions(); + private static final String TOKEN_CATEGORY = "$token"; + private TokenLightFunctions() { super(0, 5, "hasLightSource", "clearLights", "setLight", "getLights"); } @@ -84,7 +86,8 @@ public Object childEvaluate( * * @param token The token to get the light sources for. * @param category The category to get the light sources for. If "*" then the light sources for - * all categories will be returned. + * all categories will be returned. If "$token" then only light sources defined on the token + * will be returned. * @param delim the delimiter for the list. * @return a string list containing the lights that are on. * @throws ParserException if the light type can't be found. @@ -96,6 +99,12 @@ private static String getLights(Token token, String category, String delim) MapTool.getCampaign().getLightSourcesMap(); if (category.equals("*")) { + // Look up on both token and campaign. + for (LightSource ls : token.getUniqueLightSources()) { + if (token.hasLightSource(ls)) { + lightList.add(ls.getName()); + } + } for (Map lsMap : lightSourcesMap.values()) { for (LightSource ls : lsMap.values()) { if (token.hasLightSource(ls)) { @@ -103,6 +112,12 @@ private static String getLights(Token token, String category, String delim) } } } + } else if (TOKEN_CATEGORY.equals(category)) { + for (LightSource ls : token.getUniqueLightSources()) { + if (token.hasLightSource(ls)) { + lightList.add(ls.getName()); + } + } } else if (lightSourcesMap.containsKey(category)) { for (LightSource ls : lightSourcesMap.get(category).values()) { if (token.hasLightSource(ls)) { @@ -127,7 +142,8 @@ private static String getLights(Token token, String category, String delim) * Sets the light value for a token. * * @param token the token to set the light for. - * @param category the category of the light source. + * @param category the category of the light source. Use "$token" for light sources defined on the + * token. * @param name the name of the light source. * @param val the value to set for the light source, 0 for off non 0 for on. * @return 0 if the light was not found, otherwise 1; @@ -140,7 +156,9 @@ private static BigDecimal setLight(Token token, String category, String name, Bi MapTool.getCampaign().getLightSourcesMap(); Iterable sources; - if (lightSourcesMap.containsKey(category)) { + if (TOKEN_CATEGORY.equals(category)) { + sources = token.getUniqueLightSources(); + } else if (lightSourcesMap.containsKey(category)) { sources = lightSourcesMap.get(category).values(); } else { throw new ParserException( @@ -162,6 +180,7 @@ private static BigDecimal setLight(Token token, String category, String name, Bi * Checks to see if the token has a light source. The token is checked to see if it has a light * source with the name in the second parameter from the category in the first parameter. A "*" * for category indicates all categories are checked; a "*" for name indicates all names are + * checked. The "$token" category indicates that only light sources defined on the token are * checked. * * @param token the token to check. @@ -180,6 +199,12 @@ public static boolean hasLightSource(Token token, String category, String name) MapTool.getCampaign().getLightSourcesMap(); if ("*".equals(category)) { + // Look up on both token and campaign. + for (LightSource ls : token.getUniqueLightSources()) { + if (ls.getName().equals(name) && token.hasLightSource(ls)) { + return true; + } + } for (Map lsMap : lightSourcesMap.values()) { for (LightSource ls : lsMap.values()) { if (ls.getName().equals(name) && token.hasLightSource(ls)) { @@ -188,8 +213,13 @@ public static boolean hasLightSource(Token token, String category, String name) } return false; } - } - if (lightSourcesMap.containsKey(category)) { + } else if (TOKEN_CATEGORY.equals(category)) { + for (LightSource ls : token.getUniqueLightSources()) { + if ((ls.getName().equals(name) || "*".equals(name)) && token.hasLightSource(ls)) { + return true; + } + } + } else if (lightSourcesMap.containsKey(category)) { for (LightSource ls : lightSourcesMap.get(category).values()) { if ((ls.getName().equals(name) || "*".equals(name)) && token.hasLightSource(ls)) { return true; From f8d507bb164174e1771cf2c48003c7a5352868e6 Mon Sep 17 00:00:00 2001 From: Kenneth VanderLinde Date: Fri, 22 Nov 2024 11:44:02 -0800 Subject: [PATCH 4/5] Add support in the UI for unique light sources The Edit Token dialog now sports a textbox where unique light sources can be defined. The syntax is identical to the light syntax in the Campaign Properties dialog except that categories are not supported. If a token has unique light sources, its context menu will display them under "Light Sources". They will be in their own "Unique" category, separated from the campaign categories. The functionality is identical to that for campaign lights. --- .../client/ui/AbstractTokenPopupMenu.java | 9 +++++ .../ui/token/dialog/edit/EditTokenDialog.java | 27 +++++++++++++ .../dialog/edit/TokenPropertiesDialog.form | 40 +++++++++++++++---- 3 files changed, 69 insertions(+), 7 deletions(-) diff --git a/src/main/java/net/rptools/maptool/client/ui/AbstractTokenPopupMenu.java b/src/main/java/net/rptools/maptool/client/ui/AbstractTokenPopupMenu.java index c2036d646f..1d6eb3834e 100644 --- a/src/main/java/net/rptools/maptool/client/ui/AbstractTokenPopupMenu.java +++ b/src/main/java/net/rptools/maptool/client/ui/AbstractTokenPopupMenu.java @@ -151,6 +151,15 @@ protected JMenu createLightSourceMenu() { menu.addSeparator(); } + // Add unique light sources for the token. + { + JMenu subMenu = createLightCategoryMenu("Unique", tokenUnderMouse.getUniqueLightSources()); + if (subMenu.getItemCount() != 0) { + menu.add(subMenu); + menu.addSeparator(); + } + } + for (Entry> entry : MapTool.getCampaign().getLightSourcesMap().entrySet()) { JMenu subMenu = createLightCategoryMenu(entry.getKey(), entry.getValue().values()); diff --git a/src/main/java/net/rptools/maptool/client/ui/token/dialog/edit/EditTokenDialog.java b/src/main/java/net/rptools/maptool/client/ui/token/dialog/edit/EditTokenDialog.java index 6f367aa107..509ec05741 100644 --- a/src/main/java/net/rptools/maptool/client/ui/token/dialog/edit/EditTokenDialog.java +++ b/src/main/java/net/rptools/maptool/client/ui/token/dialog/edit/EditTokenDialog.java @@ -87,6 +87,7 @@ import net.rptools.maptool.util.ExtractHeroLab; import net.rptools.maptool.util.FunctionUtil; import net.rptools.maptool.util.ImageManager; +import net.rptools.maptool.util.LightSyntax; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea; @@ -172,6 +173,10 @@ public void initTerrainModifiersIgnoredList() { EnumSet.allOf(TerrainModifierOperation.class).forEach(operationModel::addElement); } + public void initUniqueLightSourcesTextPane() { + setUniqueLightSourcesEnabled(MapTool.getPlayer().isGM()); + } + public void initJtsMethodComboBox() { getJtsMethodComboBox().setModel(new DefaultComboBoxModel<>(JTS_SimplifyMethodType.values())); } @@ -201,6 +206,8 @@ public void closeDialog() { setGmNotesEnabled(MapTool.getPlayer().isGM()); getComponent("@GMName").setEnabled(MapTool.getPlayer().isGM()); + setUniqueLightSourcesEnabled(MapTool.getPlayer().isGM()); + dialog.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); setLibTokenPaneEnabled(token.isLibToken()); @@ -371,6 +378,9 @@ public void bind(final Token token) { .mapToInt(Integer::valueOf) .toArray()); + getUniqueLightSourcesTextPane() + .setText(new LightSyntax().stringifyLights(token.getUniqueLightSources())); + // Jamz: Init the Topology tab... JTabbedPane tabbedPane = getTabbedPane(); @@ -709,6 +719,15 @@ public JList getTerrainModifiersIgnoredList() { return (JList) getComponent("terrainModifiersIgnored"); } + public void setUniqueLightSourcesEnabled(boolean enabled) { + getUniqueLightSourcesTextPane().setEnabled(enabled); + getLabel("uniqueLightSourcesLabel").setEnabled(enabled); + } + + public JTextPane getUniqueLightSourcesTextPane() { + return (JTextPane) getComponent("uniqueLightSources"); + } + public JLabel getLibTokenURIErrorLabel() { return (JLabel) getComponent("Label.LibURIError"); } @@ -783,6 +802,14 @@ public boolean commit() { token.setTerrainModifiersIgnored( new HashSet<>(getTerrainModifiersIgnoredList().getSelectedValuesList())); + var uniqueLightSources = + new LightSyntax() + .parseLights(getUniqueLightSourcesTextPane().getText(), token.getUniqueLightSources()); + token.removeAllUniqueLightsources(); + for (var lightSource : uniqueLightSources.values()) { + token.addUniqueLightSource(lightSource); + } + // Get the states Component[] stateComponents = getStatesPanel().getComponents(); Container barPanel = null; diff --git a/src/main/java/net/rptools/maptool/client/ui/token/dialog/edit/TokenPropertiesDialog.form b/src/main/java/net/rptools/maptool/client/ui/token/dialog/edit/TokenPropertiesDialog.form index f1543b6b3a..57752636be 100644 --- a/src/main/java/net/rptools/maptool/client/ui/token/dialog/edit/TokenPropertiesDialog.form +++ b/src/main/java/net/rptools/maptool/client/ui/token/dialog/edit/TokenPropertiesDialog.form @@ -550,7 +550,7 @@ - + @@ -710,7 +710,7 @@ - + @@ -937,14 +937,40 @@ - + - + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + From 4305f69daf131011e866c4d6844f85c3e681fc6d Mon Sep 17 00:00:00 2001 From: Kenneth VanderLinde Date: Fri, 22 Nov 2024 11:25:47 -0800 Subject: [PATCH 5/5] Add support in UVTT importer for unique lights The importer will synthesize unique lights for each light in the UVTT file. The range will be taken from the light's `"range"` field, and the lumens will be set to 100. The colour will be set to the light's `"color"` field, unless the map image has baked lighting in - in that case, the unique light source will be clear. The `IGNORES-VBL` flag will be set if the light's `"shadow"` flag is `false`. All light properties are now written to the GM notes. Previously we wrote a hand-picked subset, but now everything is there. This avoids any chance of missing some in the future if the UVTT format adds additional properties. There are a few limitations with the import: 1. UVTT does not include the shape or texture of the light, so imported lights are always treated as circles. 2. We have no equivalent to "intensity" so that remains unused. --- .../utilities/DungeonDraftImporter.java | 81 +++++++++++++++---- 1 file changed, 64 insertions(+), 17 deletions(-) diff --git a/src/main/java/net/rptools/maptool/client/utilities/DungeonDraftImporter.java b/src/main/java/net/rptools/maptool/client/utilities/DungeonDraftImporter.java index 761efb6fc9..d9ea8b251a 100644 --- a/src/main/java/net/rptools/maptool/client/utilities/DungeonDraftImporter.java +++ b/src/main/java/net/rptools/maptool/client/utilities/DungeonDraftImporter.java @@ -20,6 +20,7 @@ import com.google.gson.JsonParser; import com.jidesoft.utils.Base64; import java.awt.BasicStroke; +import java.awt.Color; import java.awt.geom.AffineTransform; import java.awt.geom.Area; import java.awt.geom.GeneralPath; @@ -28,7 +29,7 @@ import java.io.FileInputStream; import java.io.IOException; import java.io.InputStreamReader; -import java.math.BigDecimal; +import java.util.List; import net.rptools.maptool.client.MapTool; import net.rptools.maptool.client.ui.mappropertiesdialog.MapPropertiesDialog; import net.rptools.maptool.client.ui.theme.Images; @@ -36,11 +37,16 @@ import net.rptools.maptool.language.I18N; import net.rptools.maptool.model.Asset; import net.rptools.maptool.model.AssetManager; +import net.rptools.maptool.model.GUID; import net.rptools.maptool.model.GridFactory; +import net.rptools.maptool.model.Light; +import net.rptools.maptool.model.LightSource; +import net.rptools.maptool.model.ShapeType; import net.rptools.maptool.model.Token; import net.rptools.maptool.model.Zone; import net.rptools.maptool.model.Zone.Layer; import net.rptools.maptool.model.ZoneFactory; +import net.rptools.maptool.model.drawing.DrawableColorPaint; import org.apache.commons.io.FilenameUtils; /** Class for importing Dungeondraft Universal VTT export format. */ @@ -91,6 +97,9 @@ public class DungeonDraftImporter { /** Height of the Light source icon. */ private static final int LIGHT_HEIGHT = 20; + /** Contains environmental details (ambient lighting, baked-in lighting) */ + public static final String VTT_FIELD_ENVIRONMENT = "environment"; + /** Asset to use to represent Light sources. */ private static final Asset lightSourceAsset = Asset.createImageAsset("LightSource", RessourceManager.getImage(Images.LIGHT_SOURCE)); @@ -245,9 +254,18 @@ public void importVTT() throws IOException { }); } + boolean bakedLighting = false; + if (ddvtt.has(VTT_FIELD_ENVIRONMENT)) { + var environment = ddvtt.getAsJsonObject(VTT_FIELD_ENVIRONMENT); + var bakedLightingMember = environment.get("baked_lighting"); + if (bakedLightingMember != null) { + bakedLighting = bakedLightingMember.getAsBoolean(); + } + } + JsonArray lights = ddvtt.getAsJsonArray("lights"); if (lights != null && lights.size() > 0) { - placeLights(zone, lights, pixelsPerCell); + placeLights(zone, lights, pixelsPerCell, bakedLighting); } // If everything has been successful, we can add the zone to the campaign. @@ -260,12 +278,16 @@ public void importVTT() throws IOException { * @param zone The new {@link Zone} that was created. * @param lights The {@link JsonArray} containing the lights. * @param pixelsPerCell The number of pixels per grid cell on the map. + * @param bakedLighting If {@code true}, define and attach unique lights to each light token. */ - private void placeLights(Zone zone, JsonArray lights, double pixelsPerCell) { + private void placeLights( + Zone zone, JsonArray lights, double pixelsPerCell, boolean bakedLighting) { int lightNo = 1; boolean ignoredLights = false; for (JsonElement ele : lights) { - JsonObject position = ele.getAsJsonObject().getAsJsonObject("position"); + var lightJson = ele.getAsJsonObject(); + + JsonObject position = lightJson.getAsJsonObject("position"); if (position.has("x") && position.has("y")) { Token lightToken = new Token("light-" + lightNo, lightSourceAsset.getMD5Key()); lightToken.setLayer(Layer.OBJECT); @@ -278,19 +300,44 @@ private void placeLights(Zone zone, JsonArray lights, double pixelsPerCell) { lightToken.setX((int) (position.get("x").getAsDouble() * pixelsPerCell) - LIGHT_WIDTH / 2); lightToken.setY((int) (position.get("y").getAsDouble() * pixelsPerCell) - LIGHT_HEIGHT / 2); - JsonObject lightValues = new JsonObject(); - lightValues.addProperty( - "range", ele.getAsJsonObject().getAsJsonPrimitive("range").getAsBigDecimal()); - lightValues.addProperty( - "intensity", ele.getAsJsonObject().getAsJsonPrimitive("intensity").getAsBigDecimal()); - lightValues.addProperty( - "color", ele.getAsJsonObject().getAsJsonPrimitive("color").getAsString()); - lightValues.addProperty( - "shadows", - ele.getAsJsonObject().getAsJsonPrimitive("shadows").getAsBoolean() - ? BigDecimal.ONE - : BigDecimal.ZERO); - lightToken.setGMNotes(lightValues.toString()); + // If lighting is baked in, produce a clear light. + Color color; + if (bakedLighting) { + color = null; + } else { + color = + new Color( + Integer.parseUnsignedInt( + lightJson.getAsJsonPrimitive("color").getAsString(), 16)); + } + + var light = + new Light( + ShapeType.CIRCLE, + 0., + // Range is measured in cells. + lightJson.getAsJsonPrimitive("range").getAsDouble() * zone.getUnitsPerCell(), + 0., + 0., + color == null ? null : new DrawableColorPaint(color), + 100, + false, + false); + var lightSource = + LightSource.createRegular( + "uvtt-imported", + new GUID(), + LightSource.Type.NORMAL, + false, + // "shadows" means whether the light respects light blocking. + !lightJson.getAsJsonPrimitive("shadows").getAsBoolean(), + List.of(light)); + // Install the light source... + lightToken.addUniqueLightSource(lightSource); + // ... and activate it immediately. + lightToken.addLightSource(lightSource.getId()); + + lightToken.setGMNotes(ele.toString()); zone.putToken(lightToken); lightNo++;