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.LinkedHashSet; 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()) { Iterator it = allChunksSource.iterator(); PS somePs = it.next(); List linelist = new MassiveList<>(); // Create the polygon Set polygonChunks = new MassiveSet<>(); floodFillTarget(allChunksSource, polygonChunks, somePs); List polygonCorners = new MassiveList<>(); for (PS chunk : polygonChunks) { for (Direction d : Direction.values()) { polygonCorners.add(d.getCorner(chunk)); } } Set points = new MassiveSet<>(); for (PS corner : polygonCorners) { if (points.contains(corner)) points.remove(corner); else points.add(corner); } Map edges_h = new MassiveMap<>(); Map edges_v = new MassiveMap<>(); List sorted_x = new MassiveList<>(points); Collections.sort(sorted_x, this::xThenZ); List sorted_z = new MassiveList<>(points); Collections.sort(sorted_z, this::zThenX); // Create horizontal edges for (int i = 0; i < points.size();) { int curr_z = sorted_z.get(i).getChunkZ(); while (i < points.size() && curr_z == sorted_z.get(i+1).getChunkZ()) { edges_h.put(sorted_z.get(i), sorted_z.get(i+1)); edges_h.put(sorted_z.get(i+1), sorted_z.get(i)); i += 2; } } // Create vertical edges for (int i = 0; i < points.size();) { int curr_x = sorted_x.get(i).getChunkX(); while (i < points.size() && curr_x == sorted_x.get(i+1).getChunkX()) { edges_v.put(sorted_x.get(i), sorted_x.get(i+1)); edges_v.put(sorted_x.get(i+1), sorted_x.get(i)); i += 2; } } //List linelist = getLineList(polygonChunks); linelist = new MassiveList<>(new LinkedHashSet<>(linelist)); for (Direction d : Direction.values()) { linelist.add(d.getCorner(somePs)); } // Build information for specific area String markerId = calcMarkerId(world, faction); AreaMarkerValues values = new AreaMarkerValues(faction.getName(), world, linelist.toArray(new PS[]{}), description, style); ret.put(markerId, values); } return ret; } private int zThenX(PS a, PS b) { if (a.getChunkZ() < b.getChunkZ() || (a.getChunkZ().equals(b.getChunkZ()) && a.getChunkX() < b.getChunkX())) return -1; else if (a.equals(b)) return 0; else return 1; } private int xThenZ(PS a, PS b) { if (a.getChunkX() < b.getChunkX() || (a.getChunkX().equals(b.getChunkX()) && a.getChunkZ() < b.getChunkZ())) return -1; else if (a.equals(b)) return 0; else return 1; } 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); } } }