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..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,19 +156,20 @@ 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( 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); } } @@ -163,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. @@ -181,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)) { @@ -189,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; 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 @@ - + - + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + 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/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++; 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/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); 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;