2014-12-05 06:04:00 +01:00
|
|
|
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.util.*;
|
|
|
|
import java.util.Map.Entry;
|
|
|
|
|
|
|
|
// This source code is a heavily modified version of mikeprimms plugin Dynmap-Factions.
|
2014-12-07 21:26:13 +01:00
|
|
|
public class EngineDynmap {
|
2014-12-05 06:04:00 +01:00
|
|
|
// -------------------------------------------- //
|
|
|
|
// 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
|
|
|
|
// -------------------------------------------- //
|
|
|
|
|
2018-11-20 20:51:07 +01:00
|
|
|
private static final EngineDynmap i = new EngineDynmap();
|
2018-07-12 18:11:07 +02:00
|
|
|
public DynmapAPI dynmapApi;
|
|
|
|
public MarkerAPI markerApi;
|
|
|
|
public MarkerSet markerset;
|
2018-11-07 06:38:43 +01:00
|
|
|
|
2018-11-20 20:51:07 +01:00
|
|
|
private EngineDynmap() {}
|
2014-12-07 21:26:13 +01:00
|
|
|
|
|
|
|
public static EngineDynmap getInstance() {
|
|
|
|
return i;
|
|
|
|
}
|
|
|
|
|
2018-07-12 18:11:07 +02:00
|
|
|
public static String getHtmlPlayerString(Collection<FPlayer> playersOfficersList) {
|
|
|
|
StringBuilder ret = new StringBuilder();
|
|
|
|
for (FPlayer fplayer : playersOfficersList) {
|
|
|
|
if (ret.length() > 0) {
|
|
|
|
ret.append(", ");
|
|
|
|
}
|
|
|
|
ret.append(getHtmlPlayerName(fplayer));
|
|
|
|
}
|
|
|
|
return ret.toString();
|
2014-12-07 21:26:13 +01:00
|
|
|
}
|
2014-12-05 06:04:00 +01:00
|
|
|
|
2018-07-12 18:11:07 +02:00
|
|
|
public static String getHtmlPlayerName(FPlayer fplayer) {
|
2018-11-20 20:51:07 +01:00
|
|
|
return fplayer != null ? escapeHtml(fplayer.getName()):"none";
|
2018-07-12 18:11:07 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
public static String escapeHtml(String string) {
|
|
|
|
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 == '&') {
|
2018-11-20 20:51:07 +01:00
|
|
|
out.append("&#")
|
|
|
|
.append((int) c)
|
|
|
|
.append(';');
|
2018-07-12 18:11:07 +02:00
|
|
|
} else {
|
|
|
|
out.append(c);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return out.toString();
|
|
|
|
}
|
|
|
|
|
|
|
|
// Thread Safe / Asynchronous: Yes
|
|
|
|
public static void info(String msg) {
|
2018-11-20 20:51:07 +01:00
|
|
|
System.out.println(DYNMAP_INTEGRATION + msg);
|
2018-07-12 18:11:07 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// -------------------------------------------- //
|
|
|
|
// UPDATE: HOMES
|
|
|
|
// -------------------------------------------- //
|
|
|
|
|
|
|
|
// Thread Safe / Asynchronous: Yes
|
|
|
|
public static void severe(String msg) {
|
2018-11-20 20:51:07 +01:00
|
|
|
System.out.println(DYNMAP_INTEGRATION + ChatColor.RED.toString() + msg);
|
2018-07-12 18:11:07 +02:00
|
|
|
}
|
2014-12-05 06:04:00 +01:00
|
|
|
|
2014-12-07 21:26:13 +01:00
|
|
|
public void init() {
|
2014-12-05 06:04:00 +01:00
|
|
|
Plugin dynmap = Bukkit.getServer().getPluginManager().getPlugin("dynmap");
|
|
|
|
|
2018-11-07 17:14:42 +01:00
|
|
|
if (dynmap == null || !dynmap.isEnabled()) {
|
2014-12-05 06:04:00 +01:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Should we even use dynmap?
|
2018-11-07 17:14:42 +01:00
|
|
|
if (!Conf.dynmapUse) {
|
2014-12-07 21:26:13 +01:00
|
|
|
if (this.markerset != null) {
|
2014-12-05 06:04:00 +01:00
|
|
|
this.markerset.deleteMarkerSet();
|
|
|
|
this.markerset = null;
|
|
|
|
}
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Shedule non thread safe sync at the end!
|
2018-11-07 06:38:43 +01:00
|
|
|
Bukkit.getScheduler().scheduleSyncRepeatingTask(SavageFactions.plugin, new Runnable() {
|
2014-12-05 06:04:00 +01:00
|
|
|
@Override
|
2014-12-07 21:26:13 +01:00
|
|
|
public void run() {
|
2014-12-05 06:04:00 +01:00
|
|
|
|
|
|
|
final Map<String, TempMarker> homes = createHomes();
|
|
|
|
final Map<String, TempAreaMarker> areas = createAreas();
|
|
|
|
final Map<String, Set<String>> playerSets = createPlayersets();
|
|
|
|
|
2018-11-07 17:14:42 +01:00
|
|
|
if (!updateCore()) {
|
2014-12-07 21:26:13 +01:00
|
|
|
return;
|
|
|
|
}
|
2014-12-05 06:04:00 +01:00
|
|
|
|
|
|
|
// createLayer() is thread safe but it makes use of fields set in updateCore() so we must have it after.
|
2018-11-07 17:14:42 +01:00
|
|
|
if (!updateLayer(createLayer())) {
|
2014-12-07 21:26:13 +01:00
|
|
|
return;
|
|
|
|
}
|
2014-12-05 06:04:00 +01:00
|
|
|
|
|
|
|
updateHomes(homes);
|
|
|
|
updateAreas(areas);
|
|
|
|
updatePlayersets(playerSets);
|
|
|
|
}
|
|
|
|
}, 100L, 100L);
|
|
|
|
}
|
|
|
|
|
2018-07-12 18:11:07 +02:00
|
|
|
// -------------------------------------------- //
|
|
|
|
// UPDATE: AREAS
|
|
|
|
// -------------------------------------------- //
|
|
|
|
|
2014-12-05 06:04:00 +01:00
|
|
|
// Thread Safe / Asynchronous: No
|
2014-12-07 21:26:13 +01:00
|
|
|
public boolean updateCore() {
|
2014-12-05 06:04:00 +01:00
|
|
|
// Get DynmapAPI
|
|
|
|
this.dynmapApi = (DynmapAPI) Bukkit.getPluginManager().getPlugin("dynmap");
|
2014-12-07 21:26:13 +01:00
|
|
|
if (this.dynmapApi == null) {
|
2014-12-05 06:04:00 +01:00
|
|
|
severe("Could not retrieve the DynmapAPI.");
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Get MarkerAPI
|
|
|
|
this.markerApi = this.dynmapApi.getMarkerAPI();
|
2014-12-07 21:26:13 +01:00
|
|
|
if (this.markerApi == null) {
|
2014-12-05 06:04:00 +01:00
|
|
|
severe("Could not retrieve the MarkerAPI.");
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Thread Safe / Asynchronous: Yes
|
2014-12-07 21:26:13 +01:00
|
|
|
public TempMarkerSet createLayer() {
|
2014-12-05 06:04:00 +01:00
|
|
|
TempMarkerSet ret = new TempMarkerSet();
|
|
|
|
ret.label = Conf.dynmapLayerName;
|
|
|
|
ret.minimumZoom = Conf.dynmapLayerMinimumZoom;
|
|
|
|
ret.priority = Conf.dynmapLayerPriority;
|
2018-11-07 17:14:42 +01:00
|
|
|
ret.hideByDefault = !Conf.dynmapLayerVisible;
|
2014-12-05 06:04:00 +01:00
|
|
|
return ret;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Thread Safe / Asynchronous: No
|
2014-12-07 21:26:13 +01:00
|
|
|
public boolean updateLayer(TempMarkerSet temp) {
|
2014-12-05 06:04:00 +01:00
|
|
|
this.markerset = this.markerApi.getMarkerSet(FACTIONS_MARKERSET);
|
2014-12-07 21:26:13 +01:00
|
|
|
if (this.markerset == null) {
|
2014-12-05 06:04:00 +01:00
|
|
|
this.markerset = temp.create(this.markerApi, FACTIONS_MARKERSET);
|
2014-12-07 21:26:13 +01:00
|
|
|
if (this.markerset == null) {
|
2014-12-05 06:04:00 +01:00
|
|
|
severe("Could not create the Faction Markerset/Layer");
|
|
|
|
return false;
|
|
|
|
}
|
2014-12-07 21:26:13 +01:00
|
|
|
} else {
|
2014-12-05 06:04:00 +01:00
|
|
|
temp.update(this.markerset);
|
|
|
|
}
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Thread Safe / Asynchronous: Yes
|
2014-12-07 21:26:13 +01:00
|
|
|
public Map<String, TempMarker> createHomes() {
|
2017-12-19 11:18:13 +01:00
|
|
|
Map<String, TempMarker> ret = new HashMap<>();
|
2014-12-05 06:04:00 +01:00
|
|
|
|
|
|
|
// Loop current factions
|
2014-12-07 21:26:13 +01:00
|
|
|
for (Faction faction : Factions.getInstance().getAllFactions()) {
|
2014-12-05 06:04:00 +01:00
|
|
|
Location ps = faction.getHome();
|
2014-12-07 21:26:13 +01:00
|
|
|
if (ps == null) {
|
|
|
|
continue;
|
|
|
|
}
|
2014-12-05 06:04:00 +01:00
|
|
|
|
|
|
|
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.
|
2014-12-07 21:26:13 +01:00
|
|
|
public void updateHomes(Map<String, TempMarker> homes) {
|
2014-12-05 06:04:00 +01:00
|
|
|
// Put all current faction markers in a map
|
2017-12-19 11:18:13 +01:00
|
|
|
Map<String, Marker> markers = new HashMap<>();
|
2014-12-07 21:26:13 +01:00
|
|
|
for (Marker marker : this.markerset.getMarkers()) {
|
2014-12-05 06:04:00 +01:00
|
|
|
markers.put(marker.getMarkerID(), marker);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Loop homes
|
2014-12-07 21:26:13 +01:00
|
|
|
for (Entry<String, TempMarker> entry : homes.entrySet()) {
|
2014-12-05 06:04:00 +01:00
|
|
|
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);
|
2014-12-07 21:26:13 +01:00
|
|
|
if (marker == null) {
|
2014-12-05 06:04:00 +01:00
|
|
|
marker = temp.create(this.markerApi, this.markerset, markerId);
|
2014-12-07 21:26:13 +01:00
|
|
|
if (marker == null) {
|
2014-12-05 06:04:00 +01:00
|
|
|
EngineDynmap.severe("Could not get/create the home marker " + markerId);
|
|
|
|
}
|
2014-12-07 21:26:13 +01:00
|
|
|
} else {
|
2014-12-05 06:04:00 +01:00
|
|
|
temp.update(this.markerApi, marker);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Delete Deprecated Markers
|
|
|
|
// Only old markers should now be left
|
2014-12-07 21:26:13 +01:00
|
|
|
for (Marker marker : markers.values()) {
|
2014-12-05 06:04:00 +01:00
|
|
|
marker.deleteMarker();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// -------------------------------------------- //
|
2018-07-12 18:11:07 +02:00
|
|
|
// UPDATE: PLAYERSET
|
2014-12-05 06:04:00 +01:00
|
|
|
// -------------------------------------------- //
|
|
|
|
|
|
|
|
// Thread Safe: YES
|
2014-12-07 21:26:13 +01:00
|
|
|
public Map<String, TempAreaMarker> createAreas() {
|
2014-12-05 06:04:00 +01:00
|
|
|
Map<String, Map<Faction, Set<FLocation>>> worldFactionChunks = createWorldFactionChunks();
|
|
|
|
return createAreas(worldFactionChunks);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Thread Safe: YES
|
2014-12-07 21:26:13 +01:00
|
|
|
public Map<String, Map<Faction, Set<FLocation>>> createWorldFactionChunks() {
|
2014-12-05 06:04:00 +01:00
|
|
|
// Create map "world name --> faction --> set of chunk coords"
|
2017-12-19 11:18:13 +01:00
|
|
|
Map<String, Map<Faction, Set<FLocation>>> worldFactionChunks = new HashMap<>();
|
2014-12-05 06:04:00 +01:00
|
|
|
|
|
|
|
// Note: The board is the world. The board id is the world name.
|
|
|
|
MemoryBoard board = (MemoryBoard) Board.getInstance();
|
|
|
|
|
2014-12-07 21:26:13 +01:00
|
|
|
for (Entry<FLocation, String> entry : board.flocationIds.entrySet()) {
|
2014-12-05 06:04:00 +01:00
|
|
|
String world = entry.getKey().getWorldName();
|
|
|
|
Faction chunkOwner = Factions.getInstance().getFactionById(entry.getValue());
|
|
|
|
|
|
|
|
Map<Faction, Set<FLocation>> factionChunks = worldFactionChunks.get(world);
|
2014-12-07 21:26:13 +01:00
|
|
|
if (factionChunks == null) {
|
2017-12-19 11:18:13 +01:00
|
|
|
factionChunks = new HashMap<>();
|
2014-12-05 06:04:00 +01:00
|
|
|
worldFactionChunks.put(world, factionChunks);
|
|
|
|
}
|
|
|
|
|
|
|
|
Set<FLocation> factionTerritory = factionChunks.get(chunkOwner);
|
2014-12-07 21:26:13 +01:00
|
|
|
if (factionTerritory == null) {
|
2017-12-19 11:18:13 +01:00
|
|
|
factionTerritory = new HashSet<>();
|
2014-12-05 06:04:00 +01:00
|
|
|
factionChunks.put(chunkOwner, factionTerritory);
|
|
|
|
}
|
|
|
|
|
|
|
|
factionTerritory.add(entry.getKey());
|
|
|
|
}
|
|
|
|
|
|
|
|
return worldFactionChunks;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Thread Safe: YES
|
2014-12-07 21:26:13 +01:00
|
|
|
public Map<String, TempAreaMarker> createAreas(Map<String, Map<Faction, Set<FLocation>>> worldFactionChunks) {
|
2017-12-19 11:18:13 +01:00
|
|
|
Map<String, TempAreaMarker> ret = new HashMap<>();
|
2014-12-05 06:04:00 +01:00
|
|
|
|
|
|
|
// For each world
|
2014-12-07 21:26:13 +01:00
|
|
|
for (Entry<String, Map<Faction, Set<FLocation>>> entry : worldFactionChunks.entrySet()) {
|
2014-12-05 06:04:00 +01:00
|
|
|
String world = entry.getKey();
|
|
|
|
Map<Faction, Set<FLocation>> factionChunks = entry.getValue();
|
|
|
|
|
|
|
|
// For each faction and its chunks in that world
|
2014-12-07 21:26:13 +01:00
|
|
|
for (Entry<Faction, Set<FLocation>> entry1 : factionChunks.entrySet()) {
|
2014-12-05 06:04:00 +01:00
|
|
|
Faction faction = entry1.getKey();
|
|
|
|
Set<FLocation> chunks = entry1.getValue();
|
|
|
|
Map<String, TempAreaMarker> worldFactionMarkers = createAreas(world, faction, chunks);
|
|
|
|
ret.putAll(worldFactionMarkers);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return ret;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Thread Safe: YES
|
|
|
|
// Handle specific faction on specific world
|
|
|
|
// "handle faction on world"
|
2014-12-07 21:26:13 +01:00
|
|
|
public Map<String, TempAreaMarker> createAreas(String world, Faction faction, Set<FLocation> chunks) {
|
2017-12-19 11:18:13 +01:00
|
|
|
Map<String, TempAreaMarker> ret = new HashMap<>();
|
2014-12-05 06:04:00 +01:00
|
|
|
|
|
|
|
// If the faction is visible ...
|
2018-11-07 17:14:42 +01:00
|
|
|
if (!isVisible(faction, world)) {
|
2014-12-07 21:26:13 +01:00
|
|
|
return ret;
|
|
|
|
}
|
2014-12-05 06:04:00 +01:00
|
|
|
|
|
|
|
// ... and has any chunks ...
|
2014-12-07 21:26:13 +01:00
|
|
|
if (chunks.isEmpty()) {
|
|
|
|
return ret;
|
|
|
|
}
|
2014-12-05 06:04:00 +01:00
|
|
|
|
|
|
|
// 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();
|
2017-12-19 11:18:13 +01:00
|
|
|
LinkedList<FLocation> allChunks = new LinkedList<>();
|
2014-12-07 21:26:13 +01:00
|
|
|
for (FLocation chunk : chunks) {
|
|
|
|
allChunkFlags.setFlag((int) chunk.getX(), (int) chunk.getZ(), true); // Set flag for chunk
|
2014-12-05 06:04:00 +01:00
|
|
|
allChunks.addLast(chunk);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Loop through until we don't find more areas
|
2014-12-07 21:26:13 +01:00
|
|
|
while (allChunks != null) {
|
2014-12-05 06:04:00 +01:00
|
|
|
TileFlags ourChunkFlags = null;
|
|
|
|
LinkedList<FLocation> ourChunks = null;
|
|
|
|
LinkedList<FLocation> newChunks = null;
|
|
|
|
|
|
|
|
int minimumX = Integer.MAX_VALUE;
|
|
|
|
int minimumZ = Integer.MAX_VALUE;
|
2014-12-07 21:26:13 +01:00
|
|
|
for (FLocation chunk : allChunks) {
|
|
|
|
int chunkX = (int) chunk.getX();
|
|
|
|
int chunkZ = (int) chunk.getZ();
|
2014-12-05 06:04:00 +01:00
|
|
|
|
|
|
|
// If we need to start shape, and this block is not part of one yet
|
2014-12-07 21:26:13 +01:00
|
|
|
if (ourChunkFlags == null && allChunkFlags.getFlag(chunkX, chunkZ)) {
|
2014-12-05 06:04:00 +01:00
|
|
|
ourChunkFlags = new TileFlags(); // Create map for shape
|
2017-12-19 11:18:13 +01:00
|
|
|
ourChunks = new LinkedList<>();
|
2014-12-05 06:04:00 +01:00
|
|
|
floodFillTarget(allChunkFlags, ourChunkFlags, chunkX, chunkZ); // Copy shape
|
|
|
|
ourChunks.add(chunk); // Add it to our chunk list
|
|
|
|
minimumX = chunkX;
|
|
|
|
minimumZ = chunkZ;
|
|
|
|
}
|
|
|
|
// If shape found, and we're in it, add to our node list
|
2014-12-07 21:26:13 +01:00
|
|
|
else if (ourChunkFlags != null && ourChunkFlags.getFlag(chunkX, chunkZ)) {
|
2014-12-05 06:04:00 +01:00
|
|
|
ourChunks.add(chunk);
|
2014-12-07 21:26:13 +01:00
|
|
|
if (chunkX < minimumX) {
|
2014-12-05 06:04:00 +01:00
|
|
|
minimumX = chunkX;
|
|
|
|
minimumZ = chunkZ;
|
2014-12-07 21:26:13 +01:00
|
|
|
} else if (chunkX == minimumX && chunkZ < minimumZ) {
|
2014-12-05 06:04:00 +01:00
|
|
|
minimumZ = chunkZ;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// Else, keep it in the list for the next polygon
|
2014-12-07 21:26:13 +01:00
|
|
|
else {
|
|
|
|
if (newChunks == null) {
|
2017-12-19 11:18:13 +01:00
|
|
|
newChunks = new LinkedList<>();
|
2014-12-07 21:26:13 +01:00
|
|
|
}
|
2014-12-05 06:04:00 +01:00
|
|
|
newChunks.add(chunk);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Replace list (null if no more to process)
|
|
|
|
allChunks = newChunks;
|
|
|
|
|
2014-12-07 21:26:13 +01:00
|
|
|
if (ourChunkFlags == null) {
|
|
|
|
continue;
|
|
|
|
}
|
2014-12-05 06:04:00 +01:00
|
|
|
|
|
|
|
// Trace outline of blocks - start from minx, minz going to x+
|
|
|
|
int initialX = minimumX;
|
|
|
|
int initialZ = minimumZ;
|
|
|
|
int currentX = minimumX;
|
|
|
|
int currentZ = minimumZ;
|
|
|
|
Direction direction = Direction.XPLUS;
|
2017-12-19 11:18:13 +01:00
|
|
|
ArrayList<int[]> linelist = new ArrayList<>();
|
2014-12-07 21:26:13 +01:00
|
|
|
linelist.add(new int[]{initialX, initialZ}); // Add start point
|
|
|
|
while ((currentX != initialX) || (currentZ != initialZ) || (direction != Direction.ZMINUS)) {
|
|
|
|
switch (direction) {
|
2014-12-05 06:04:00 +01:00
|
|
|
case XPLUS: // Segment in X+ direction
|
2018-11-07 17:14:42 +01:00
|
|
|
if (!ourChunkFlags.getFlag(currentX + 1, currentZ)) { // Right turn?
|
2014-12-07 21:26:13 +01:00
|
|
|
linelist.add(new int[]{currentX + 1, currentZ}); // Finish line
|
2014-12-05 06:04:00 +01:00
|
|
|
direction = Direction.ZPLUS; // Change direction
|
2018-11-07 17:14:42 +01:00
|
|
|
} else if (!ourChunkFlags.getFlag(currentX + 1, currentZ - 1)) { // Straight?
|
2014-12-05 06:04:00 +01:00
|
|
|
currentX++;
|
2014-12-07 21:26:13 +01:00
|
|
|
} else { // Left turn
|
|
|
|
linelist.add(new int[]{currentX + 1, currentZ}); // Finish line
|
2014-12-05 06:04:00 +01:00
|
|
|
direction = Direction.ZMINUS;
|
|
|
|
currentX++;
|
|
|
|
currentZ--;
|
|
|
|
}
|
2014-12-07 21:26:13 +01:00
|
|
|
break;
|
2014-12-05 06:04:00 +01:00
|
|
|
case ZPLUS: // Segment in Z+ direction
|
2018-11-07 17:14:42 +01:00
|
|
|
if (!ourChunkFlags.getFlag(currentX, currentZ + 1)) { // Right turn?
|
2014-12-07 21:26:13 +01:00
|
|
|
linelist.add(new int[]{currentX + 1, currentZ + 1}); // Finish line
|
2014-12-05 06:04:00 +01:00
|
|
|
direction = Direction.XMINUS; // Change direction
|
2018-11-07 17:14:42 +01:00
|
|
|
} else if (!ourChunkFlags.getFlag(currentX + 1, currentZ + 1)) { // Straight?
|
2014-12-05 06:04:00 +01:00
|
|
|
currentZ++;
|
2014-12-07 21:26:13 +01:00
|
|
|
} else { // Left turn
|
|
|
|
linelist.add(new int[]{currentX + 1, currentZ + 1}); // Finish line
|
2014-12-05 06:04:00 +01:00
|
|
|
direction = Direction.XPLUS;
|
|
|
|
currentX++;
|
|
|
|
currentZ++;
|
|
|
|
}
|
2014-12-07 21:26:13 +01:00
|
|
|
break;
|
2014-12-05 06:04:00 +01:00
|
|
|
case XMINUS: // Segment in X- direction
|
2018-11-07 17:14:42 +01:00
|
|
|
if (!ourChunkFlags.getFlag(currentX - 1, currentZ)) { // Right turn?
|
2014-12-07 21:26:13 +01:00
|
|
|
linelist.add(new int[]{currentX, currentZ + 1}); // Finish line
|
2014-12-05 06:04:00 +01:00
|
|
|
direction = Direction.ZMINUS; // Change direction
|
2018-11-07 17:14:42 +01:00
|
|
|
} else if (!ourChunkFlags.getFlag(currentX - 1, currentZ + 1)) { // Straight?
|
2014-12-05 06:04:00 +01:00
|
|
|
currentX--;
|
2014-12-07 21:26:13 +01:00
|
|
|
} else { // Left turn
|
|
|
|
linelist.add(new int[]{currentX, currentZ + 1}); // Finish line
|
2014-12-05 06:04:00 +01:00
|
|
|
direction = Direction.ZPLUS;
|
|
|
|
currentX--;
|
|
|
|
currentZ++;
|
|
|
|
}
|
2014-12-07 21:26:13 +01:00
|
|
|
break;
|
2014-12-05 06:04:00 +01:00
|
|
|
case ZMINUS: // Segment in Z- direction
|
2018-11-07 17:14:42 +01:00
|
|
|
if (!ourChunkFlags.getFlag(currentX, currentZ - 1)) { // Right turn?
|
2014-12-07 21:26:13 +01:00
|
|
|
linelist.add(new int[]{currentX, currentZ}); // Finish line
|
2014-12-05 06:04:00 +01:00
|
|
|
direction = Direction.XPLUS; // Change direction
|
2018-11-07 17:14:42 +01:00
|
|
|
} else if (!ourChunkFlags.getFlag(currentX - 1, currentZ - 1)) { // Straight?
|
2014-12-05 06:04:00 +01:00
|
|
|
currentZ--;
|
2014-12-07 21:26:13 +01:00
|
|
|
} else { // Left turn
|
|
|
|
linelist.add(new int[]{currentX, currentZ}); // Finish line
|
2014-12-05 06:04:00 +01:00
|
|
|
direction = Direction.XMINUS;
|
|
|
|
currentX--;
|
|
|
|
currentZ--;
|
|
|
|
}
|
2014-12-07 21:26:13 +01:00
|
|
|
break;
|
2014-12-05 06:04:00 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
int sz = linelist.size();
|
|
|
|
double[] x = new double[sz];
|
|
|
|
double[] z = new double[sz];
|
2014-12-07 21:26:13 +01:00
|
|
|
for (int i = 0; i < sz; i++) {
|
2014-12-05 06:04:00 +01:00
|
|
|
int[] line = linelist.get(i);
|
|
|
|
x[i] = (double) line[0] * (double) BLOCKS_PER_CHUNK;
|
|
|
|
z[i] = (double) line[1] * (double) BLOCKS_PER_CHUNK;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Build information for specific area
|
|
|
|
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();
|
|
|
|
|
|
|
|
ret.put(markerId, temp);
|
|
|
|
|
|
|
|
markerIndex++;
|
|
|
|
}
|
|
|
|
|
|
|
|
return ret;
|
|
|
|
}
|
|
|
|
|
2018-07-12 18:11:07 +02:00
|
|
|
// -------------------------------------------- //
|
|
|
|
// UTIL & SHARED
|
|
|
|
// -------------------------------------------- //
|
|
|
|
|
2014-12-05 06:04:00 +01:00
|
|
|
// Thread Safe: NO
|
2014-12-07 21:26:13 +01:00
|
|
|
public void updateAreas(Map<String, TempAreaMarker> areas) {
|
2014-12-05 06:04:00 +01:00
|
|
|
// Map Current
|
2017-12-19 11:18:13 +01:00
|
|
|
Map<String, AreaMarker> markers = new HashMap<>();
|
2014-12-07 21:26:13 +01:00
|
|
|
for (AreaMarker marker : this.markerset.getAreaMarkers()) {
|
2014-12-05 06:04:00 +01:00
|
|
|
markers.put(marker.getMarkerID(), marker);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Loop New
|
2014-12-07 21:26:13 +01:00
|
|
|
for (Entry<String, TempAreaMarker> entry : areas.entrySet()) {
|
2014-12-05 06:04:00 +01:00
|
|
|
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);
|
2014-12-07 21:26:13 +01:00
|
|
|
if (marker == null) {
|
2014-12-05 06:04:00 +01:00
|
|
|
marker = temp.create(this.markerset, markerId);
|
2014-12-07 21:26:13 +01:00
|
|
|
if (marker == null) {
|
2014-12-05 06:04:00 +01:00
|
|
|
severe("Could not get/create the area marker " + markerId);
|
|
|
|
}
|
2014-12-07 21:26:13 +01:00
|
|
|
} else {
|
2014-12-05 06:04:00 +01:00
|
|
|
temp.update(marker);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Only old/outdated should now be left. Delete them.
|
2014-12-07 21:26:13 +01:00
|
|
|
for (AreaMarker marker : markers.values()) {
|
2014-12-05 06:04:00 +01:00
|
|
|
marker.deleteMarker();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Thread Safe / Asynchronous: Yes
|
2014-12-07 21:26:13 +01:00
|
|
|
public String createPlayersetId(Faction faction) {
|
|
|
|
if (faction == null) {
|
|
|
|
return null;
|
|
|
|
}
|
2015-09-08 18:46:48 +02:00
|
|
|
if (faction.isWilderness()) {
|
2014-12-07 21:26:13 +01:00
|
|
|
return null;
|
|
|
|
}
|
2014-12-05 06:04:00 +01:00
|
|
|
String factionId = faction.getId();
|
2014-12-07 21:26:13 +01:00
|
|
|
if (factionId == null) {
|
|
|
|
return null;
|
|
|
|
}
|
2014-12-05 06:04:00 +01:00
|
|
|
return FACTIONS_PLAYERSET_ + factionId;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Thread Safe / Asynchronous: Yes
|
2014-12-07 21:26:13 +01:00
|
|
|
public Set<String> createPlayerset(Faction faction) {
|
|
|
|
if (faction == null) {
|
|
|
|
return null;
|
|
|
|
}
|
2015-09-08 18:46:48 +02:00
|
|
|
if (faction.isWilderness()) {
|
2014-12-07 21:26:13 +01:00
|
|
|
return null;
|
|
|
|
}
|
2014-12-05 06:04:00 +01:00
|
|
|
|
2017-12-19 11:18:13 +01:00
|
|
|
Set<String> ret = new HashSet<>();
|
2014-12-05 06:04:00 +01:00
|
|
|
|
2014-12-07 21:26:13 +01:00
|
|
|
for (FPlayer fplayer : faction.getFPlayers()) {
|
2014-12-05 06:04:00 +01:00
|
|
|
// 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
|
2014-12-07 21:26:13 +01:00
|
|
|
public Map<String, Set<String>> createPlayersets() {
|
2018-11-07 17:14:42 +01:00
|
|
|
if (!Conf.dynmapVisibilityByFaction) {
|
2014-12-05 06:04:00 +01:00
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2017-12-19 11:18:13 +01:00
|
|
|
Map<String, Set<String>> ret = new HashMap<>();
|
2014-12-05 06:04:00 +01:00
|
|
|
|
2014-12-07 21:26:13 +01:00
|
|
|
for (Faction faction : Factions.getInstance().getAllFactions()) {
|
2014-12-05 06:04:00 +01:00
|
|
|
String playersetId = createPlayersetId(faction);
|
2014-12-07 21:26:13 +01:00
|
|
|
if (playersetId == null) {
|
|
|
|
continue;
|
|
|
|
}
|
2014-12-05 06:04:00 +01:00
|
|
|
Set<String> playerIds = createPlayerset(faction);
|
2014-12-07 21:26:13 +01:00
|
|
|
if (playerIds == null) {
|
|
|
|
continue;
|
|
|
|
}
|
2014-12-05 06:04:00 +01:00
|
|
|
ret.put(playersetId, playerIds);
|
|
|
|
}
|
|
|
|
|
|
|
|
return ret;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Thread Safe / Asynchronous: No
|
2014-12-07 21:26:13 +01:00
|
|
|
public void updatePlayersets(Map<String, Set<String>> playersets) {
|
|
|
|
if (playersets == null) {
|
2014-12-05 06:04:00 +01:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Remove
|
2014-12-07 21:26:13 +01:00
|
|
|
for (PlayerSet set : this.markerApi.getPlayerSets()) {
|
2018-11-07 17:14:42 +01:00
|
|
|
if (!set.getSetID().startsWith(FACTIONS_PLAYERSET_)) {
|
2014-12-07 21:26:13 +01:00
|
|
|
continue;
|
|
|
|
}
|
2014-12-05 06:04:00 +01:00
|
|
|
|
|
|
|
// (Null means remove all)
|
2014-12-07 21:26:13 +01:00
|
|
|
if (playersets.containsKey(set.getSetID())) {
|
|
|
|
continue;
|
|
|
|
}
|
2014-12-05 06:04:00 +01:00
|
|
|
|
|
|
|
set.deleteSet();
|
|
|
|
}
|
|
|
|
|
|
|
|
// Add / Update
|
2014-12-07 21:26:13 +01:00
|
|
|
for (Entry<String, Set<String>> entry : playersets.entrySet()) {
|
2014-12-05 06:04:00 +01:00
|
|
|
// Extract from Entry
|
|
|
|
String setId = entry.getKey();
|
|
|
|
Set<String> playerIds = entry.getValue();
|
|
|
|
|
|
|
|
// Get Creatively
|
|
|
|
PlayerSet set = this.markerApi.getPlayerSet(setId);
|
2014-12-07 21:26:13 +01:00
|
|
|
if (set == null) {
|
|
|
|
set = this.markerApi.createPlayerSet(setId, // id
|
2018-01-05 02:40:27 +01:00
|
|
|
true, // symmetric
|
|
|
|
playerIds, // players
|
|
|
|
false // persistent
|
2014-12-07 21:26:13 +01:00
|
|
|
);
|
|
|
|
}
|
|
|
|
if (set == null) {
|
2014-12-05 06:04:00 +01:00
|
|
|
severe("Could not get/create the player set " + setId);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Set Content
|
|
|
|
set.setPlayers(playerIds);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Thread Safe / Asynchronous: Yes
|
2014-12-07 21:26:13 +01:00
|
|
|
private String getDescription(Faction faction) {
|
2014-12-05 06:04:00 +01:00
|
|
|
String ret = "<div class=\"regioninfo\">" + Conf.dynmapDescription + "</div>";
|
|
|
|
|
|
|
|
// Name
|
|
|
|
String name = faction.getTag();
|
2019-01-27 03:41:25 +01:00
|
|
|
name = escapeHtml(ChatColor.stripColor(name));
|
2014-12-05 06:04:00 +01:00
|
|
|
ret = ret.replace("%name%", name);
|
|
|
|
|
|
|
|
// Description
|
|
|
|
String description = faction.getDescription();
|
2019-01-27 03:41:25 +01:00
|
|
|
description = escapeHtml(ChatColor.stripColor(description));
|
2014-12-05 06:04:00 +01:00
|
|
|
ret = ret.replace("%description%", description);
|
|
|
|
|
|
|
|
// Money
|
|
|
|
|
|
|
|
String money = "unavailable";
|
2019-01-27 03:41:25 +01:00
|
|
|
if (Conf.bankEnabled && Conf.dynmapDescriptionMoney)
|
2014-12-05 06:04:00 +01:00
|
|
|
money = String.format("%.2f", Econ.getBalance(faction.getAccountId()));
|
|
|
|
ret = ret.replace("%money%", money);
|
|
|
|
|
|
|
|
|
|
|
|
// Players
|
|
|
|
Set<FPlayer> playersList = faction.getFPlayers();
|
|
|
|
String playersCount = String.valueOf(playersList.size());
|
|
|
|
String players = getHtmlPlayerString(playersList);
|
|
|
|
|
|
|
|
FPlayer playersLeaderObject = faction.getFPlayerAdmin();
|
|
|
|
String playersLeader = getHtmlPlayerName(playersLeaderObject);
|
|
|
|
|
2019-01-27 03:41:25 +01:00
|
|
|
ArrayList<FPlayer> playersCoAdminsList = faction.getFPlayersWhereRole(Role.COLEADER);
|
|
|
|
String playersCoAdminsCount = String.valueOf(playersCoAdminsList.size());
|
|
|
|
String playersCoAdmins = getHtmlPlayerString(playersCoAdminsList);
|
2014-12-05 06:04:00 +01:00
|
|
|
|
|
|
|
ArrayList<FPlayer> playersModeratorsList = faction.getFPlayersWhereRole(Role.MODERATOR);
|
|
|
|
String playersModeratorsCount = String.valueOf(playersModeratorsList.size());
|
|
|
|
String playersModerators = getHtmlPlayerString(playersModeratorsList);
|
|
|
|
|
|
|
|
|
|
|
|
ArrayList<FPlayer> 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);
|
2019-01-27 03:41:25 +01:00
|
|
|
ret = ret.replace("%players.admins%", playersCoAdmins);
|
|
|
|
ret = ret.replace("%players.admins.count%", playersCoAdminsCount);
|
2014-12-05 06:04:00 +01:00
|
|
|
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
|
2014-12-07 21:26:13 +01:00
|
|
|
private boolean isVisible(Faction faction, String world) {
|
|
|
|
if (faction == null) {
|
|
|
|
return false;
|
|
|
|
}
|
2014-12-05 06:04:00 +01:00
|
|
|
final String factionId = faction.getId();
|
2014-12-07 21:26:13 +01:00
|
|
|
if (factionId == null) {
|
|
|
|
return false;
|
|
|
|
}
|
2014-12-05 06:04:00 +01:00
|
|
|
final String factionName = faction.getTag();
|
2014-12-07 21:26:13 +01:00
|
|
|
if (factionName == null) {
|
|
|
|
return false;
|
|
|
|
}
|
2014-12-05 06:04:00 +01:00
|
|
|
|
|
|
|
Set<String> visible = Conf.dynmapVisibleFactions;
|
|
|
|
Set<String> hidden = Conf.dynmapHiddenFactions;
|
|
|
|
|
2018-11-07 17:14:42 +01:00
|
|
|
if (!visible.isEmpty() && !visible.contains(factionId) && !visible.contains(factionName) && !visible.contains("world:" + world)) {
|
2014-12-05 06:04:00 +01:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2018-11-07 17:14:42 +01:00
|
|
|
return !hidden.contains(factionId) && !hidden.contains(factionName) && !hidden.contains("world:" + world);
|
2014-12-05 06:04:00 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// Thread Safe / Asynchronous: Yes
|
2014-12-07 21:26:13 +01:00
|
|
|
public DynmapStyle getStyle(Faction faction) {
|
2014-12-05 06:04:00 +01:00
|
|
|
DynmapStyle ret;
|
|
|
|
|
|
|
|
ret = Conf.dynmapFactionStyles.get(faction.getId());
|
2014-12-07 21:26:13 +01:00
|
|
|
if (ret != null) {
|
|
|
|
return ret;
|
|
|
|
}
|
2014-12-05 06:04:00 +01:00
|
|
|
|
|
|
|
ret = Conf.dynmapFactionStyles.get(faction.getTag());
|
2014-12-07 21:26:13 +01:00
|
|
|
if (ret != null) {
|
|
|
|
return ret;
|
|
|
|
}
|
2014-12-05 06:04:00 +01:00
|
|
|
|
|
|
|
return Conf.dynmapDefaultStyle;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Find all contiguous blocks, set in target and clear in source
|
2014-12-07 21:26:13 +01:00
|
|
|
private int floodFillTarget(TileFlags source, TileFlags destination, int x, int y) {
|
2014-12-05 06:04:00 +01:00
|
|
|
int cnt = 0;
|
2017-12-19 11:18:13 +01:00
|
|
|
ArrayDeque<int[]> stack = new ArrayDeque<>();
|
2014-12-07 21:26:13 +01:00
|
|
|
stack.push(new int[]{x, y});
|
2014-12-05 06:04:00 +01:00
|
|
|
|
2018-11-07 17:14:42 +01:00
|
|
|
while (!stack.isEmpty()) {
|
2014-12-05 06:04:00 +01:00
|
|
|
int[] nxt = stack.pop();
|
|
|
|
x = nxt[0];
|
|
|
|
y = nxt[1];
|
2014-12-07 21:26:13 +01:00
|
|
|
if (source.getFlag(x, y)) { // Set in src
|
2014-12-05 06:04:00 +01:00
|
|
|
source.setFlag(x, y, false); // Clear source
|
|
|
|
destination.setFlag(x, y, true); // Set in destination
|
|
|
|
cnt++;
|
2014-12-07 21:26:13 +01:00
|
|
|
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});
|
|
|
|
}
|
2014-12-05 06:04:00 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return cnt;
|
|
|
|
}
|
2018-07-12 18:11:07 +02:00
|
|
|
|
|
|
|
enum Direction {
|
|
|
|
XPLUS, ZPLUS, XMINUS, ZMINUS
|
|
|
|
}
|
2014-12-05 06:04:00 +01:00
|
|
|
}
|