From fd865ca0371ea1dc2a379e1681058920b2c5315d Mon Sep 17 00:00:00 2001 From: Magnus Ulf Date: Sun, 19 Jan 2020 14:43:20 +0100 Subject: [PATCH] Add Dynmap support --- pom.xml | 5 + src/com/massivecraft/factions/Factions.java | 4 +- .../massivecraft/factions/entity/MConf.java | 77 ++ .../integration/dynmap/AreaMarkerValues.java | 211 ++++++ .../integration/dynmap/DynmapStyle.java | 90 +++ .../integration/dynmap/EngineDynmap.java | 691 ++++++++++++++++++ .../integration/dynmap/IntegrationDynmap.java | 55 ++ .../integration/dynmap/LayerValues.java | 107 +++ .../integration/dynmap/MarkerValues.java | 135 ++++ 9 files changed, 1374 insertions(+), 1 deletion(-) create mode 100644 src/com/massivecraft/factions/integration/dynmap/AreaMarkerValues.java create mode 100644 src/com/massivecraft/factions/integration/dynmap/DynmapStyle.java create mode 100644 src/com/massivecraft/factions/integration/dynmap/EngineDynmap.java create mode 100644 src/com/massivecraft/factions/integration/dynmap/IntegrationDynmap.java create mode 100644 src/com/massivecraft/factions/integration/dynmap/LayerValues.java create mode 100644 src/com/massivecraft/factions/integration/dynmap/MarkerValues.java diff --git a/pom.xml b/pom.xml index f329a2f5..5bcb7991 100644 --- a/pom.xml +++ b/pom.xml @@ -69,6 +69,11 @@ mineverse.Aust1n46.chat MineverseChat + + + us.dynmap + dynmap + diff --git a/src/com/massivecraft/factions/Factions.java b/src/com/massivecraft/factions/Factions.java index fb3c4267..83f8ceeb 100644 --- a/src/com/massivecraft/factions/Factions.java +++ b/src/com/massivecraft/factions/Factions.java @@ -66,6 +66,7 @@ import com.massivecraft.factions.entity.migrator.MigratorMPlayer001Ranks; import com.massivecraft.factions.entity.migrator.MigratorMPlayer002UsingAdminMode; import com.massivecraft.factions.entity.migrator.MigratorTerritoryAccess001Restructure; import com.massivecraft.factions.event.EventFactionsChunkChangeType; +import com.massivecraft.factions.integration.dynmap.IntegrationDynmap; import com.massivecraft.factions.integration.lwc.IntegrationLwc; import com.massivecraft.factions.integration.placeholderapi.IntegrationPlaceholderAPI; import com.massivecraft.factions.integration.venturechat.IntegrationVentureChat; @@ -195,7 +196,8 @@ public class Factions extends MassivePlugin IntegrationPlaceholderAPI.class, IntegrationVentureChat.class, IntegrationLwc.class, - IntegrationWorldGuard.class + IntegrationWorldGuard.class, + IntegrationDynmap.class ); } diff --git a/src/com/massivecraft/factions/entity/MConf.java b/src/com/massivecraft/factions/entity/MConf.java index 5a38bc8e..a6c364fb 100644 --- a/src/com/massivecraft/factions/entity/MConf.java +++ b/src/com/massivecraft/factions/entity/MConf.java @@ -4,12 +4,15 @@ import com.massivecraft.factions.Factions; import com.massivecraft.factions.Rel; import com.massivecraft.factions.engine.EngineChat; import com.massivecraft.factions.event.EventFactionsChunkChangeType; +import com.massivecraft.factions.integration.dynmap.DynmapStyle; +import com.massivecraft.factions.integration.dynmap.IntegrationDynmap; import com.massivecraft.massivecore.collections.BackstringSet; import com.massivecraft.massivecore.collections.MassiveSet; import com.massivecraft.massivecore.collections.WorldExceptionSet; import com.massivecraft.massivecore.command.editor.annotation.EditorName; import com.massivecraft.massivecore.command.editor.annotation.EditorType; import com.massivecraft.massivecore.command.editor.annotation.EditorTypeInner; +import com.massivecraft.massivecore.command.editor.annotation.EditorVisible; import com.massivecraft.massivecore.command.type.TypeMillisDiff; import com.massivecraft.massivecore.store.Entity; import com.massivecraft.massivecore.util.MUtil; @@ -670,4 +673,78 @@ public class MConf extends Entity public boolean useNewMoneySystem = false; + // -------------------------------------------- // + // INTEGRATION: DYNMAP + // -------------------------------------------- // + + // Should the dynmap intagration be used? + public boolean dynmapEnabled = true; + + // Should the dynmap updates be logged to console output? + public boolean dynmapLogTimeSpent = false; + + // Name of the Factions layer + public String dynmapLayerName = "Factions"; + + // Should the layer be visible per default + public boolean dynmapLayerHiddenByDefault = false; + + // Ordering priority in layer menu (low goes before high - default is 0) + public int dynmapLayerPriority = 2; + + // (optional) set minimum zoom level before layer is visible (0 = defalt, always visible) + public int dynmapLayerMinimumZoom = 0; + + // Format for popup - substitute values for macros + //public String dynmapInfowindowFormat = "
%regionname%
Flags
%flags%
"; + public String dynmapFactionDescription = + "
\n" + + "%name%
\n" + + "%description%
\n" + + "
\n" + + "Leader: %players.leader%
\n" + + "Members: %players%
\n" + + "
\n" + + "Age: %age%
\n" + + "Bank: %money%
\n" + + "
\n" + + "Flags:
\n" + + "%flags.table3%\n" + + "
"; + + // Enable the %money% macro. Only do this if you know your economy manager is thread safe. + public boolean dynmapShowMoneyInDescription = false; + + // Allow players in faction to see one another on Dynmap (only relevant if Dynmap has 'player-info-protected' enabled) + //public boolean dynmapVisibilityByFaction = true; + + // Optional setting to limit which regions to show. + // If empty all regions are shown. + // Specify Faction either by name or UUID. + // To show all regions on a given world, add 'world:' to the list. + public Set dynmapVisibleFactions = new MassiveSet<>(); + + // Optional setting to hide specific Factions. + // Specify Faction either by name or UUID. + // To hide all regions on a given world, add 'world:' to the list. + public Set dynmapHiddenFactions = new MassiveSet<>(); + + @EditorVisible(false) + public DynmapStyle dynmapDefaultStyle = new DynmapStyle( + IntegrationDynmap.DYNMAP_STYLE_LINE_COLOR, + IntegrationDynmap.DYNMAP_STYLE_LINE_OPACITY, + IntegrationDynmap.DYNMAP_STYLE_LINE_WEIGHT, + IntegrationDynmap.DYNMAP_STYLE_FILL_COLOR, + IntegrationDynmap.DYNMAP_STYLE_FILL_OPACITY, + IntegrationDynmap.DYNMAP_STYLE_HOME_MARKER, + IntegrationDynmap.DYNMAP_STYLE_BOOST + ); + + // Optional per Faction style overrides. Any defined replace those in dynmapDefaultStyle. + // Specify Faction either by name or UUID. + @EditorVisible(false) + public Map dynmapFactionStyles = MUtil.map( + "SafeZone", new DynmapStyle().withLineColor("#FF00FF").withFillColor("#FF00FF").withBoost(false), + "WarZone", new DynmapStyle().withLineColor("#FF0000").withFillColor("#FF0000").withBoost(false) + ); } diff --git a/src/com/massivecraft/factions/integration/dynmap/AreaMarkerValues.java b/src/com/massivecraft/factions/integration/dynmap/AreaMarkerValues.java new file mode 100644 index 00000000..b85df912 --- /dev/null +++ b/src/com/massivecraft/factions/integration/dynmap/AreaMarkerValues.java @@ -0,0 +1,211 @@ +package com.massivecraft.factions.integration.dynmap; + +import com.massivecraft.massivecore.util.MUtil; +import org.dynmap.markers.AreaMarker; +import org.dynmap.markers.MarkerAPI; +import org.dynmap.markers.MarkerSet; + +public class AreaMarkerValues +{ + // -------------------------------------------- // + // FIELDS + // -------------------------------------------- // + + private final String label; + public String getLabel() { return label; } + public AreaMarkerValues withLabel(String label) { return new AreaMarkerValues(label, world, x, z, description, lineColor, lineOpacity, lineWeight, fillColor, fillOpacity, boost); } + + private final String world; + public String getWorld() { return world; } + public AreaMarkerValues withWorld(String world) { return new AreaMarkerValues(label, world, x, z, description, lineColor, lineOpacity, lineWeight, fillColor, fillOpacity, boost); } + + private final double[] x; + public double[] getX() { return x; } + public AreaMarkerValues withX(double[] x) { return new AreaMarkerValues(label, world, x, z, description, lineColor, lineOpacity, lineWeight, fillColor, fillOpacity, boost); } + + private final double[] z; + public double[] getZ() { return z; } + public AreaMarkerValues withZ(double[] z) { return new AreaMarkerValues(label, world, x, z, description, lineColor, lineOpacity, lineWeight, fillColor, fillOpacity, boost); } + + private final String description; + public String getDescription() { return description; } + public AreaMarkerValues withDescription(String description) { return new AreaMarkerValues(label, world, x, z, description, lineColor, lineOpacity, lineWeight, fillColor, fillOpacity, boost); } + + private final int lineColor; + public int getLineColor() { return lineColor; } + public AreaMarkerValues withLineColor(int lineColor) { return new AreaMarkerValues(label, world, x, z, description, lineColor, lineOpacity, lineWeight, fillColor, fillOpacity, boost); } + + private final double lineOpacity; + public double getLineOpacity() { return lineOpacity; } + public AreaMarkerValues withLineOpacity(double lineOpacity) { return new AreaMarkerValues(label, world, x, z, description, lineColor, lineOpacity, lineWeight, fillColor, fillOpacity, boost); } + + private final int lineWeight; + public int getLineWeight() { return lineWeight; } + public AreaMarkerValues withLineWright(int lineWeight) { return new AreaMarkerValues(label, world, x, z, description, lineColor, lineOpacity, lineWeight, fillColor, fillOpacity, boost); } + + private final int fillColor; + public int getFillColor() { return fillColor; } + public AreaMarkerValues withFillColor(int fillColor) { return new AreaMarkerValues(label, world, x, z, description, lineColor, lineOpacity, lineWeight, fillColor, fillOpacity, boost); } + + private final double fillOpacity; + public double getFillOpacity() { return fillOpacity; } + public AreaMarkerValues withFillOpacity(double fillOpacity) { return new AreaMarkerValues(label, world, x, z, description, lineColor, lineOpacity, lineWeight, fillColor, fillOpacity, boost); } + + private final boolean boost; + public boolean isBoost() { return boost; } + public AreaMarkerValues withBoost(boolean boost) { return new AreaMarkerValues(label, world, x, z, description, lineColor, lineOpacity, lineWeight, fillColor, fillOpacity, boost); } + + public AreaMarkerValues withStyle(DynmapStyle style) + { + return new AreaMarkerValues(label, world, x, z, description, style); + } + + // -------------------------------------------- // + // CONSTRUCTOR + // -------------------------------------------- // + + public AreaMarkerValues(String label, String world, double[] x, double[] z, String description, DynmapStyle style) + { + this(label, world, x, z, description, style.getLineColor(), style.getLineOpacity(), style.getLineWeight(), style.getFillColor(), style.getFillOpacity(), style.getBoost()); + } + + public AreaMarkerValues(String label, String world, double[] x, double[] z, String description, int lineColor, double lineOpacity, int lineWeight, int fillColor, double fillOpacity, boolean boost) + { + this.label = label; + this.world = world; + this.x = x; + this.z = z; + this.description = description; + this.lineColor = lineColor; + this.lineOpacity = lineOpacity; + this.lineWeight = lineWeight; + this.fillColor = fillColor; + this.fillOpacity = fillOpacity; + this.boost = boost; + } + + // -------------------------------------------- // + // MASTER + // -------------------------------------------- // + + public AreaMarker ensureExistsAndUpdated(AreaMarker areaMarker, MarkerAPI markerApi, MarkerSet markerset, String markerId) + { + // NOTE: I remove from the map created just in the beginning of this method. + // NOTE: That way what is left at the end will be outdated markers to remove. + if (areaMarker == null) + { + areaMarker = create(markerApi, markerset, markerId); + } + else + { + update(markerApi, markerset, areaMarker); + } + + if (areaMarker == null) + { + EngineDynmap.logSevere("Could not get/create the area marker " + markerId); + } + + return areaMarker; + } + + // -------------------------------------------- // + // CREATE + // -------------------------------------------- // + + public AreaMarker create(MarkerAPI markerApi, MarkerSet markerset, String markerId) + { + AreaMarker ret = markerset.createAreaMarker( + markerId, + this.getLabel(), + false, + this.getWorld(), + this.getX(), + this.getZ(), + false // not persistent + ); + + if (ret == null) return null; + + // Description + ret.setDescription(this.getDescription()); + + // Line Style + ret.setLineStyle(this.getLineWeight(), this.getLineOpacity(), this.getLineColor()); + + // Fill Style + ret.setFillStyle(this.getFillOpacity(), this.getFillColor()); + + // Boost Flag + ret.setBoostFlag(this.isBoost()); + + return ret; + } + + // -------------------------------------------- // + // UPDATE + // -------------------------------------------- // + + public void update(MarkerAPI markerApi, MarkerSet markerset, AreaMarker marker) + { + // Corner Locations + if (!equals(marker, this.x, this.z)) + { + marker.setCornerLocations(this.x, this.z); + } + + // Label + MUtil.setIfDifferent(this.getLabel(), marker::getLabel, marker::setLabel); + + // Description + MUtil.setIfDifferent(this.getDescription(), marker::getDescription, marker::setDescription); + + // Line Style + if + ( + !MUtil.equals(marker.getLineWeight(), this.lineWeight) + || + !MUtil.equals(marker.getLineOpacity(), this.lineOpacity) + || + !MUtil.equals(marker.getLineColor(), this.lineColor) + ) + { + marker.setLineStyle(this.lineWeight, this.lineOpacity, this.lineColor); + } + + // Fill Style + if + ( + !MUtil.equals(marker.getFillOpacity(), this.fillOpacity) + || + !MUtil.equals(marker.getFillColor(), this.fillColor) + ) + { + marker.setFillStyle(this.fillOpacity, this.fillColor); + } + + // Boost Flag + MUtil.setIfDifferent(this.isBoost(), marker::getBoostFlag, marker::setBoostFlag); + } + + // -------------------------------------------- // + // UTIL + // -------------------------------------------- // + + public static boolean equals(AreaMarker marker, double[] x, double[] z) + { + int length = marker.getCornerCount(); + + if (x.length != length) return false; + if (z.length != length) return false; + + for (int i = 0; i < length; i++) + { + if (marker.getCornerX(i) != x[i]) return false; + if (marker.getCornerZ(i) != z[i]) return false; + } + + return true; + } + +} \ No newline at end of file diff --git a/src/com/massivecraft/factions/integration/dynmap/DynmapStyle.java b/src/com/massivecraft/factions/integration/dynmap/DynmapStyle.java new file mode 100644 index 00000000..743224c0 --- /dev/null +++ b/src/com/massivecraft/factions/integration/dynmap/DynmapStyle.java @@ -0,0 +1,90 @@ +package com.massivecraft.factions.integration.dynmap; + +import com.massivecraft.factions.entity.MConf; + +public class DynmapStyle +{ + // -------------------------------------------- // + // FIELDS + // -------------------------------------------- // + + public final String lineColor; + public int getLineColor() { return getColor(coalesce(this.lineColor, MConf.get().dynmapDefaultStyle.lineColor, IntegrationDynmap.DYNMAP_STYLE_LINE_COLOR)); } + public DynmapStyle withLineColor(String lineColor) { return new DynmapStyle(lineColor, lineOpacity, lineWeight, fillColor, fillOpacity, homeMarker, boost); } + + public final Double lineOpacity; + public double getLineOpacity() { return coalesce(this.lineOpacity, MConf.get().dynmapDefaultStyle.lineOpacity, IntegrationDynmap.DYNMAP_STYLE_LINE_OPACITY); } + public DynmapStyle withLineOpacity(Double lineOpacity) { return new DynmapStyle(lineColor, lineOpacity, lineWeight, fillColor, fillOpacity, homeMarker, boost); } + + public final Integer lineWeight; + public int getLineWeight() { return coalesce(this.lineWeight, MConf.get().dynmapDefaultStyle.lineWeight, IntegrationDynmap.DYNMAP_STYLE_LINE_WEIGHT); } + public DynmapStyle withLineWeight(Integer lineWeight) { return new DynmapStyle(lineColor, lineOpacity, lineWeight, fillColor, fillOpacity, homeMarker, boost); } + + public final String fillColor; + public int getFillColor() { return getColor(coalesce(this.fillColor, MConf.get().dynmapDefaultStyle.fillColor, IntegrationDynmap.DYNMAP_STYLE_FILL_COLOR)); } + public DynmapStyle withFillColor(String fillColor) { return new DynmapStyle(lineColor, lineOpacity, lineWeight, fillColor, fillOpacity, homeMarker, boost); } + + public final Double fillOpacity; + public double getFillOpacity() { return coalesce(this.fillOpacity, MConf.get().dynmapDefaultStyle.fillOpacity, IntegrationDynmap.DYNMAP_STYLE_FILL_OPACITY); } + public DynmapStyle withFillOpacity(Double fillOpacity) { return new DynmapStyle(lineColor, lineOpacity, lineWeight, fillColor, fillOpacity, homeMarker, boost); } + + // NOTE: We just return the string here. We do not return the resolved Dynmap MarkerIcon object. + // The reason is we use this class in the MConf. For serialization to work Dynmap would have to be loaded and we can't require that. + // Using dynmap is optional. + public final String homeMarker; + public String getHomeMarker() { return coalesce(this.homeMarker, MConf.get().dynmapDefaultStyle.homeMarker, IntegrationDynmap.DYNMAP_STYLE_HOME_MARKER); } + public DynmapStyle withHomeMarker(String homeMarker) { return new DynmapStyle(lineColor, lineOpacity, lineWeight, fillColor, fillOpacity, homeMarker, boost); } + + public final Boolean boost; + public boolean getBoost() { return coalesce(this.boost, MConf.get().dynmapDefaultStyle.boost, IntegrationDynmap.DYNMAP_STYLE_BOOST); } + public DynmapStyle withBoost(Boolean boost) { return new DynmapStyle(lineColor, lineOpacity, lineWeight, fillColor, fillOpacity, homeMarker, boost); } + + // -------------------------------------------- // + // CONSTRUCTOR + // -------------------------------------------- // + + public DynmapStyle() + { + this(null, null, null, null, null, null, null); + } + + public DynmapStyle(String lineColor, Double lineOpacity, Integer lineWeight, String fillColor, Double fillOpacity, String homeMarker, Boolean boost) + { + this.lineColor = lineColor; + this.lineOpacity = lineOpacity; + this.lineWeight = lineWeight; + this.fillColor = fillColor; + this.fillOpacity = fillOpacity; + this.homeMarker = homeMarker; + this.boost = boost; + } + + // -------------------------------------------- // + // UTIL + // -------------------------------------------- // + + @SafeVarargs + public static T coalesce(T... items) + { + for (T item : items) + { + if (item != null) return item; + } + return null; + } + + public static int getColor(String string) + { + int ret = 0x00FF00; + try + { + ret = Integer.parseInt(string.substring(1), 16); + } + catch (NumberFormatException nfx) + { + + } + return ret; + } + +} \ No newline at end of file diff --git a/src/com/massivecraft/factions/integration/dynmap/EngineDynmap.java b/src/com/massivecraft/factions/integration/dynmap/EngineDynmap.java new file mode 100644 index 00000000..47e41f5c --- /dev/null +++ b/src/com/massivecraft/factions/integration/dynmap/EngineDynmap.java @@ -0,0 +1,691 @@ +package com.massivecraft.factions.integration.dynmap; + +import com.massivecraft.factions.Factions; +import com.massivecraft.factions.entity.BoardColl; +import com.massivecraft.factions.entity.Faction; +import com.massivecraft.factions.entity.MConf; +import com.massivecraft.factions.entity.MFlag; +import com.massivecraft.factions.entity.MPlayer; +import com.massivecraft.factions.integration.Econ; +import com.massivecraft.massivecore.Engine; +import com.massivecraft.massivecore.collections.MassiveList; +import com.massivecraft.massivecore.collections.MassiveMap; +import com.massivecraft.massivecore.collections.MassiveSet; +import com.massivecraft.massivecore.money.Money; +import com.massivecraft.massivecore.ps.PS; +import com.massivecraft.massivecore.util.MUtil; +import com.massivecraft.massivecore.util.TimeDiffUtil; +import com.massivecraft.massivecore.util.TimeUnit; +import com.massivecraft.massivecore.util.Txt; +import org.apache.commons.lang.StringEscapeUtils; +import org.bukkit.Bukkit; +import org.bukkit.ChatColor; +import org.dynmap.DynmapAPI; +import org.dynmap.markers.AreaMarker; +import org.dynmap.markers.MarkerAPI; +import org.dynmap.markers.MarkerSet; + +import java.util.ArrayDeque; +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class EngineDynmap extends Engine +{ + // -------------------------------------------- // + // INSTANCE & CONSTRUCT + // -------------------------------------------- // + + private static EngineDynmap i = new EngineDynmap(); + public static EngineDynmap get() { return i; } + private EngineDynmap() + { + // Async + this.setSync(false); + + // Every 15 seconds + this.setPeriod(15 * 20L); + } + + // -------------------------------------------- // + // FIELDS + // -------------------------------------------- // + + private DynmapAPI dynmapApi; + private MarkerAPI markerApi; + private MarkerSet markerset; + + // -------------------------------------------- // + // RUN: UPDATE + // -------------------------------------------- // + + // Thread Safe / Asynchronous: Yes + @Override + public void run() + { + // Is Dynmap enabled? + if (MConf.get().dynmapEnabled) + { + this.perform(); + } + else + { + this.disable(); + } + } + + public void perform() + { + long before = System.currentTimeMillis(); + + // We do what we can here. + // You /can/ run this method from the main server thread but it's not recommended at all. + // This method is supposed to be run async to avoid locking the main server thread. + //final Map homes = createHomes(); + final Map areas = createAreas(); + + logTimeSpent("Async", before); + + // Shedule non thread safe sync at the end! + Bukkit.getScheduler().scheduleSyncDelayedTask(Factions.get(), () -> this.updateFactionsDynmap(areas)); + } + + public void updateFactionsDynmap(Map areas) + { + long before = System.currentTimeMillis(); + + if (!Bukkit.isPrimaryThread()) throw new IllegalStateException("async"); + + if (!fetchDynmapAPI()) return; + + // createLayer() is thread safe but it makes use of fields set in fetchDynmapAPI() so we must have it after. + if (!updateLayer(createLayer())) return; + + updateAreas(areas); + + logTimeSpent("Sync", before); + } + + public void disable() + { + if (this.markerset != null) + { + this.markerset.deleteMarkerSet(); + this.markerset = null; + } + } + + // Thread Safe / Asynchronous: Yes + public static void logTimeSpent(String name, long start) + { + if (!MConf.get().dynmapLogTimeSpent) return; + long end = System.currentTimeMillis(); + long duration = end-start; + + String message = Txt.parse("Dynmap %s took %dms.", name, duration); + Factions.get().log(message); + } + + // -------------------------------------------- // + // API + // -------------------------------------------- // + + // Thread Safe / Asynchronous: No + public boolean fetchDynmapAPI() + { + // Get DynmapAPI + this.dynmapApi = (DynmapAPI) Bukkit.getPluginManager().getPlugin("dynmap"); + if (this.dynmapApi == null) + { + logSevere("Could not access the DynmapAPI."); + return false; + } + + // Get MarkerAPI + this.markerApi = this.dynmapApi.getMarkerAPI(); + if (this.markerApi == null) + { + logSevere("Could not access the MarkerAPI."); + return false; + } + + return true; + } + + // -------------------------------------------- // + // UPDATE: Layer + // -------------------------------------------- // + + // Thread Safe / Asynchronous: Yes + public LayerValues createLayer() + { + return new LayerValues( + MConf.get().dynmapLayerName, + MConf.get().dynmapLayerMinimumZoom, + MConf.get().dynmapLayerPriority, + MConf.get().dynmapLayerHiddenByDefault + ); + } + + // Thread Safe / Asynchronous: No + public boolean updateLayer(LayerValues temp) + { + this.markerset = temp.ensureExistsAndUpdated(this.markerApi, IntegrationDynmap.FACTIONS_MARKERSET); + return this.markerset != null; + } + + // -------------------------------------------- // + // UPDATE: AREAS + // -------------------------------------------- // + + // Thread Safe: YES + public Map createAreas() + { + Map>> worldFactionChunks = BoardColl.get().getWorldToFactionToChunks(false); + return createAreas(worldFactionChunks); + + } + + // Thread Safe: YES + public Map createAreas(Map>> worldFactionChunks) + { + // For each world create the areas + return worldFactionChunks.entrySet().stream() + .map(this::createAreas) + // And combine all of those into a single map: + .map(Map::entrySet) + .flatMap(Set::stream) + .collect(Collectors.toMap(Entry::getKey, Entry::getValue)); + } + + public Map createAreas(Entry>> superEntry) + { + return createAreas(superEntry.getKey(), superEntry.getValue()); + } + + public Map createAreas(String world, Map> map) + { + // For each entry convert it into the appropriate map (with method below) + return map.entrySet().stream() + .map(e -> createAreas(world, e)) + // And combine all of those into a single map: + .map(Map::entrySet) + .flatMap(Set::stream) + .collect(Collectors.toMap(Entry::getKey, Entry::getValue)); + } + + public Map createAreas(String world, Entry> entry) + { + return createAreas(world, entry.getKey(), entry.getValue()); + } + + public Map createAreas(String world, Faction faction, Set chunks) + { + // If the faction is visible ... + if (!isVisible(faction, world)) return Collections.emptyMap(); + + // ... and has any chunks ... + if (chunks.isEmpty()) return Collections.emptyMap(); + + Map ret = new MassiveMap<>(); + + // Get info + String description = getDescription(faction); + DynmapStyle style = this.getStyle(faction); + + // Here we start of with all chunks + // This field is slowly cleared when the chunks are grouped into polygons + Set allChunksSource = new MassiveSet<>(chunks); + + while (!allChunksSource.isEmpty()) + { + PS somePs = allChunksSource.iterator().next(); + + // Create the polygon + Set polygonChunks = new MassiveSet<>(); + floodFillTarget(allChunksSource, polygonChunks, somePs); + List linelist = getLineList(polygonChunks); + + // Calc the x and y arrays + int sz = linelist.size(); + double[] x = new double[sz]; + double[] z = new double[sz]; + + int i = 0; + for (PS ps : linelist) + { + x[i] = ps.getLocationX(true); + z[i] = ps.getLocationZ(true); + i++; + } + + // Build information for specific area + String markerId = calcMarkerId(world, faction); + AreaMarkerValues values = new AreaMarkerValues(faction.getName(), world, x, z, description, style); + ret.put(markerId, values); + } + + return ret; + } + + private static PS getMinimum(Collection pss) + { + int minimumX = Integer.MAX_VALUE; + int minimumZ = Integer.MAX_VALUE; + + for (PS chunk : pss) + { + int chunkX = chunk.getChunkX(); + int chunkZ = chunk.getChunkZ(); + + if (chunkX < minimumX) + { + minimumX = chunkX; + minimumZ = chunkZ; + } + else if (chunkX == minimumX && chunkZ < minimumZ) + { + minimumZ = chunkZ; + } + } + return PS.valueOf(minimumX, minimumZ); + } + + // XPLUS, ZPLUS, XMINUS, ZMINUS + private static List getLineList(Set polygonChunks) + { + PS minimumChunk = getMinimum(polygonChunks); + + //final int initialX = minimumChunk.getChunkX(); + //final int initialZ = minimumChunk.getChunkZ(); + //int currentX = initialX; + //int currentZ = initialZ; + + PS currentChunk = minimumChunk; + + Direction direction = Direction.XPLUS; + List linelist = new MassiveList<>(); + + linelist.add(minimumChunk); // Add start point + while ((!currentChunk.equals(minimumChunk)) || (direction != Direction.ZMINUS)) + { + PS adjacent = direction.adjacent(currentChunk); + PS corner = direction.getCorner(currentChunk); + // If the adjacent chunk is not present + + if (!polygonChunks.contains(adjacent)) + { // Right turn? + linelist.add(corner); // Finish line + direction = direction.turnRight(); // Change direction + } + + // If the chunk left of the adjacent is not present + else if (!polygonChunks.contains(direction.turnLeft().adjacent(adjacent))) + { // Straight? + currentChunk = adjacent; + } + + else + { // Left turn + linelist.add(corner); // Finish line + direction = direction.turnLeft(); + + // Left turn of adjacent + currentChunk = direction.adjacent(adjacent); + } + } + + return linelist; + } + + // IS CLAIMED + + private static boolean isSoutheastClaimed(PS ps, Collection polygon) + { + return polygon.contains(PS.valueOf(ps.getChunkX() - 1, ps.getChunkZ() + 1)); + } + + private static boolean isNortheastClaimed(PS ps, Collection polygon) + { + return polygon.contains(PS.valueOf(ps.getChunkX() + 1, ps.getChunkZ() + 1)); + } + + private static boolean isSouthwestClaimed(PS ps, Collection polygon) + { + return polygon.contains(PS.valueOf(ps.getChunkX() - 1, ps.getChunkZ() - 1)); + } + + private static boolean isNorthwestClaimed(PS ps, Collection polygon) + { + return polygon.contains(PS.valueOf(ps.getChunkX() + 1, ps.getChunkZ() - 1)); + } + + // GET CHUNKS + + private static PS getNortheastPS(PS ps) + { + return PS.valueOf(ps.getChunkX() + 1, ps.getChunkZ() + 1); + } + + private static PS getSoutheastPS(PS ps) + { + return PS.valueOf(ps.getChunkX(), ps.getChunkZ() + 1); + } + + private static PS getSouthwestPS(PS ps) + { + return PS.valueOf(ps.getChunkX(), ps.getChunkZ()); + } + + private static PS getNorthwestPS(PS ps) + { + return PS.valueOf(ps.getChunkX() + 1, ps.getChunkZ()); + } + + // This markerIndex, is if a faction has several claims in a single world + private int markerIdx = 0; + private String lastPartialMarkerId = ""; + public String calcMarkerId(String world, Faction faction) + { + // Calc current partial + String partial = IntegrationDynmap.FACTIONS_AREA_ + world + "__" + faction.getId() + "__"; + + // If different than last time, then reset the counter + if (!partial.equals(lastPartialMarkerId)) markerIdx = 0; + + this.lastPartialMarkerId = partial; + + return partial + markerIdx++; + } + + // Thread Safe: NO + public void updateAreas(Map values) + { + // Cleanup old markers + this.markerset.getAreaMarkers().stream() // Get current markers + .filter(am -> !values.containsKey(am.getMarkerID())) // That are not in the new map + .forEach(AreaMarker::deleteMarker); // and delete them + + + // Map Current + Map markers = getMarkerMap(this.markerset); + + // Loop New + values.forEach((markerId, value) -> + value.ensureExistsAndUpdated(markers.get(markerId), this.markerApi, this.markerset, markerId)); + + } + + private static Map getMarkerMap(MarkerSet markerSet) + { + return markerSet.getAreaMarkers().stream().collect(Collectors.toMap(AreaMarker::getMarkerID, m->m)); + } + + // -------------------------------------------- // + // UTIL & SHARED + // -------------------------------------------- // + + // Thread Safe / Asynchronous: Yes + private String getDescription(Faction faction) + { + String ret = "
" + MConf.get().dynmapFactionDescription + "
"; + + // Name + String name = faction.getName(); + ret = addToHtml(ret, "name", name); + + // Description + String description = faction.getDescriptionDesc(); + ret = addToHtml(ret, "description", description); + + // MOTD (probably shouldn't be shown but if the server owner specifies it, I don't care) + String motd = faction.getMotd(); + if (motd != null) ret = addToHtml(ret, "motd", motd); + + // Age + long ageMillis = faction.getAge(); + LinkedHashMap ageUnitcounts = TimeDiffUtil.limit(TimeDiffUtil.unitcounts(ageMillis, TimeUnit.getAllButMillisSecondsAndMinutes()), 3); + String age = TimeDiffUtil.formatedVerboose(ageUnitcounts, ""); + ret = addToHtml(ret, "age", age); + + // Money + String money = "unavailable"; + if (Econ.isEnabled() && MConf.get().dynmapShowMoneyInDescription) + { + money = Money.format(Econ.getMoney(faction)); + } + ret = addToHtml(ret, "money", money); + + // Flags + Map flags = MFlag.getAll().stream() + .filter(MFlag::isVisible) + .collect(Collectors.toMap(m -> m, faction::getFlag)); + + List flagMapParts = new MassiveList<>(); + List flagTableParts = new MassiveList<>(); + + for (Entry entry : flags.entrySet()) + { + String flagName = entry.getKey().getName(); + boolean value = entry.getValue(); + + String bool = String.valueOf(value); + String color = calcBoolcolor(flagName, value); + String boolcolor = calcBoolcolor(String.valueOf(value), value); + + ret = ret.replace("%" + flagName + ".bool%", bool); // true + ret = ret.replace("%" + flagName + ".color%", color); // monsters (red or green) + ret = ret.replace("%" + flagName + ".boolcolor%", boolcolor); // true (red or green) + + flagMapParts.add(flagName + ": " + boolcolor); + flagTableParts.add(color); + } + + String flagMap = Txt.implode(flagMapParts, "
\n"); + ret = ret.replace("%flags.map%", flagMap); + + // The server can specify the wished number of columns + // So we loop over the possibilities + for (int cols = 1; cols <= 10; cols++) + { + String flagTable = getHtmlAsciTable(flagTableParts, cols); + ret = ret.replace("%flags.table" + cols + "%", flagTable); + } + + // Players + List playersList = faction.getMPlayers(); + String playersCount = String.valueOf(playersList.size()); + String players = getHtmlPlayerString(playersList); + + MPlayer playersLeaderObject = faction.getLeader(); + String playersLeader = getHtmlPlayerName(playersLeaderObject); + + ret = ret.replace("%players%", players); + ret = ret.replace("%players.count%", playersCount); + ret = ret.replace("%players.leader%", playersLeader); + + return ret; + } + + public static String getHtmlAsciTable(Collection strings, final int cols) + { + StringBuilder ret = new StringBuilder(); + + int count = 0; + for (Iterator iter = strings.iterator(); iter.hasNext();) + { + String string = iter.next(); + count++; + + ret.append(string); + + if (iter.hasNext()) + { + boolean lineBreak = count % cols == 0; + ret.append(lineBreak ? "
" : " | "); + } + } + + return ret.toString(); + } + + public static String getHtmlPlayerString(List mplayers) + { + List names = mplayers.stream().map(EngineDynmap::getHtmlPlayerName).collect(Collectors.toList()); + return Txt.implodeCommaAndDot(names); + } + + public static String getHtmlPlayerName(MPlayer mplayer) + { + if (mplayer == null) return "none"; + return StringEscapeUtils.escapeHtml(mplayer.getName()); + } + + public static String calcBoolcolor(String string, boolean bool) + { + return "" + string + ""; + } + + public static String addToHtml(String ret, String target, String replace) + { + if (ret == null) throw new NullPointerException("ret"); + if (target == null) throw new NullPointerException("target"); + if (replace == null) throw new NullPointerException("replace"); + + target = "%" + target + "%"; + replace = ChatColor.stripColor(replace); + replace = StringEscapeUtils.escapeHtml(replace); + return ret.replace(target, replace); + } + + // Thread Safe / Asynchronous: Yes + private boolean isVisible(Faction faction, String world) + { + if (faction == null) throw new NullPointerException("faction"); + if (world == null) throw new NullPointerException("world"); + + final String factionId = faction.getId(); + final String factionName = faction.getName(); + final String worldId = "world:" + world; + + Set ids = MUtil.set(factionId, factionName, worldId); + + if (factionId == null) throw new NullPointerException("faction id"); + if (factionName == null) throw new NullPointerException("faction name"); + + Set visible = MConf.get().dynmapVisibleFactions; + Set hidden = MConf.get().dynmapHiddenFactions; + + + if (!visible.isEmpty() && visible.stream().noneMatch(ids::contains)) + { + return false; + } + + if (!hidden.isEmpty() && hidden.stream().anyMatch(ids::contains)) + { + return false; + } + + + return true; + } + + // Thread Safe / Asynchronous: Yes + public DynmapStyle getStyle(Faction faction) + { + Map styles = MConf.get().dynmapFactionStyles; + + return DynmapStyle.coalesce( + styles.get(faction.getId()), + styles.get(faction.getName()), + MConf.get().dynmapDefaultStyle + ); + } + + public static void logSevere(String msg) + { + String message = ChatColor.RED.toString() + msg; + Factions.get().log(message); + } + + enum Direction + { + XPLUS, ZPLUS, XMINUS, ZMINUS + + ; + + public PS adjacent(PS ps) + { + switch (this) + { + case XPLUS: return PS.valueOf(ps.getChunkX() + 1, ps.getChunkZ()); + case ZPLUS: return PS.valueOf(ps.getChunkX(), ps.getChunkZ() + 1); + case XMINUS: return PS.valueOf(ps.getChunkX() - 1, ps.getChunkZ()); + case ZMINUS: return PS.valueOf(ps.getChunkX(), ps.getChunkZ() - 1); + } + throw new RuntimeException("say what"); + } + + public PS getCorner(PS ps) + { + switch (this) + { + case XPLUS: return PS.valueOf(ps.getChunkX() + 1, ps.getChunkZ()); + case ZPLUS: return PS.valueOf(ps.getChunkX() + 1, ps.getChunkZ() + 1); + case XMINUS: return PS.valueOf(ps.getChunkX(), ps.getChunkZ() + 1); + case ZMINUS: return PS.valueOf(ps.getChunkX(), ps.getChunkZ()); + } + throw new RuntimeException("say what"); + } + + public Direction turnRight() + { + return values()[(this.ordinal() + 1) % values().length]; + } + + public Direction turnAround() + { + return this.turnRight().turnRight(); + } + + public Direction turnLeft() + { + return this.turnRight().turnRight().turnRight(); + } + } + + private void floodFillTarget(Collection source, Collection destination, PS startChunk) + { + // Create the deque + ArrayDeque stack = new ArrayDeque<>(); + stack.push(startChunk); + + // And for each item in the queue + while (!stack.isEmpty()) + { + PS next = stack.pop(); + + // If it is in the source + // Remove it from there to avoid double-counting (and endless recursion) + if (!source.remove(next)) continue; + + // Add to destination + destination.add(next); + + // And look in adjacent chunks that are within the source + Stream.of(Direction.values()) + .map(d -> d.adjacent(next)) + .filter(source::contains) + .forEach(stack::push); + } + } + +} diff --git a/src/com/massivecraft/factions/integration/dynmap/IntegrationDynmap.java b/src/com/massivecraft/factions/integration/dynmap/IntegrationDynmap.java new file mode 100644 index 00000000..68f7b126 --- /dev/null +++ b/src/com/massivecraft/factions/integration/dynmap/IntegrationDynmap.java @@ -0,0 +1,55 @@ +package com.massivecraft.factions.integration.dynmap; + +import com.massivecraft.factions.integration.worldguard.EngineWorldGuard; +import com.massivecraft.massivecore.Engine; +import com.massivecraft.massivecore.Integration; + +public class IntegrationDynmap extends Integration +{ + // -------------------------------------------- // + // CONSTANTS + // -------------------------------------------- // + + // Constants must be here rather than in EngineDynmap. + // MConf relies on DynmapStyle which relies on these constants + // and we must be able to load MConf without EngineDynmap. + public final static int BLOCKS_PER_CHUNK = 16; + + public final static String FACTIONS = "factions"; + public final static String FACTIONS_ = FACTIONS + "_"; + + public final static String FACTIONS_MARKERSET = FACTIONS_ + "markerset"; + + public final static String FACTIONS_AREA = FACTIONS_ + "area"; + public final static String FACTIONS_AREA_ = FACTIONS_AREA + "_"; + + public final static transient String DYNMAP_STYLE_LINE_COLOR = "#00FF00"; + public final static transient double DYNMAP_STYLE_LINE_OPACITY = 0.8D; + public final static transient int DYNMAP_STYLE_LINE_WEIGHT = 3; + public final static transient String DYNMAP_STYLE_FILL_COLOR = "#00FF00"; + public final static transient double DYNMAP_STYLE_FILL_OPACITY = 0.35D; + public final static transient String DYNMAP_STYLE_HOME_MARKER = "greenflag"; + public final static transient boolean DYNMAP_STYLE_BOOST = false; + + // -------------------------------------------- // + // INSTANCE & CONSTRUCT + // -------------------------------------------- // + + private static IntegrationDynmap i = new IntegrationDynmap(); + public static IntegrationDynmap get() { return i; } + private IntegrationDynmap() + { + this.setPluginName("dynmap"); + } + + // -------------------------------------------- // + // OVERRIDE + // -------------------------------------------- // + + @Override + public Engine getEngine() + { + return EngineDynmap.get(); + } + +} diff --git a/src/com/massivecraft/factions/integration/dynmap/LayerValues.java b/src/com/massivecraft/factions/integration/dynmap/LayerValues.java new file mode 100644 index 00000000..5392c224 --- /dev/null +++ b/src/com/massivecraft/factions/integration/dynmap/LayerValues.java @@ -0,0 +1,107 @@ +package com.massivecraft.factions.integration.dynmap; + +import com.massivecraft.massivecore.util.MUtil; +import org.dynmap.markers.MarkerAPI; +import org.dynmap.markers.MarkerSet; + +public class LayerValues +{ + + // -------------------------------------------- // + // FIELDS + // -------------------------------------------- // + + private final String label; + public String getLabel() { return label; } + public LayerValues withLabel(String label) { return new LayerValues(label, minimumZoom, priority, hiddenByDefault); } + + private final int minimumZoom; + public int getMinimumZoom() { return minimumZoom; } + public LayerValues withMinimumZoom(int minimumZoom) { return new LayerValues(label, minimumZoom, priority, hiddenByDefault); } + + private final int priority; + public int getPriority() { return priority; } + public LayerValues withPriority(int priority) { return new LayerValues(label, minimumZoom, priority, hiddenByDefault); } + + private final boolean hiddenByDefault; + public boolean isHiddenByDefault() { return hiddenByDefault; } + public LayerValues withHidenByDefault(boolean hideByDefault) { return new LayerValues(label, minimumZoom, priority, hideByDefault); } + + // -------------------------------------------- // + // CONSTRUCTOR + // -------------------------------------------- // + + public LayerValues(String label, int minimumZoom, int priority, boolean hideByDefault) + { + this.label = label; + this.minimumZoom = minimumZoom; + this.priority = priority; + this.hiddenByDefault = hideByDefault; + } + + // -------------------------------------------- // + // MASTER + // -------------------------------------------- // + + public MarkerSet ensureExistsAndUpdated(MarkerAPI api, String id) + { + MarkerSet set = api.getMarkerSet(id); + if (set == null) + { + set = this.create(api, id); + } + else + { + this.update(set); + } + + if (set == null) + { + EngineDynmap.logSevere("Could not create the Faction Markerset/Layer"); + } + + return set; + } + + // -------------------------------------------- // + // CREATE + // -------------------------------------------- // + + public MarkerSet create(MarkerAPI markerApi, String id) + { + MarkerSet ret = markerApi.createMarkerSet(id, this.label, null, false); // ("null, false" at the end means "all icons allowed, not perisistent") + if (ret == null) return null; + + // Minimum Zoom + if (this.minimumZoom > 0) + { + ret.setMinZoom(this.getMinimumZoom()); + } + + // Priority + ret.setLayerPriority(this.getPriority()); + + // Hide by Default + ret.setHideByDefault(this.isHiddenByDefault()); + return ret; + } + + // -------------------------------------------- // + // UPDATE + // -------------------------------------------- // + + public void update(MarkerSet markerset) + { + // Minimum Zoom + if (this.minimumZoom > 0) + { + MUtil.setIfDifferent(this.getMinimumZoom(), markerset::getMinZoom, markerset::setMinZoom); + } + + // Set other values + MUtil.setIfDifferent(this.getLabel(), markerset::getMarkerSetLabel, markerset::setMarkerSetLabel); + MUtil.setIfDifferent(this.getPriority(), markerset::getLayerPriority, markerset::setLayerPriority); + MUtil.setIfDifferent(this.isHiddenByDefault(), markerset::getHideByDefault, markerset::setHideByDefault); + } + +} diff --git a/src/com/massivecraft/factions/integration/dynmap/MarkerValues.java b/src/com/massivecraft/factions/integration/dynmap/MarkerValues.java new file mode 100644 index 00000000..50c747f6 --- /dev/null +++ b/src/com/massivecraft/factions/integration/dynmap/MarkerValues.java @@ -0,0 +1,135 @@ +package com.massivecraft.factions.integration.dynmap; + +import com.massivecraft.massivecore.util.MUtil; +import org.dynmap.markers.Marker; +import org.dynmap.markers.MarkerAPI; +import org.dynmap.markers.MarkerIcon; +import org.dynmap.markers.MarkerSet; + +public class MarkerValues +{ + // -------------------------------------------- // + // FIELDS + // -------------------------------------------- // + + private final String label; + public String getLabel() { return label; } + public MarkerValues withLabel(String label) { return new MarkerValues(label, world, x, y, z, iconName, description); } + + private final String world; + public String getWorld() { return world; } + public MarkerValues withWorld(String world) { return new MarkerValues(label, world, x, y, z, iconName, description); } + + private final double x; + public double getX() { return x; } + public MarkerValues withX(double x) { return new MarkerValues(label, world, x, y, z, iconName, description); } + + private final double y; + public double getY() { return y; } + public MarkerValues withY(double y) { return new MarkerValues(label, world, x, y, z, iconName, description); } + + private final double z; + public double getZ() { return z; } + public MarkerValues withZ(double z) { return new MarkerValues(label, world, x, y, z, iconName, description); } + + private final String iconName; + public String getIconName() { return iconName; } + public MarkerValues withIconName(String iconName) { return new MarkerValues(label, world, x, y, z, iconName, description); } + + private final String description; + public String getDescription() { return description; } + public MarkerValues withDescription(String description) { return new MarkerValues(label, world, x, y, z, iconName, description); } + + // -------------------------------------------- // + // CONSTRUCTOR + // -------------------------------------------- // + + public MarkerValues(String label, String world, double x, double y, double z, String iconName, String description) + { + this.label = label; + this.world = world; + this.x = x; + this.y = y; + this.z = z; + this.iconName = iconName; + this.description = description; + } + + // -------------------------------------------- // + // MAKE SURE EXISTS + // -------------------------------------------- // + + public Marker ensureExistsAndUpdated(MarkerAPI markerApi, MarkerSet markerset, String id) + { + throw new UnsupportedOperationException("todo"); + } + + // -------------------------------------------- // + // CREATE + // -------------------------------------------- // + + public Marker create(MarkerAPI markerApi, MarkerSet markerset, String markerId) + { + Marker ret = markerset.createMarker( + markerId, + this.getLabel(), + this.getWorld(), + this.getX(), + this.getY(), + this.getZ(), + getMarkerIcon(markerApi, this.getIconName()), + false // not persistent + ); + + if (ret == null) return null; + + ret.setDescription(this.getDescription()); + + return ret; + } + + // -------------------------------------------- // + // UPDATE + // -------------------------------------------- // + + public void update(MarkerAPI markerApi, MarkerSet markerset, Marker marker) + { + if + ( + !MUtil.equals(marker.getWorld(), this.getWorld()) + || + marker.getX() != this.getX() + || + marker.getY() != this.getY() + || + marker.getZ() != this.getZ() + ) + { + marker.setLocation( + this.getWorld(), + this.getX(), + this.getY(), + this.getZ() + ); + } + + MUtil.setIfDifferent(this.getLabel(), marker::getLabel, marker::setLabel); + + MarkerIcon icon = getMarkerIcon(markerApi, this.iconName); + MUtil.setIfDifferent(icon, marker::getMarkerIcon, marker::setMarkerIcon); + + MUtil.setIfDifferent(this.getDescription(), marker::getDescription, marker::setDescription); + } + + // -------------------------------------------- // + // UTIL + // -------------------------------------------- // + + public static MarkerIcon getMarkerIcon(MarkerAPI markerApi, String name) + { + MarkerIcon ret = markerApi.getMarkerIcon(name); + if (ret == null) ret = markerApi.getMarkerIcon(IntegrationDynmap.DYNMAP_STYLE_HOME_MARKER); + return ret; + } + +} \ No newline at end of file