package com.massivecraft.factions.integration.dynmap; import com.massivecraft.factions.*; import com.massivecraft.factions.integration.Econ; import com.massivecraft.factions.struct.Role; import com.massivecraft.factions.zcore.persist.MemoryBoard; import org.bukkit.Bukkit; import org.bukkit.ChatColor; import org.bukkit.Location; import org.bukkit.plugin.Plugin; import org.dynmap.DynmapAPI; import org.dynmap.markers.*; import org.dynmap.utils.TileFlags; import java.awt.Point; import java.util.*; import java.util.Map.Entry; // This source code is a heavily modified version of mikeprimms plugin Dynmap-Factions. public class EngineDynmap { /** * @author FactionsUUID Team */ // -------------------------------------------- // // CONSTANTS // -------------------------------------------- // public final static int BLOCKS_PER_CHUNK = 16; public final static String DYNMAP_INTEGRATION = "\u00A7dDynmap Integration: \u00A7e"; 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_HOME = FACTIONS_ + "home"; public final static String FACTIONS_HOME_ = FACTIONS_HOME + "_"; public final static String FACTIONS_PLAYERSET = FACTIONS_ + "playerset"; public final static String FACTIONS_PLAYERSET_ = FACTIONS_PLAYERSET + "_"; // -------------------------------------------- // // INSTANCE & CONSTRUCT // -------------------------------------------- // private static final EngineDynmap i = new EngineDynmap(); public DynmapAPI dynmapApi; public MarkerAPI markerApi; public MarkerSet markerset; List> polyLine = new ArrayList>(); private EngineDynmap() { } public static EngineDynmap getInstance() { return i; } public static String getHtmlPlayerString(Collection playersOfficersList) { StringBuilder ret = new StringBuilder(); for (FPlayer fplayer : playersOfficersList) { if (ret.length() > 0) { ret.append(", "); } ret.append(getHtmlPlayerName(fplayer)); } return ret.toString(); } public static String getHtmlPlayerName(FPlayer fplayer) { return fplayer != null ? escapeHtml(fplayer.getName()) : "none"; } public static String escapeHtml(String string) { if (string == null) { return ""; } StringBuilder out = new StringBuilder(Math.max(16, string.length())); for (int i = 0; i < string.length(); i++) { char c = string.charAt(i); if (c > 127 || c == '"' || c == '<' || c == '>' || c == '&') { out.append("&#").append((int) c).append(';'); } else { out.append(c); } } return out.toString(); } // Thread Safe / Asynchronous: Yes public static void info(String msg) { System.out.println(DYNMAP_INTEGRATION + msg); } // -------------------------------------------- // // UPDATE: HOMES // -------------------------------------------- // // Thread Safe / Asynchronous: Yes public static void severe(String msg) { System.out.println(DYNMAP_INTEGRATION + ChatColor.RED.toString() + msg); } public void init() { Plugin dynmap = Bukkit.getServer().getPluginManager().getPlugin("dynmap"); if (dynmap == null || !dynmap.isEnabled()) { return; } // Should we even use dynmap? if (!Conf.dynmapUse) { if (this.markerset != null) { this.markerset.deleteMarkerSet(); this.markerset = null; } return; } // Shedule non thread safe sync at the end! Bukkit.getScheduler().scheduleSyncRepeatingTask(FactionsPlugin.getInstance(), () -> { final Map homes = createHomes(); final Map areas = createAreas(); final Map polys = createPolys(areas); final Map> playerSets = createPlayersets(); if (!updateCore()) { return; } // createLayer() is thread safe but it makes use of fields set in updateCore() so we must have it after. if (!updateLayer(createLayer())) { return; } updateHomes(homes); updateAreas(areas); updatePolys(polys); updatePlayersets(playerSets); }, 100L, 100L); } // -------------------------------------------- // // UPDATE: AREAS // -------------------------------------------- // // Thread Safe / Asynchronous: No public boolean updateCore() { // Get DynmapAPI this.dynmapApi = (DynmapAPI) Bukkit.getPluginManager().getPlugin("dynmap"); if (this.dynmapApi == null) { severe("Could not retrieve the DynmapAPI."); return false; } // Get MarkerAPI this.markerApi = this.dynmapApi.getMarkerAPI(); if (this.markerApi == null) { severe("Could not retrieve the MarkerAPI."); return false; } return true; } // Thread Safe / Asynchronous: Yes public TempMarkerSet createLayer() { TempMarkerSet ret = new TempMarkerSet(); ret.label = Conf.dynmapLayerName; ret.minimumZoom = Conf.dynmapLayerMinimumZoom; ret.priority = Conf.dynmapLayerPriority; ret.hideByDefault = !Conf.dynmapLayerVisible; return ret; } // Thread Safe / Asynchronous: No public boolean updateLayer(TempMarkerSet temp) { this.markerset = this.markerApi.getMarkerSet(FACTIONS_MARKERSET); if (this.markerset == null) { this.markerset = temp.create(this.markerApi, FACTIONS_MARKERSET); if (this.markerset == null) { severe("Could not create the Faction Markerset/Layer"); return false; } } else { temp.update(this.markerset); } return true; } // Thread Safe / Asynchronous: Yes public Map createHomes() { Map ret = new HashMap<>(); // Loop current factions for (Faction faction : Factions.getInstance().getAllFactions()) { Location ps = faction.getHome(); if (ps == null) { continue; } DynmapStyle style = getStyle(faction); String markerId = FACTIONS_HOME_ + faction.getId(); TempMarker temp = new TempMarker(); temp.label = ChatColor.stripColor(faction.getTag()); temp.world = ps.getWorld().toString(); temp.x = ps.getX(); temp.y = ps.getY(); temp.z = ps.getZ(); temp.iconName = style.getHomeMarker(); temp.description = getDescription(faction); ret.put(markerId, temp); } return ret; } // Thread Safe / Asynchronous: No // This method places out the faction home markers into the factions markerset. public void updateHomes(Map homes) { // Put all current faction markers in a map Map markers = new HashMap<>(); for (Marker marker : this.markerset.getMarkers()) { markers.put(marker.getMarkerID(), marker); } // Loop homes for (Entry entry : homes.entrySet()) { String markerId = entry.getKey(); TempMarker temp = entry.getValue(); // Get Creative // 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. Marker marker = markers.remove(markerId); if (marker == null) { marker = temp.create(this.markerApi, this.markerset, markerId); marker = temp.create(this.markerApi, this.markerset, markerId); if (marker == null) { EngineDynmap.severe("Could not get/create the home marker " + markerId); } } else { temp.update(this.markerApi, marker); } } // Delete Deprecated Markers // Only old markers should now be left for (Marker marker : markers.values()) { marker.deleteMarker(); } } // -------------------------------------------- // // UPDATE: PLAYERSET // -------------------------------------------- // // Thread Safe: YES public Map createPolys(Map areas) { Map ret = new HashMap(); for (Entry entry : areas.entrySet()) { String markerID = entry.getKey(); TempAreaMarker area = entry.getValue(); int counter = 0; for (List points : area.getPolyLine()) { markerID = markerID + "_poly_" + counter; TempPolyLineMarker tempPoly = new TempPolyLineMarker(); tempPoly.polyLine = points; tempPoly.lineColor = area.lineColor; tempPoly.lineOpacity = area.lineOpacity; tempPoly.lineWeight = area.lineWeight; tempPoly.world = area.world; ret.put(markerID, tempPoly); counter++; } } return ret; } public Map createAreas() { Map>> worldFactionChunks = createWorldFactionChunks(); return createAreas(worldFactionChunks); } // Thread Safe: YES public Map>> createWorldFactionChunks() { // Create map "world name --> faction --> set of chunk coords" Map>> worldFactionChunks = new HashMap<>(); // Note: The board is the world. The board id is the world name. MemoryBoard board = (MemoryBoard) Board.getInstance(); for (Entry entry : board.flocationIds.entrySet()) { String world = entry.getKey().getWorldName(); Faction chunkOwner = Factions.getInstance().getFactionById(entry.getValue()); Map> factionChunks = worldFactionChunks.get(world); if (factionChunks == null) { factionChunks = new HashMap<>(); worldFactionChunks.put(world, factionChunks); } Set factionTerritory = factionChunks.get(chunkOwner); if (factionTerritory == null) { factionTerritory = new HashSet<>(); factionChunks.put(chunkOwner, factionTerritory); } factionTerritory.add(entry.getKey()); } return worldFactionChunks; } // Thread Safe: YES public Map createAreas(Map>> worldFactionChunks) { Map ret = new HashMap<>(); // For each world for (Entry>> entry : worldFactionChunks.entrySet()) { String world = entry.getKey(); Map> factionChunks = entry.getValue(); // For each faction and its chunks in that world for (Entry> entry1 : factionChunks.entrySet()) { Faction faction = entry1.getKey(); Set chunks = entry1.getValue(); Map worldFactionMarkers = createAreas(world, faction, chunks); ret.putAll(worldFactionMarkers); } } return ret; } // Thread Safe: YES // Handle specific faction on specific world // "handle faction on world" public Map createAreas(String world, Faction faction, Set chunks) { Map ret = new HashMap<>(); // If the faction is visible ... if (!isVisible(faction, world)) { return ret; } // ... and has any chunks ... if (chunks.isEmpty()) { return ret; } // Index of polygon for given faction int markerIndex = 0; // Create the info window String description = getDescription(faction); // Fetch Style DynmapStyle style = this.getStyle(faction); // Loop through chunks: set flags on chunk map TileFlags allChunkFlags = new TileFlags(); LinkedList allChunks = new LinkedList<>(); for (FLocation chunk : chunks) { allChunkFlags.setFlag((int) chunk.getX(), (int) chunk.getZ(), true); // Set flag for chunk allChunks.addLast(chunk); } // Loop through until we don't find more areas while (allChunks != null) { TileFlags ourChunkFlags = null; LinkedList ourChunks = null; LinkedList newChunks = null; for (FLocation chunk : allChunks) { int chunkX = (int) chunk.getX(); int chunkZ = (int) chunk.getZ(); // If we need to start shape, and this block is not part of one yet if (ourChunkFlags == null && allChunkFlags.getFlag(chunkX, chunkZ)) { ourChunkFlags = new TileFlags(); // Create map for shape ourChunks = new LinkedList<>(); floodFillTarget(allChunkFlags, ourChunkFlags, chunkX, chunkZ); // Copy shape ourChunks.add(chunk); // Add it to our chunk list } // If shape found, and we're in it, add to our node list else if (ourChunkFlags != null && ourChunkFlags.getFlag(chunkX, chunkZ)) { ourChunks.add(chunk); } // Else, keep it in the list for the next polygon else { if (newChunks == null) { newChunks = new LinkedList<>(); } newChunks.add(chunk); } } // Replace list (null if no more to process) allChunks = newChunks; if (ourChunkFlags == null) { continue; } List outputLines = new ArrayList(); Map lines = new HashMap(); if (ourChunks == null) { continue; } for (FLocation loc : ourChunks) { int x = loc.getChunk().getX(); int z = loc.getChunk().getZ(); TempLine line = new TempLine(new Point(x * 16, z * 16), new Point(x * 16 + 16, z * 16)); if (lines.containsKey(line)) { lines.put(line, lines.get(line) + 1); } else { lines.put(line, 1); } line = new TempLine(new Point(x * 16 + 16, z * 16), new Point(x * 16 + 16, z * 16 + 16)); if (lines.containsKey(line)) { lines.put(line, lines.get(line) + 1); } else { lines.put(line, 1); } line = new TempLine(new Point(x * 16 + 16, z * 16 + 16), new Point(x * 16, z * 16 + 16)); if (lines.containsKey(line)) { lines.put(line, lines.get(line) + 1); } else { lines.put(line, 1); } line = new TempLine(new Point(x * 16, z * 16 + 16), new Point(x * 16, z * 16)); if (lines.containsKey(line)) { lines.put(line, lines.get(line) + 1); } else { lines.put(line, 1); } } Iterator> iterator = lines.entrySet().iterator(); List lineList = new ArrayList(); lineList.addAll(lines.keySet()); while (iterator.hasNext()) { Entry entry = iterator.next(); if (entry.getValue() > 1) { lineList.remove(entry.getKey()); } } // Find the leftmost MCRWPoint TempLine l = null; for (Iterator it = lineList.iterator(); it.hasNext();) { TempLine tl = it.next(); if (l == null || tl.getP1().x < l.getP1().x) l = tl; } for (Iterator it = lineList.iterator(); it.hasNext();) { TempLine tl = it.next(); if (tl.getP2().x < l.getP2().x) l = tl; } outputLines.add(l); lineList.remove(l); while (lineList.size() > 0) { // MCRWPoint targetp = new MCRWPoint((int) lastLine.x1, (int) lastLine.y1); // MCRWPointWLines.get(targetp); TempLine nextLine = null; for (Iterator it = lineList.iterator(); it.hasNext();) { TempLine line = it.next(); if (l.getP2().x == line.getP1().x && l.getP2().y == line.getP1().y) { nextLine = line; } } if (nextLine != null) { outputLines.add(nextLine); lineList.remove(nextLine); l = nextLine; } else { outputLines.get(outputLines.size() - 1).addAdditionLines(CamScan1(lineList)); break; } } List outputPoints = new ArrayList(); List polyPoints = new ArrayList(); for (int i = 0; i < outputLines.size(); i++) { Point p = new Point(outputLines.get(i).getP1().x, outputLines.get(i).getP1().y); outputPoints.add(p); polyPoints.add(p); if (outputLines.get(i).getConnectedLines().size() > 0) { outputPoints.addAll(addRecursivePoints(new Point((int) outputLines.get(i).getP1().x, (int) outputLines.get(i).getP1().y), outputLines.get(i))); } p = new Point((int) outputLines.get(i).getP2().x, (int) outputLines.get(i).getP2().y); outputPoints.add(p); polyPoints.add(p); } polyLine.add(polyPoints); // Build information for specific area double[] x = new double[outputPoints.size()]; double[] z = new double[outputPoints.size()]; for (int i = 0; i < outputPoints.size(); i++) { x[i] = outputPoints.get(i).x; z[i] = outputPoints.get(i).y; } String markerId = FACTIONS_ + world + "__" + faction.getId() + "__" + markerIndex; TempAreaMarker temp = new TempAreaMarker(); temp.label = faction.getTag(); temp.world = world; temp.x = x; temp.z = z; temp.description = description; temp.lineColor = style.getLineColor(); temp.lineOpacity = style.getLineOpacity(); temp.lineWeight = style.getLineWeight(); temp.fillColor = style.getFillColor(); temp.fillOpacity = style.getFillOpacity(); temp.boost = style.getBoost(); temp.setPolyLine(polyLine); ret.put(markerId, temp); polyLine.clear(); markerIndex++; } return ret; } public List addRecursivePoints(Point returnPoint, TempLine line) { List ret = new ArrayList(); boolean shouldReturn = false; List connectedLines = line.getConnectedLines(); List polyPoints = new ArrayList(); for (TempLine line2 : connectedLines) { Point p = new Point((int) line2.getP1().x, (int) line2.getP1().y); ret.add(p); polyPoints.add(p); shouldReturn = true; if (line2.getConnectedLines().size() > 0) { ret.addAll(addRecursivePoints(new Point((int) line2.getP1().x, (int) line2.getP1().y), line2)); } p = new Point((int) line2.getP2().x, (int) line2.getP2().y); ret.add(p); polyPoints.add(p); } if (shouldReturn) { ret.add(returnPoint); } polyLine.add(polyPoints); return ret; } private List CamScan1(List lineList) { List ret = new ArrayList(); // Find the leftmost MCRWPoint TempLine l = null; for (Iterator it = lineList.iterator(); it.hasNext();) { TempLine tl = it.next(); if (l == null || tl.getP1().x < l.getP1().x) l = tl; } for (Iterator it = lineList.iterator(); it.hasNext();) { TempLine tl = it.next(); if (tl.getP2().x < l.getP2().x) l = tl; } ret.add(l); lineList.remove(l); while (lineList.size() > 0) { // MCRWPoint targetp = new MCRWPoint((int) lastLine.x1, (int) lastLine.y1); // MCRWPointWLines.get(targetp); TempLine thisChunkLine = null; for (Iterator it = lineList.iterator(); it.hasNext();) { TempLine line = it.next(); if (l.getP2().x == line.getP1().x && l.getP2().y == line.getP1().y) { thisChunkLine = line; } } if (thisChunkLine != null) { ret.add(thisChunkLine); lineList.remove(thisChunkLine); l = thisChunkLine; } else { // break; ret.get(ret.size() - 1).addAdditionLines(CamScan1(lineList)); } } return ret; } // -------------------------------------------- // // UTIL & SHARED // -------------------------------------------- // public void updatePolys(Map polys) { // Map Current Map markers = new HashMap<>(); for (PolyLineMarker marker : this.markerset.getPolyLineMarkers()) { markers.put(marker.getMarkerID(), marker); } // Loop New for (Entry entry : polys.entrySet()) { String markerId = entry.getKey(); TempPolyLineMarker temp = entry.getValue(); // Get Creative // 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. PolyLineMarker marker = markers.remove(markerId); if (marker == null) { marker = temp.create(this.markerset, markerId); if (marker == null) { severe("Could not get/create the area marker " + markerId); } } else { temp.update(marker); } } // Only old/outdated should now be left. Delete them. for (PolyLineMarker marker : markers.values()) { marker.deleteMarker(); } } // Thread Safe: NO public void updateAreas(Map areas) { // Map Current Map markers = new HashMap<>(); for (AreaMarker marker : this.markerset.getAreaMarkers()) { markers.put(marker.getMarkerID(), marker); } // Loop New for (Entry entry : areas.entrySet()) { String markerId = entry.getKey(); TempAreaMarker temp = entry.getValue(); // Get Creative // 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. AreaMarker marker = markers.remove(markerId); if (marker == null) { marker = temp.create(this.markerset, markerId); if (marker == null) { severe("Could not get/create the area marker " + markerId); } } else { temp.update(marker); } } // Only old/outdated should now be left. Delete them. for (AreaMarker marker : markers.values()) { marker.deleteMarker(); } } // Thread Safe / Asynchronous: Yes public String createPlayersetId(Faction faction) { if (faction == null) { return null; } if (faction.isWilderness()) { return null; } String factionId = faction.getId(); if (factionId == null) { return null; } return FACTIONS_PLAYERSET_ + factionId; } // Thread Safe / Asynchronous: Yes public Set createPlayerset(Faction faction) { if (faction == null) { return null; } if (faction.isWilderness()) { return null; } Set ret = new HashSet<>(); for (FPlayer fplayer : faction.getFPlayers()) { // NOTE: We add both UUID and name. This might be a good idea for future proofing. ret.add(fplayer.getId()); ret.add(fplayer.getName()); } return ret; } // Thread Safe / Asynchronous: Yes public Map> createPlayersets() { if (!Conf.dynmapVisibilityByFaction) { return null; } Map> ret = new HashMap<>(); for (Faction faction : Factions.getInstance().getAllFactions()) { String playersetId = createPlayersetId(faction); if (playersetId == null) { continue; } Set playerIds = createPlayerset(faction); if (playerIds == null) { continue; } ret.put(playersetId, playerIds); } return ret; } // Thread Safe / Asynchronous: No public void updatePlayersets(Map> playersets) { if (playersets == null) { return; } // Remove for (PlayerSet set : this.markerApi.getPlayerSets()) { if (!set.getSetID().startsWith(FACTIONS_PLAYERSET_)) { continue; } // (Null means remove all) if (playersets.containsKey(set.getSetID())) { continue; } set.deleteSet(); } // Add / Update for (Entry> entry : playersets.entrySet()) { // Extract from Entry String setId = entry.getKey(); Set playerIds = entry.getValue(); // Get Creatively PlayerSet set = this.markerApi.getPlayerSet(setId); if (set == null) { set = this.markerApi.createPlayerSet(setId, // id true, // symmetric playerIds, // players false // persistent ); } if (set == null) { severe("Could not get/create the player set " + setId); continue; } // Set Content set.setPlayers(playerIds); } } // Thread Safe / Asynchronous: Yes private String getDescription(Faction faction) { String ret = "
" + Conf.dynmapDescription + "
"; // Name String name = faction.getTag(); name = escapeHtml(ChatColor.stripColor(name)); ret = ret.replace("%name%", name); // Description String description = faction.getDescription(); description = escapeHtml(ChatColor.stripColor(description)); ret = ret.replace("%description%", description); // Money String money = "unavailable"; if (Conf.bankEnabled && Conf.dynmapDescriptionMoney) money = String.format("%.2f", Econ.getBalance(faction.getAccountId())); ret = ret.replace("%money%", money); // Players Set playersList = faction.getFPlayers(); String playersCount = String.valueOf(playersList.size()); String players = getHtmlPlayerString(playersList); FPlayer playersLeaderObject = faction.getFPlayerAdmin(); String playersLeader = getHtmlPlayerName(playersLeaderObject); ArrayList playersCoAdminsList = faction.getFPlayersWhereRole(Role.COLEADER); String playersCoAdminsCount = String.valueOf(playersCoAdminsList.size()); String playersCoAdmins = getHtmlPlayerString(playersCoAdminsList); ArrayList playersModeratorsList = faction.getFPlayersWhereRole(Role.MODERATOR); String playersModeratorsCount = String.valueOf(playersModeratorsList.size()); String playersModerators = getHtmlPlayerString(playersModeratorsList); ArrayList playersNormalsList = faction.getFPlayersWhereRole(Role.NORMAL); String playersNormalsCount = String.valueOf(playersNormalsList.size()); String playersNormals = getHtmlPlayerString(playersNormalsList); ret = ret.replace("%players%", players); ret = ret.replace("%players.count%", playersCount); ret = ret.replace("%players.leader%", playersLeader); ret = ret.replace("%players.admins%", playersCoAdmins); ret = ret.replace("%players.admins.count%", playersCoAdminsCount); ret = ret.replace("%players.moderators%", playersModerators); ret = ret.replace("%players.moderators.count%", playersModeratorsCount); ret = ret.replace("%players.normals%", playersNormals); ret = ret.replace("%players.normals.count%", playersNormalsCount); return ret; } // Thread Safe / Asynchronous: Yes private boolean isVisible(Faction faction, String world) { if (faction == null) { return false; } final String factionId = faction.getId(); if (factionId == null) { return false; } final String factionName = faction.getTag(); if (factionName == null) { return false; } Set visible = Conf.dynmapVisibleFactions; Set hidden = Conf.dynmapHiddenFactions; if (!visible.isEmpty() && !visible.contains(factionId) && !visible.contains(factionName) && !visible.contains("world:" + world)) { return false; } return !hidden.contains(factionId) && !hidden.contains(factionName) && !hidden.contains("world:" + world); } // Thread Safe / Asynchronous: Yes public DynmapStyle getStyle(Faction faction) { DynmapStyle ret; ret = Conf.dynmapFactionStyles.get(faction.getId()); if (ret != null) { return ret; } ret = Conf.dynmapFactionStyles.get(faction.getTag()); if (ret != null) { return ret; } return Conf.dynmapDefaultStyle; } // Find all contiguous blocks, set in target and clear in source private int floodFillTarget(TileFlags source, TileFlags destination, int x, int y) { int cnt = 0; ArrayDeque stack = new ArrayDeque<>(); stack.push(new int[] { x, y }); while (!stack.isEmpty()) { int[] nxt = stack.pop(); x = nxt[0]; y = nxt[1]; if (source.getFlag(x, y)) { // Set in src source.setFlag(x, y, false); // Clear source destination.setFlag(x, y, true); // Set in destination cnt++; if (source.getFlag(x + 1, y)) { stack.push(new int[] { x + 1, y }); } if (source.getFlag(x - 1, y)) { stack.push(new int[] { x - 1, y }); } if (source.getFlag(x, y + 1)) { stack.push(new int[] { x, y + 1 }); } if (source.getFlag(x, y - 1)) { stack.push(new int[] { x, y - 1 }); } } } return cnt; } enum Direction { XPLUS, ZPLUS, XMINUS, ZMINUS } }