diff --git a/src/main/java/net/rptools/maptool/client/ui/campaignproperties/CampaignPropertiesDialog.java b/src/main/java/net/rptools/maptool/client/ui/campaignproperties/CampaignPropertiesDialog.java index a6230cf7d2..176c711aef 100644 --- a/src/main/java/net/rptools/maptool/client/ui/campaignproperties/CampaignPropertiesDialog.java +++ b/src/main/java/net/rptools/maptool/client/ui/campaignproperties/CampaignPropertiesDialog.java @@ -18,23 +18,13 @@ import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.KeyEvent; -import java.io.BufferedReader; import java.io.File; import java.io.IOException; -import java.io.LineNumberReader; -import java.io.StringReader; -import java.text.ParseException; -import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.LinkedList; import java.util.List; import java.util.Map; -import java.util.Map.Entry; -import java.util.TreeMap; -import java.util.regex.Pattern; import javax.swing.*; import net.rptools.lib.FileUtil; import net.rptools.maptool.client.AppConstants; @@ -53,9 +43,9 @@ import net.rptools.maptool.model.LightSource; import net.rptools.maptool.model.ShapeType; import net.rptools.maptool.model.SightType; -import net.rptools.maptool.model.drawing.DrawableColorPaint; +import net.rptools.maptool.util.LightSyntax; import net.rptools.maptool.util.PersistenceUtil; -import net.rptools.maptool.util.StringUtil; +import net.rptools.maptool.util.SightSyntax; public class CampaignPropertiesDialog extends JDialog { public enum Status { @@ -241,195 +231,11 @@ private void copyCampaignToUI(CampaignProperties campaignProperties) { } private String updateSightPanel(Map sightTypeMap) { - StringBuilder builder = new StringBuilder(); - for (SightType sight : sightTypeMap.values()) { - builder.append(sight.getName()).append(": "); - - builder.append(sight.getShape().name().toLowerCase()).append(" "); - - switch (sight.getShape()) { - case SQUARE, CIRCLE, GRID, HEX: - break; - case BEAM: - if (sight.getArc() != 0) { - builder.append("arc=").append(StringUtil.formatDecimal(sight.getArc())).append(' '); - } else { - builder.append("arc=4").append(StringUtil.formatDecimal(sight.getArc())).append(' '); - } - if (sight.getOffset() != 0) { - builder - .append("offset=") - .append(StringUtil.formatDecimal(sight.getOffset())) - .append(' '); - } - break; - case CONE: - if (sight.getArc() != 0) { - builder.append("arc=").append(StringUtil.formatDecimal(sight.getArc())).append(' '); - } - if (sight.getOffset() != 0) { - builder - .append("offset=") - .append(StringUtil.formatDecimal(sight.getOffset())) - .append(' '); - } - break; - } - if (sight.getDistance() != 0) { - builder - .append("distance=") - .append(StringUtil.formatDecimal(sight.getDistance())) - .append(' '); - } - - // Scale with Token - if (sight.isScaleWithToken()) { - builder.append("scale "); - } - // Multiplier - if (sight.getMultiplier() != 1 && sight.getMultiplier() != 0) { - builder.append("x").append(StringUtil.formatDecimal(sight.getMultiplier())).append(' '); - } - // Personal light - if (sight.getPersonalLightSource() != null) { - LightSource source = sight.getPersonalLightSource(); - - if (source.getLightList() != null) { - for (Light light : source.getLightList()) { - double range = light.getRadius(); - - builder.append("r").append(StringUtil.formatDecimal(range)); - - if (light.getPaint() != null && light.getPaint() instanceof DrawableColorPaint) { - Color color = (Color) light.getPaint().getPaint(); - builder.append(toHex(color)); - } - final var lumens = light.getLumens(); - if (lumens >= 0) { - builder.append('+'); - } - builder.append(Integer.toString(lumens, 10)); - builder.append(' '); - } - } - } - builder.append('\n'); - } - return builder.toString(); + return new SightSyntax().stringify(sightTypeMap); } private String updateLightPanel(Map> lightSources) { - StringBuilder builder = new StringBuilder(); - for (Entry> entry : lightSources.entrySet()) { - builder.append(entry.getKey()); - builder.append("\n----\n"); - - for (LightSource lightSource : entry.getValue().values()) { - builder.append(lightSource.getName()).append(":"); - - if (lightSource.getType() != LightSource.Type.NORMAL) { - builder.append(' ').append(lightSource.getType().name().toLowerCase()); - } - if (lightSource.isScaleWithToken()) { - builder.append(" scale"); - } - - final var lastParameters = new LinkedHashMap(); - lastParameters.put("", null); - lastParameters.put("arc", 0.); - lastParameters.put("offset", 0.); - lastParameters.put("GM", false); - lastParameters.put("OWNER", false); - - for (Light light : lightSource.getLightList()) { - final var parameters = new HashMap<>(); - - // TODO: This HAS to change, the lights need to be auto describing, this hard wiring sucks - if (lightSource.getType() == LightSource.Type.AURA) { - parameters.put("GM", light.isGM()); - parameters.put("OWNER", light.isOwnerOnly()); - } - - parameters.put("", light.getShape().name().toLowerCase()); - switch (light.getShape()) { - default: - throw new RuntimeException( - "Unrecognized shape: " + light.getShape().toString().toLowerCase()); - case SQUARE, GRID, CIRCLE, HEX: - break; - case BEAM: - parameters.put("arc", light.getArcAngle()); - parameters.put("offset", light.getFacingOffset()); - break; - case CONE: - parameters.put("arc", light.getArcAngle()); - parameters.put("offset", light.getFacingOffset()); - break; - } - - for (final var parameterEntry : lastParameters.entrySet()) { - final var key = parameterEntry.getKey(); - final var oldValue = parameterEntry.getValue(); - final var newValue = parameters.get(key); - - if (newValue != null && !newValue.equals(oldValue)) { - lastParameters.put(key, newValue); - - // Special case: booleans are flags that are either present or not. - if (newValue instanceof Boolean b) { - if (b) { - builder.append(" ").append(key); - } - } else { - builder.append(" "); - if (!"".equals(key)) { - // Special case: don't include a key= for shapes. - builder.append(key).append("="); - } - builder.append( - switch (newValue) { - case Double d -> StringUtil.formatDecimal(d); - default -> newValue.toString(); - }); - } - } - } - - builder.append(' ').append(StringUtil.formatDecimal(light.getRadius())); - if (light.getPaint() instanceof DrawableColorPaint) { - Color color = (Color) light.getPaint().getPaint(); - builder.append(toHex(color)); - } - if (lightSource.getType() == LightSource.Type.NORMAL) { - final var lumens = light.getLumens(); - if (lumens >= 0) { - builder.append('+'); - } - builder.append(Integer.toString(lumens, 10)); - } - } - builder.append('\n'); - } - builder.append('\n'); - } - return builder.toString(); - } - - private String toHex(Color color) { - StringBuilder builder = new StringBuilder("#"); - - builder.append(padLeft(Integer.toHexString(color.getRed()), '0', 2)); - builder.append(padLeft(Integer.toHexString(color.getGreen()), '0', 2)); - builder.append(padLeft(Integer.toHexString(color.getBlue()), '0', 2)); - - return builder.toString(); - } - - private String padLeft(String str, char padChar, int length) { - while (str.length() < length) { - str = padChar + str; - } - return str; + return new LightSyntax().stringifyCategorizedLights(lightSources); } private void updateRepositoryList(CampaignProperties properties) { @@ -457,7 +263,9 @@ private void copyUIToCampaign() { campaign.getLightSourcesMap().clear(); campaign.getLightSourcesMap().putAll(lightMap); - commitSightMap(getSightPanel().getText()); + List sightMap = commitSightMap(getSightPanel().getText()); + campaign.setSightTypes(sightMap); + tokenStatesController.copyUIToCampaign(campaign); tokenBarController.copyUIToCampaign(campaign); @@ -470,154 +278,8 @@ private void copyUIToCampaign() { } } - private void commitSightMap(final String text) { - List sightList = new LinkedList(); - LineNumberReader reader = new LineNumberReader(new BufferedReader(new StringReader(text))); - String line = null; - String toBeParsed = null, errmsg = null; - List errlog = new LinkedList(); - try { - while ((line = reader.readLine()) != null) { - line = line.trim(); - - // Blanks - if (line.length() == 0 || line.indexOf(':') < 1) { - continue; - } - // Parse line - int split = line.indexOf(':'); - String label = line.substring(0, split).trim(); - String value = line.substring(split + 1).trim(); - - if (label.length() == 0) { - continue; - } - // Parse Details - double magnifier = 1; - // If null, no personal light has been defined. - List personalLightLights = null; - - String[] args = value.split("\\s+"); - ShapeType shape = ShapeType.CIRCLE; - boolean scaleWithToken = false; - int arc = 90; - float range = 0; - int offset = 0; - double pLightRange = 0; - - for (String arg : args) { - assert arg.length() > 0; // The split() uses "one or more spaces", removing empty strings - try { - shape = ShapeType.valueOf(arg.toUpperCase()); - arc = shape == ShapeType.BEAM ? 4 : arc; - continue; - } catch (IllegalArgumentException iae) { - // Expected when not defining a shape - } - // Scale with Token - if (arg.equalsIgnoreCase("SCALE")) { - scaleWithToken = true; - continue; - } - try { - - if (arg.startsWith("x")) { - toBeParsed = arg.substring(1); // Used in the catch block, below - errmsg = "msg.error.mtprops.sight.multiplier"; // (ditto) - magnifier = StringUtil.parseDecimal(toBeParsed); - } else if (arg.startsWith("r")) { // XXX Why not "r=#" instead of "r#"?? - toBeParsed = arg.substring(1); - errmsg = "msg.error.mtprops.sight.range"; - - final var rangeRegex = Pattern.compile("([^#+-]*)(#[0-9a-fA-F]+)?([+-]\\d*)?"); - final var matcher = rangeRegex.matcher(toBeParsed); - if (matcher.find()) { - pLightRange = StringUtil.parseDecimal(matcher.group(1)); - final var colorString = matcher.group(2); - final var lumensString = matcher.group(3); - // Note that Color.decode() _wants_ the leading "#", otherwise it might not treat - // the value as a hex code. - Color personalLightColor = null; - if (colorString != null) { - personalLightColor = Color.decode(colorString); - } - int perRangeLumens = 100; - if (lumensString != null) { - perRangeLumens = Integer.parseInt(lumensString, 10); - if (perRangeLumens == 0) { - errlog.add( - I18N.getText("msg.error.mtprops.sight.zerolumens", reader.getLineNumber())); - perRangeLumens = 100; - } - } - - if (personalLightLights == null) { - personalLightLights = new ArrayList<>(); - } - personalLightLights.add( - new Light( - shape, - 0, - pLightRange, - arc, - personalLightColor == null - ? null - : new DrawableColorPaint(personalLightColor), - perRangeLumens, - false, - false)); - } else { - throw new ParseException( - String.format("Unrecognized personal light syntax: %s", arg), 0); - } - } else if (arg.startsWith("arc=") && arg.length() > 4) { - toBeParsed = arg.substring(4); - errmsg = "msg.error.mtprops.sight.arc"; - arc = StringUtil.parseInteger(toBeParsed); - } else if (arg.startsWith("distance=") && arg.length() > 9) { - toBeParsed = arg.substring(9); - errmsg = "msg.error.mtprops.sight.distance"; - range = StringUtil.parseDecimal(toBeParsed).floatValue(); - } else if (arg.startsWith("offset=") && arg.length() > 7) { - toBeParsed = arg.substring(7); - errmsg = "msg.error.mtprops.sight.offset"; - offset = StringUtil.parseInteger(toBeParsed); - } else { - toBeParsed = arg; - errmsg = - I18N.getText( - "msg.error.mtprops.sight.unknownField", reader.getLineNumber(), toBeParsed); - errlog.add(errmsg); - } - } catch (ParseException e) { - assert errmsg != null; - errlog.add(I18N.getText(errmsg, reader.getLineNumber(), toBeParsed)); - } - } - - LightSource personalLight = - personalLightLights == null - ? null - : LightSource.createPersonal(scaleWithToken, personalLightLights); - SightType sight = - new SightType(label, magnifier, personalLight, shape, arc, scaleWithToken); - sight.setDistance(range); - sight.setOffset(offset); - - // Store - sightList.add(sight); - } - } catch (IOException ioe) { - MapTool.showError("msg.error.mtprops.sight.ioexception", ioe); - } - if (!errlog.isEmpty()) { - // Show the user a list of errors so they can (attempt to) correct all of them at once - MapTool.showFeedback(errlog.toArray()); - errlog.clear(); - throw new IllegalArgumentException( - "msg.error.mtprops.sight.definition"); // Don't save sights... - } - campaign.setSightTypes(sightList); + private List commitSightMap(final String text) { + return new SightSyntax().parse(text); } /** @@ -645,206 +307,7 @@ private void commitSightMap(final String text) { */ private Map> commitLightMap( final String text, final Map> originalLightSourcesMap) { - Map> lightMap = new TreeMap>(); - LineNumberReader reader = new LineNumberReader(new BufferedReader(new StringReader(text))); - String line = null; - List errlog = new LinkedList(); - - try { - String currentGroupName = null; - Map lightSourceMap = null; - - while ((line = reader.readLine()) != null) { - line = line.trim(); - - // Comments - if (line.length() > 0 && line.charAt(0) == '-') { - continue; - } - // Blank lines - if (line.length() == 0) { - if (currentGroupName != null) { - lightMap.put(currentGroupName, lightSourceMap); - } - currentGroupName = null; - continue; - } - // New group - if (currentGroupName == null) { - currentGroupName = line; - lightSourceMap = new HashMap(); - continue; - } - // Item - int split = line.indexOf(':'); - if (split < 1) { - continue; - } - - // region Light source properties. - String name = line.substring(0, split).trim(); - GUID id = new GUID(); - LightSource.Type type = LightSource.Type.NORMAL; - boolean scaleWithToken = false; - List lights = new ArrayList<>(); - // endregion - // region Individual light properties - ShapeType shape = ShapeType.CIRCLE; // TODO: Make a preference for default shape - double arc = 0; - double offset = 0; - boolean gmOnly = false; - boolean owner = false; - String distance = null; - // endregion - - for (String arg : line.substring(split + 1).split("\\s+")) { - arg = arg.trim(); - if (arg.length() == 0) { - continue; - } - if (arg.equalsIgnoreCase("GM")) { - gmOnly = true; - owner = false; - continue; - } - if (arg.equalsIgnoreCase("OWNER")) { - gmOnly = false; - owner = true; - continue; - } - // Scale with token designation - if (arg.equalsIgnoreCase("SCALE")) { - scaleWithToken = true; - continue; - } - // Shape designation ? - try { - shape = ShapeType.valueOf(arg.toUpperCase()); - arc = shape == ShapeType.BEAM ? 4 : arc; - continue; - } catch (IllegalArgumentException iae) { - // Expected when not defining a shape - } - - // Type designation ? - try { - type = LightSource.Type.valueOf(arg.toUpperCase()); - continue; - } catch (IllegalArgumentException iae) { - // Expected when not defining a shape - } - - // Facing offset designation - if (arg.toUpperCase().startsWith("OFFSET=")) { - try { - offset = Integer.parseInt(arg.substring(7)); - continue; - } catch (NullPointerException noe) { - errlog.add( - I18N.getText("msg.error.mtprops.light.offset", reader.getLineNumber(), arg)); - } - } - - // Parameters - split = arg.indexOf('='); - if (split > 0) { - String key = arg.substring(0, split); - String value = arg.substring(split + 1); - - // TODO: Make this a generic map to pass instead of 'arc' - if ("arc".equalsIgnoreCase(key)) { - try { - arc = StringUtil.parseDecimal(value); - shape = - (shape != ShapeType.CONE && shape != ShapeType.BEAM) - ? ShapeType.CONE - : shape; // If the user specifies an arc, force the shape to CONE - } catch (ParseException pe) { - errlog.add( - I18N.getText("msg.error.mtprops.light.arc", reader.getLineNumber(), value)); - } - } - continue; - } - - Color color = null; - int perRangeLumens = 100; - distance = arg; - - final var rangeRegex = Pattern.compile("([^#+-]*)(#[0-9a-fA-F]+)?([+-]\\d*)?"); - final var matcher = rangeRegex.matcher(arg); - if (matcher.find()) { - distance = matcher.group(1); - final var colorString = matcher.group(2); - final var lumensString = matcher.group(3); - // Note that Color.decode() _wants_ the leading "#", otherwise it might not treat the - // value as a hex code. - if (colorString != null) { - color = Color.decode(colorString); - } - if (lumensString != null) { - perRangeLumens = Integer.parseInt(lumensString, 10); - if (perRangeLumens == 0) { - errlog.add( - I18N.getText("msg.error.mtprops.light.zerolumens", reader.getLineNumber())); - perRangeLumens = 100; - } - } - } - - boolean isAura = type == LightSource.Type.AURA; - if (!isAura && (gmOnly || owner)) { - errlog.add(I18N.getText("msg.error.mtprops.light.gmOrOwner", reader.getLineNumber())); - gmOnly = false; - owner = false; - } - owner = gmOnly ? false : owner; - try { - Light t = - new Light( - shape, - offset, - StringUtil.parseDecimal(distance), - arc, - color == null ? null : new DrawableColorPaint(color), - perRangeLumens, - gmOnly, - owner); - lights.add(t); - } catch (ParseException pe) { - errlog.add( - I18N.getText("msg.error.mtprops.light.distance", reader.getLineNumber(), distance)); - } - } - // Keep ID the same if modifying existing light. This avoids tokens losing their lights when - // the light definition is modified. - if (originalLightSourcesMap.containsKey(currentGroupName)) { - for (LightSource ls : originalLightSourcesMap.get(currentGroupName).values()) { - if (ls.getName().equalsIgnoreCase(name)) { - assert ls.getId() != null; - id = ls.getId(); - break; - } - } - } - - final var source = LightSource.createRegular(name, id, type, scaleWithToken, lights); - lightSourceMap.put(source.getId(), source); - } - // Last group - if (currentGroupName != null) { - lightMap.put(currentGroupName, lightSourceMap); - } - } catch (IOException ioe) { - MapTool.showError("msg.error.mtprops.light.ioexception", ioe); - } - if (!errlog.isEmpty()) { - MapTool.showFeedback(errlog.toArray()); - errlog.clear(); - throw new IllegalArgumentException( - "msg.error.mtprops.light.definition"); // Don't save lights... - } - return lightMap; + return new LightSyntax().parseCategorizedLights(text, originalLightSourcesMap); } public JEditorPane getLightPanel() { 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 de76923ef6..01fbf49ef2 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; @@ -1210,6 +1237,7 @@ public void initTokenDetails() { public void initTokenLayoutPanel() { TokenLayoutPanel layoutPanel = new TokenLayoutPanel(); + layoutPanel.setMinimumSize(new Dimension(150, 125)); layoutPanel.setPreferredSize(new Dimension(150, 125)); layoutPanel.setName("tokenLayout"); @@ -1218,6 +1246,7 @@ public void initTokenLayoutPanel() { public void initCharsheetPanel() { ImageAssetPanel panel = new ImageAssetPanel(); + panel.setMinimumSize(new Dimension(150, 125)); panel.setPreferredSize(new Dimension(150, 125)); panel.setName("charsheet"); panel.setLayout(new GridLayout()); @@ -1227,6 +1256,7 @@ public void initCharsheetPanel() { public void initPortraitPanel() { ImageAssetPanel panel = new ImageAssetPanel(); + panel.setMinimumSize(new Dimension(150, 125)); panel.setPreferredSize(new Dimension(150, 125)); panel.setName("portrait"); panel.setLayout(new GridLayout()); 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 d4a0110eb1..11a6ea16d2 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 @@ - + @@ -562,7 +562,7 @@ - + @@ -570,7 +570,7 @@ - + @@ -578,7 +578,7 @@ - + @@ -587,7 +587,7 @@ - + @@ -595,7 +595,7 @@ - + @@ -604,7 +604,7 @@ - + @@ -613,7 +613,7 @@ - + @@ -622,7 +622,7 @@ - + @@ -631,7 +631,7 @@ - + @@ -641,7 +641,7 @@ - + @@ -654,7 +654,7 @@ - + @@ -665,7 +665,7 @@ - + @@ -673,7 +673,7 @@ - + @@ -682,7 +682,7 @@ - + @@ -692,7 +692,7 @@ - + @@ -700,7 +700,7 @@ - + @@ -710,7 +710,7 @@ - + @@ -809,7 +809,7 @@ - + @@ -818,7 +818,7 @@ - + @@ -827,7 +827,7 @@ - + @@ -836,7 +836,7 @@ - + @@ -844,7 +844,7 @@ - + @@ -853,7 +853,7 @@ - + @@ -862,7 +862,7 @@ - + @@ -871,7 +871,7 @@ - + @@ -881,7 +881,7 @@ - + @@ -896,7 +896,8 @@ - + + @@ -912,11 +913,6 @@ - - - - - @@ -941,6 +937,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/java/net/rptools/maptool/client/ui/token/dialog/edit/TokenPropertiesDialog.java b/src/main/java/net/rptools/maptool/client/ui/token/dialog/edit/TokenPropertiesDialog.java index 64f7636133..30bee1f16a 100644 --- a/src/main/java/net/rptools/maptool/client/ui/token/dialog/edit/TokenPropertiesDialog.java +++ b/src/main/java/net/rptools/maptool/client/ui/token/dialog/edit/TokenPropertiesDialog.java @@ -14,7 +14,6 @@ */ package net.rptools.maptool.client.ui.token.dialog.edit; -import java.awt.*; import javax.swing.*; import net.rptools.maptool.client.swing.htmleditorsplit.HtmlEditorSplit; diff --git a/src/main/java/net/rptools/maptool/model/Token.java b/src/main/java/net/rptools/maptool/model/Token.java index 49319b8f8e..a3febe6d5a 100644 --- a/src/main/java/net/rptools/maptool/model/Token.java +++ b/src/main/java/net/rptools/maptool/model/Token.java @@ -933,6 +933,10 @@ 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. diff --git a/src/main/java/net/rptools/maptool/util/LightSyntax.java b/src/main/java/net/rptools/maptool/util/LightSyntax.java new file mode 100644 index 0000000000..770ab217ad --- /dev/null +++ b/src/main/java/net/rptools/maptool/util/LightSyntax.java @@ -0,0 +1,395 @@ +/* + * This software Copyright by the RPTools.net development team, and + * licensed under the Affero GPL Version 3 or, at your option, any later + * version. + * + * MapTool Source Code is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * + * You should have received a copy of the GNU Affero General Public + * License * along with this source Code. If not, please visit + * and specifically the Affero license + * text at . + */ +package net.rptools.maptool.util; + +import java.awt.Color; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.LineNumberReader; +import java.io.StringReader; +import java.text.ParseException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import java.util.regex.Pattern; +import net.rptools.maptool.client.MapTool; +import net.rptools.maptool.language.I18N; +import net.rptools.maptool.model.GUID; +import net.rptools.maptool.model.Light; +import net.rptools.maptool.model.LightSource; +import net.rptools.maptool.model.ShapeType; +import net.rptools.maptool.model.drawing.DrawableColorPaint; + +public class LightSyntax { + private static final int DEFAULT_LUMENS = 100; + + public Map parseLights(String text, Iterable original) { + final var lightSourceMap = new HashMap(); + final var reader = new LineNumberReader(new BufferedReader(new StringReader(text))); + List errlog = new LinkedList<>(); + + try { + String line; + while ((line = reader.readLine()) != null) { + line = line.trim(); + + var source = parseLightLine(line, reader.getLineNumber(), original, errlog); + if (source != null) { + lightSourceMap.put(source.getId(), source); + } + } + } catch (IOException ioe) { + MapTool.showError("msg.error.mtprops.light.ioexception", ioe); + } + + if (!errlog.isEmpty()) { + MapTool.showFeedback(errlog.toArray()); + errlog.clear(); + throw new IllegalArgumentException( + "msg.error.mtprops.light.definition"); // Don't save lights... + } + + return lightSourceMap; + } + + public Map> parseCategorizedLights( + String text, final Map> originalLightSourcesMap) { + final var lightMap = new TreeMap>(); + final var reader = new LineNumberReader(new BufferedReader(new StringReader(text))); + List errlog = new LinkedList<>(); + + try { + Collection currentGroupOriginalLightSources = Collections.emptyList(); + Map lightSourceMap = null; + + String line; + while ((line = reader.readLine()) != null) { + line = line.trim(); + + // Blank lines + if (line.isEmpty()) { + lightSourceMap = null; + continue; + } + // New group + if (lightSourceMap == null) { + final var currentGroupName = line; + currentGroupOriginalLightSources = + originalLightSourcesMap + .getOrDefault(currentGroupName, Collections.emptyMap()) + .values(); + lightSourceMap = new HashMap<>(); + lightMap.put(currentGroupName, lightSourceMap); + continue; + } + + var source = + parseLightLine(line, reader.getLineNumber(), currentGroupOriginalLightSources, errlog); + if (source != null) { + lightSourceMap.put(source.getId(), source); + } + } + lightMap.values().removeIf(Map::isEmpty); + } catch (IOException ioe) { + MapTool.showError("msg.error.mtprops.light.ioexception", ioe); + } + + if (!errlog.isEmpty()) { + MapTool.showFeedback(errlog.toArray()); + errlog.clear(); + throw new IllegalArgumentException( + "msg.error.mtprops.light.definition"); // Don't save lights... + } + + return lightMap; + } + + public String stringifyLights(Iterable lights) { + StringBuilder builder = new StringBuilder(); + writeLightLines(builder, lights); + return builder.toString(); + } + + public String stringifyCategorizedLights(Map> lightSources) { + StringBuilder builder = new StringBuilder(); + for (Map.Entry> entry : lightSources.entrySet()) { + builder.append(entry.getKey()); + builder.append("\n----\n"); + + writeLightLines(builder, entry.getValue().values()); + builder.append('\n'); + } + return builder.toString(); + } + + private void writeLightLines(StringBuilder builder, Iterable lights) { + for (LightSource lightSource : lights) { + builder.append(lightSource.getName()).append(":"); + + if (lightSource.getType() != LightSource.Type.NORMAL) { + builder.append(' ').append(lightSource.getType().name().toLowerCase()); + } + if (lightSource.isScaleWithToken()) { + builder.append(" scale"); + } + + final var lastParameters = new LinkedHashMap(); + lastParameters.put("", null); + lastParameters.put("arc", 0.); + lastParameters.put("offset", 0.); + lastParameters.put("GM", false); + lastParameters.put("OWNER", false); + + for (Light light : lightSource.getLightList()) { + final var parameters = new HashMap<>(); + + // TODO: This HAS to change, the lights need to be auto describing, this hard wiring sucks + if (lightSource.getType() == LightSource.Type.AURA) { + parameters.put("GM", light.isGM()); + parameters.put("OWNER", light.isOwnerOnly()); + } + + parameters.put("", light.getShape().name().toLowerCase()); + switch (light.getShape()) { + default: + throw new RuntimeException( + "Unrecognized shape: " + light.getShape().toString().toLowerCase()); + case SQUARE, GRID, CIRCLE, HEX: + break; + case BEAM: + parameters.put("arc", light.getArcAngle()); + parameters.put("offset", light.getFacingOffset()); + break; + case CONE: + parameters.put("arc", light.getArcAngle()); + parameters.put("offset", light.getFacingOffset()); + break; + } + + for (final var parameterEntry : lastParameters.entrySet()) { + final var key = parameterEntry.getKey(); + final var oldValue = parameterEntry.getValue(); + final var newValue = parameters.get(key); + + if (newValue != null && !newValue.equals(oldValue)) { + lastParameters.put(key, newValue); + + // Special case: booleans are flags that are either present or not. + if (newValue instanceof Boolean b) { + if (b) { + builder.append(" ").append(key); + } + } else { + builder.append(" "); + if (!"".equals(key)) { + // Special case: don't include a key= for shapes. + builder.append(key).append("="); + } + builder.append( + switch (newValue) { + case Double d -> StringUtil.formatDecimal(d); + default -> newValue.toString(); + }); + } + } + } + + builder.append(' ').append(StringUtil.formatDecimal(light.getRadius())); + if (light.getPaint() instanceof DrawableColorPaint) { + Color color = (Color) light.getPaint().getPaint(); + builder.append(toHex(color)); + } + if (lightSource.getType() == LightSource.Type.NORMAL) { + final var lumens = light.getLumens(); + if (lumens != DEFAULT_LUMENS) { + if (lumens >= 0) { + builder.append('+'); + } + builder.append(Integer.toString(lumens, 10)); + } + } + } + builder.append('\n'); + } + } + + private LightSource parseLightLine( + String line, int lineNumber, Iterable originalInCategory, List errlog) { + // Blank lines, comments + if (line.isEmpty() || line.charAt(0) == '-') { + return null; + } + + // Item + int split = line.indexOf(':'); + if (split < 1) { + return null; + } + + // region Light source properties. + String name = line.substring(0, split).trim(); + GUID id = new GUID(); + LightSource.Type type = LightSource.Type.NORMAL; + boolean scaleWithToken = false; + List lights = new ArrayList<>(); + // endregion + // region Individual light properties + ShapeType shape = ShapeType.CIRCLE; // TODO: Make a preference for default shape + double arc = 0; + double offset = 0; + boolean gmOnly = false; + boolean ownerOnly = false; + String distance; + // endregion + + for (String arg : line.substring(split + 1).split("\\s+")) { + arg = arg.trim(); + if (arg.isEmpty()) { + continue; + } + if (arg.equalsIgnoreCase("GM")) { + gmOnly = true; + ownerOnly = false; + continue; + } + if (arg.equalsIgnoreCase("OWNER")) { + gmOnly = false; + ownerOnly = true; + continue; + } + // Scale with token designation + if (arg.equalsIgnoreCase("SCALE")) { + scaleWithToken = true; + continue; + } + // Shape designation ? + try { + shape = ShapeType.valueOf(arg.toUpperCase()); + arc = shape == ShapeType.BEAM ? 4 : arc; + continue; + } catch (IllegalArgumentException iae) { + // Expected when not defining a shape + } + + // Type designation ? + try { + type = LightSource.Type.valueOf(arg.toUpperCase()); + continue; + } catch (IllegalArgumentException iae) { + // Expected when not defining a shape + } + + // Facing offset designation + if (arg.toUpperCase().startsWith("OFFSET=")) { + try { + offset = Integer.parseInt(arg.substring(7)); + continue; + } catch (NullPointerException noe) { + errlog.add(I18N.getText("msg.error.mtprops.light.offset", lineNumber, arg)); + } + } + + // Parameters + split = arg.indexOf('='); + if (split > 0) { + String key = arg.substring(0, split); + String value = arg.substring(split + 1); + + // TODO: Make this a generic map to pass instead of 'arc' + if ("arc".equalsIgnoreCase(key)) { + try { + arc = StringUtil.parseDecimal(value); + shape = + (shape != ShapeType.CONE && shape != ShapeType.BEAM) + ? ShapeType.CONE + : shape; // If the user specifies an arc, force the shape to CONE + } catch (ParseException pe) { + errlog.add(I18N.getText("msg.error.mtprops.light.arc", lineNumber, value)); + } + } + continue; + } + + Color color = null; + int perRangeLumens = DEFAULT_LUMENS; + distance = arg; + + final var rangeRegex = Pattern.compile("([^#+-]*)(#[0-9a-fA-F]+)?([+-]\\d*)?"); + final var matcher = rangeRegex.matcher(arg); + if (matcher.find()) { + distance = matcher.group(1); + final var colorString = matcher.group(2); + final var lumensString = matcher.group(3); + // Note that Color.decode() _wants_ the leading "#", otherwise it might not treat the + // value as a hex code. + if (colorString != null) { + color = Color.decode(colorString); + } + if (lumensString != null) { + perRangeLumens = Integer.parseInt(lumensString, 10); + if (perRangeLumens == 0) { + errlog.add(I18N.getText("msg.error.mtprops.light.zerolumens", lineNumber)); + perRangeLumens = DEFAULT_LUMENS; + } + } + } + + boolean isAura = type == LightSource.Type.AURA; + if (!isAura && (gmOnly || ownerOnly)) { + errlog.add(I18N.getText("msg.error.mtprops.light.gmOrOwner", lineNumber)); + gmOnly = false; + ownerOnly = false; + } + ownerOnly = !gmOnly && ownerOnly; + try { + Light t = + new Light( + shape, + offset, + StringUtil.parseDecimal(distance), + arc, + color == null ? null : new DrawableColorPaint(color), + perRangeLumens, + gmOnly, + ownerOnly); + lights.add(t); + } catch (ParseException pe) { + errlog.add(I18N.getText("msg.error.mtprops.light.distance", lineNumber, distance)); + } + } + + // Keep ID the same if modifying existing light. This avoids tokens losing their lights when + // the light definition is modified. + for (LightSource ls : originalInCategory) { + if (name.equalsIgnoreCase(ls.getName())) { + assert ls.getId() != null; + id = ls.getId(); + break; + } + } + + return LightSource.createRegular(name, id, type, scaleWithToken, lights); + } + + private String toHex(Color color) { + return String.format("#%06x", color.getRGB() & 0x00FFFFFF); + } +} diff --git a/src/main/java/net/rptools/maptool/util/SightSyntax.java b/src/main/java/net/rptools/maptool/util/SightSyntax.java new file mode 100644 index 0000000000..c36a9ca7aa --- /dev/null +++ b/src/main/java/net/rptools/maptool/util/SightSyntax.java @@ -0,0 +1,271 @@ +/* + * This software Copyright by the RPTools.net development team, and + * licensed under the Affero GPL Version 3 or, at your option, any later + * version. + * + * MapTool Source Code is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * + * You should have received a copy of the GNU Affero General Public + * License * along with this source Code. If not, please visit + * and specifically the Affero license + * text at . + */ +package net.rptools.maptool.util; + +import java.awt.Color; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.LineNumberReader; +import java.io.StringReader; +import java.text.ParseException; +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.regex.Pattern; +import net.rptools.maptool.client.MapTool; +import net.rptools.maptool.language.I18N; +import net.rptools.maptool.model.Light; +import net.rptools.maptool.model.LightSource; +import net.rptools.maptool.model.ShapeType; +import net.rptools.maptool.model.SightType; +import net.rptools.maptool.model.drawing.DrawableColorPaint; + +public class SightSyntax { + private static final int DEFAULT_LUMENS = 100; + + public List parse(String text) { + final var sightList = new LinkedList(); + final var reader = new LineNumberReader(new BufferedReader(new StringReader(text))); + String line; + String toBeParsed = null, errmsg = null; + List errlog = new LinkedList<>(); + try { + while ((line = reader.readLine()) != null) { + line = line.trim(); + + // Blanks + if (line.isEmpty() || line.indexOf(':') < 1) { + continue; + } + // Parse line + int split = line.indexOf(':'); + String label = line.substring(0, split).trim(); + String value = line.substring(split + 1).trim(); + + if (label.isEmpty()) { + continue; + } + // Parse Details + double magnifier = 1; + // If null, no personal light has been defined. + List personalLightLights = null; + + String[] args = value.split("\\s+"); + ShapeType shape = ShapeType.CIRCLE; + boolean scaleWithToken = false; + int arc = 90; + float range = 0; + int offset = 0; + + for (String arg : args) { + assert !arg.isEmpty(); // The split() uses "one or more spaces", removing empty strings + try { + shape = ShapeType.valueOf(arg.toUpperCase()); + arc = shape == ShapeType.BEAM ? 4 : arc; + continue; + } catch (IllegalArgumentException iae) { + // Expected when not defining a shape + } + // Scale with Token + if (arg.equalsIgnoreCase("SCALE")) { + scaleWithToken = true; + continue; + } + try { + + if (arg.startsWith("x")) { + toBeParsed = arg.substring(1); // Used in the catch block, below + errmsg = "msg.error.mtprops.sight.multiplier"; // (ditto) + magnifier = StringUtil.parseDecimal(toBeParsed); + } else if (arg.startsWith("r")) { // XXX Why not "r=#" instead of "r#"?? + toBeParsed = arg.substring(1); + errmsg = "msg.error.mtprops.sight.range"; + + final var rangeRegex = Pattern.compile("([^#+-]*)(#[0-9a-fA-F]+)?([+-]\\d*)?"); + final var matcher = rangeRegex.matcher(toBeParsed); + if (matcher.find()) { + var pLightRange = 0.; + pLightRange = StringUtil.parseDecimal(matcher.group(1)); + final var colorString = matcher.group(2); + final var lumensString = matcher.group(3); + // Note that Color.decode() _wants_ the leading "#", otherwise it might not treat + // the value as a hex code. + Color personalLightColor = null; + if (colorString != null) { + personalLightColor = Color.decode(colorString); + } + int perRangeLumens = DEFAULT_LUMENS; + if (lumensString != null) { + perRangeLumens = Integer.parseInt(lumensString, 10); + if (perRangeLumens == 0) { + errlog.add( + I18N.getText("msg.error.mtprops.sight.zerolumens", reader.getLineNumber())); + perRangeLumens = DEFAULT_LUMENS; + } + } + + if (personalLightLights == null) { + personalLightLights = new ArrayList<>(); + } + personalLightLights.add( + new Light( + shape, + 0, + pLightRange, + arc, + personalLightColor == null + ? null + : new DrawableColorPaint(personalLightColor), + perRangeLumens, + false, + false)); + } else { + throw new ParseException( + String.format("Unrecognized personal light syntax: %s", arg), 0); + } + } else if (arg.startsWith("arc=") && arg.length() > 4) { + toBeParsed = arg.substring(4); + errmsg = "msg.error.mtprops.sight.arc"; + arc = StringUtil.parseInteger(toBeParsed); + } else if (arg.startsWith("distance=") && arg.length() > 9) { + toBeParsed = arg.substring(9); + errmsg = "msg.error.mtprops.sight.distance"; + range = StringUtil.parseDecimal(toBeParsed).floatValue(); + } else if (arg.startsWith("offset=") && arg.length() > 7) { + toBeParsed = arg.substring(7); + errmsg = "msg.error.mtprops.sight.offset"; + offset = StringUtil.parseInteger(toBeParsed); + } else { + toBeParsed = arg; + errmsg = + I18N.getText( + "msg.error.mtprops.sight.unknownField", reader.getLineNumber(), toBeParsed); + errlog.add(errmsg); + } + } catch (ParseException e) { + assert errmsg != null; + errlog.add(I18N.getText(errmsg, reader.getLineNumber(), toBeParsed)); + } + } + + LightSource personalLight = + personalLightLights == null + ? null + : LightSource.createPersonal(scaleWithToken, personalLightLights); + SightType sight = + new SightType(label, magnifier, personalLight, shape, arc, scaleWithToken); + sight.setDistance(range); + sight.setOffset(offset); + + // Store + sightList.add(sight); + } + } catch (IOException ioe) { + MapTool.showError("msg.error.mtprops.sight.ioexception", ioe); + } + if (!errlog.isEmpty()) { + // Show the user a list of errors so they can (attempt to) correct all of them at once + MapTool.showFeedback(errlog.toArray()); + errlog.clear(); + throw new IllegalArgumentException( + "msg.error.mtprops.sight.definition"); // Don't save sights... + } + + return sightList; + } + + public String stringify(Map sightTypeMap) { + StringBuilder builder = new StringBuilder(); + for (SightType sight : sightTypeMap.values()) { + builder.append(sight.getName()).append(": "); + + builder.append(sight.getShape().name().toLowerCase()).append(" "); + + switch (sight.getShape()) { + case SQUARE, CIRCLE, GRID, HEX: + break; + case BEAM: + if (sight.getArc() != 0) { + builder.append("arc=").append(StringUtil.formatDecimal(sight.getArc())).append(' '); + } else { + builder.append("arc=4").append(StringUtil.formatDecimal(sight.getArc())).append(' '); + } + if (sight.getOffset() != 0) { + builder + .append("offset=") + .append(StringUtil.formatDecimal(sight.getOffset())) + .append(' '); + } + break; + case CONE: + if (sight.getArc() != 0) { + builder.append("arc=").append(StringUtil.formatDecimal(sight.getArc())).append(' '); + } + if (sight.getOffset() != 0) { + builder + .append("offset=") + .append(StringUtil.formatDecimal(sight.getOffset())) + .append(' '); + } + break; + } + if (sight.getDistance() != 0) { + builder + .append("distance=") + .append(StringUtil.formatDecimal(sight.getDistance())) + .append(' '); + } + + // Scale with Token + if (sight.isScaleWithToken()) { + builder.append("scale "); + } + // Multiplier + if (sight.getMultiplier() != 1 && sight.getMultiplier() != 0) { + builder.append("x").append(StringUtil.formatDecimal(sight.getMultiplier())).append(' '); + } + // Personal light + if (sight.getPersonalLightSource() != null) { + LightSource source = sight.getPersonalLightSource(); + + for (Light light : source.getLightList()) { + double range = light.getRadius(); + + builder.append("r").append(StringUtil.formatDecimal(range)); + + if (light.getPaint() != null && light.getPaint() instanceof DrawableColorPaint) { + Color color = (Color) light.getPaint().getPaint(); + builder.append(toHex(color)); + } + final var lumens = light.getLumens(); + if (lumens != DEFAULT_LUMENS) { + if (lumens >= 0) { + builder.append('+'); + } + builder.append(Integer.toString(lumens, 10)); + } + builder.append(' '); + } + } + builder.append('\n'); + } + return builder.toString(); + } + + private static String toHex(Color color) { + return String.format("#%06x", color.getRGB() & 0x00FFFFFF); + } +} diff --git a/src/main/resources/net/rptools/maptool/language/i18n.properties b/src/main/resources/net/rptools/maptool/language/i18n.properties index 71c5a83f8b..d52fc35d3a 100644 --- a/src/main/resources/net/rptools/maptool/language/i18n.properties +++ b/src/main/resources/net/rptools/maptool/language/i18n.properties @@ -276,6 +276,7 @@ EditTokenDialog.label.sight.has = Has Sight: EditTokenDialog.label.terrain.mod = Terrain Modifier: EditTokenDialog.label.terrain.mod.tooltip= Adjust the cost of movement other tokens will may to move over or through this token. Default multiplier of 1 equals no change (1 * 1 = 1). EditTokenDialog.label.image = Image Table: +EditTokenDialog.label.uniqueLightSources = Unique Light Sources: EditTokenDialog.label.opacity = Token Opacity: EditTokenDialog.label.opacity.tooltip = Change the opacity of the token. EditTokenDialog.label.opacity.100 = 100%