Compare commits

..

No commits in common. "master" and "v0.2.99-rc3" have entirely different histories.

18 changed files with 142 additions and 450 deletions

View File

@ -1,15 +0,0 @@
pipeline:
build:
image: maven:3-openjdk-18-slim
commands:
- mvn package
release:
image: plugins/gitea-release
settings:
api_key:
from_secret: GITEA_KEY
base_url: https://gitea.malloc.hackerbots.net/
files:
- ./target/*.jar
when:
event: tag

View File

@ -1,71 +1 @@
# Regions
A plugin to carve up your minecraft world into named regions.
For most of minecraftian history, players, server owners, and content builders
have sought to find a way around one of the least exciting problems in
minecraft: How to travel long distances on a big world.
In 2010, Mojang gave us minecarts. Using some clever physics glitches,
minecrafters devised minecart boosters to send them through distant lands at a
modest speed.
Later that year, we all set sail for the infinite seas upon our new boats.
Travel was swift, provided water.
Soon after, we were blessed with the bright magicks of redstone and powered
rails. No longer did minecrafters need to rely on janky collision physics to
move ourselves through the non-aquatic world.
With the Beta 1.9 release, Ender pearls and speed potions were introduced. We
catapulted ourselves to terrifying new heights and found the world that much
smaller.
After the Beta era, Mojang bestowed upon us a terrifying and awesome power:
Nether portals. Soon long distance travel was a reasonable idea, if you didn't
mind losing your entire inventory to an errant ghast or lava pool.
The 1.9 update allowed us to take to the sky with Elytra, and 1.11 sent us into
the distant horizon with firework rockets. With enough determination, a compass,
and some gunpowder, the world was all that much smaller to us.
And yet, we remain unsatiated. Dissatisfied with the high cost of elytra and the
regular need to move great distances quickly, a great number of server plugins
that included teleportation proliferated the pages of spigotmc.org.
And yet still, we remain dissatisfied. Typing out a /warp or /home command is
trivial. Instantaneous teleportation at your fingertips might sound great, but
there remains a distinct un-minecraftian feel about it.
What if there was a more immersive way to add fast travel to your server?
What if your players didn't need to do impossible feats like scrying some runes
into a "chat box", something out of place from the minecraft world?
For your consideration: **Regions**
## Features
- Point-to-point teleportation with a GUI
![Teleportation GUI](docs/use-region-post.gif)
- Create a point of interest in your world, give it a name, apply a banner.
![Creation Animation](docs/create-region-post.gif)
- Fast travel routes between POIs are automatically established.
- Create World Hubs, accessable from any other POI on the world
- Players can only travel to POI's they've already explored
![Discovery Animation](docs/discover-region-post.gif)
- An incredibly cool and flashy teleportation effect
- Pay for your fast travel with XP levels
- Attempting to jump without enough XP might lead to a dangerous misfire,
dropping you an unexpected distance from your destination.
- Level up your region posts with craftable Region Post Charges
- Not enough XP? No worries, you can pay for the ticket with a post's stored
charges
- Craft a Region Compass to locate the nearest region post
- Wrap a latern in charges to create an anchor with which any player can create
their own local region post
![Crafting](docs/craft-region-items.gif)
- Restrict any of the above features using permissions
- Get a spiffy notification whenever you cross a region's border and enter a new
land
- Dynmap integration
- Asynchronous chunk loading and teleportation on Paper servers that all but
eliminates teleportation-induced lag

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 MiB

31
pom.xml
View File

@ -4,24 +4,17 @@
<groupId>us.camin.regions</groupId>
<artifactId>Regions</artifactId>
<packaging>jar</packaging>
<version>0.4.0</version>
<version>0.2.99-rc3</version>
<name>regions</name>
<url>http://maven.apache.org</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.bstats</groupId>
<artifactId>bstats-bukkit</artifactId>
<version>2.2.1</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.comphenix.protocol</groupId>
<artifactId>ProtocolLib</artifactId>
<version>4.7.0-SNAPSHOT</version>
<scope>provided</scope>
<version>4.6.0</version>
</dependency>
<dependency>
<groupId>io.papermc</groupId>
@ -46,17 +39,17 @@
<version>2.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.github.angeschossen</groupId>
<artifactId>LandsAPI</artifactId>
<version>6.15.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.4</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.10</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<extensions>
@ -93,10 +86,6 @@
<pattern>io.papermc.lib</pattern>
<shadedPattern>us.camin.regions.paperlib</shadedPattern> <!-- Replace this -->
</relocation>
<relocation>
<pattern>org.bstats</pattern>
<shadedPattern>us.camin.regions.bstats</shadedPattern>
</relocation>
</relocations>
</configuration>
<executions>
@ -177,10 +166,6 @@
<id>papermc</id>
<url>http://papermc.io/repo/repository/maven-public/</url>
</repository>
<repository>
<id>jitpack.io</id>
<url>https://jitpack.io</url>
</repository>
<repository><id>dynmap-repo</id><url>http://repo.mikeprimm.com/</url></repository>
<repository><id>imagej</id><url>http://maven.imagej.net/content/repositories/public/</url></repository>
</repositories>

View File

@ -71,7 +71,7 @@ public class DynmapEventRelay implements Listener {
}
}
private void updatePolygons(World world) {
public void updatePolygons(World world) {
List<GenericMarker> oldMarkers = m_borderMarkers.get(world);
if (oldMarkers != null) {
for(GenericMarker marker : oldMarkers) {
@ -91,10 +91,17 @@ public class DynmapEventRelay implements Listener {
log.info("Could not generate polygon for region " + region.name());
continue;
}
AreaMarker marker = m_borderSet.createAreaMarker(null, region.name(), false, world.getName(), polygon.x, polygon.z, false);
marker.setFillStyle(0.7, region.color().getColor().asRGB());
marker.setLineStyle(2, 0.8, region.color().getColor().asRGB());
m_borderMarkers.get(world).add(marker);
boolean isFrontier = geom.isFrontier(region);
if (!isFrontier) {
AreaMarker marker = m_borderSet.createAreaMarker(null, region.name(), false, world.getName(), polygon.x, polygon.z, false);
marker.setFillStyle(0.7, region.color().getColor().asRGB());
marker.setLineStyle(2, 0.8, region.color().getColor().asRGB());
m_borderMarkers.get(world).add(marker);
} else {
PolyLineMarker marker = m_borderSet.createPolyLineMarker(null, region.name(), false, world.getName(), polygon.x, polygon.y, polygon.z, false);
marker.setLineStyle(2, 0.5, region.color().getColor().asRGB());
m_borderMarkers.get(world).add(marker);
}
// Add a line between each region, for teleportations
double thickness = Math.max(1, Math.log(geom.neighbors(region).size()) * 2.75);
@ -102,13 +109,9 @@ public class DynmapEventRelay implements Listener {
double x[] = { neighbor.location().getBlockX(), region.location().getBlockX() };
double y[] = { 64, 64 };
double z[] = { neighbor.location().getBlockZ(), region.location().getBlockZ() };
String label = neighbor.name() + " / " + region.name();
String description = "<p>Travel Cost to " + neighbor.name() + ": " + region.getTravelCost(neighbor) + "</p>";
description += "<p>Travel Cost to " + region.name() + ": " + neighbor.getTravelCost(region) + "</p>";
PolyLineMarker routeMarker = m_routesSet.createPolyLineMarker(null, label, true, world.getName(), x, y, z, false);
routeMarker.setDescription(description);
routeMarker.setLineStyle((int)Math.ceil(thickness), 0.5, region.color().getColor().asRGB());
m_borderMarkers.get(world).add(routeMarker);
PolyLineMarker marker = m_routesSet.createPolyLineMarker(null, null, false, world.getName(), x, y, z, false);
marker.setLineStyle((int)Math.ceil(thickness), 0.5, region.color().getColor().asRGB());
m_borderMarkers.get(world).add(marker);
}
}
}
@ -116,17 +119,11 @@ public class DynmapEventRelay implements Listener {
private void createMarkerForRegion(Region region) {
Location loc = region.location();
MarkerIcon icon = m_api.getMarkerIcon("compass");
if (region.isHub()) {
icon = m_api.getMarkerIcon("world");
}
CircleMarker circleMarker = m_routesSet.createCircleMarker(null, region.name(), false, loc.getWorld().getName(), loc.getX(), loc.getY(), loc.getZ(), 60, 60, false);
Marker marker = m_centerSet.createMarker(null, region.name(), loc.getWorld().getName(), loc.getX(), loc.getY(), loc.getZ(), icon, false);
circleMarker.setFillStyle(0.75, region.color().getColor().asRGB());
circleMarker.setLineStyle(0, 0, 0);
String desc = "<h2>" + region.name() + "</h2>";
if (region.isHub()) {
desc += "<p><strong>World Hub</strong></p>";
}
marker.setDescription(desc);
circleMarker.setDescription(desc);
}

View File

@ -34,8 +34,8 @@ import com.comphenix.protocol.events.PacketContainer;
import com.comphenix.protocol.PacketType;
import com.comphenix.protocol.wrappers.EnumWrappers.TitleAction;
import com.comphenix.protocol.wrappers.WrappedChatComponent;
import com.comphenix.protocol.utility.MinecraftProtocolVersion;
import com.destroystokyo.paper.Title;
import java.util.logging.Logger;
import us.camin.regions.events.PlayerMoveInEvent;
@ -50,64 +50,9 @@ public class PlayerNotifier implements Listener {
Logger log = Logger.getLogger("Regions.PlayerNotifier");
RegionManager m_manager;
Plugin m_plugin;
ProtocolManager m_protocolManager;
int m_protoVersion = 0;
public PlayerNotifier(Plugin plugin, RegionManager manager) {
m_manager = manager;
m_plugin = plugin;
m_protocolManager = ProtocolLibrary.getProtocolManager();
if (m_protocolManager != null) {
m_protoVersion = MinecraftProtocolVersion.getCurrentVersion();
}
}
private void sendTitle(Player player, WrappedChatComponent title, WrappedChatComponent subtitle) {
// Title packet format changed in 1.17
if (m_protoVersion < 755) {
PacketContainer setTitle = m_protocolManager.createPacket(PacketType.Play.Server.TITLE);
setTitle.getChatComponents().write(0, title);
PacketContainer setSubtitle = m_protocolManager.createPacket(PacketType.Play.Server.TITLE);
setSubtitle.getChatComponents().write(0, subtitle);
setSubtitle.getTitleActions().write(0, TitleAction.SUBTITLE);
try {
m_protocolManager.sendServerPacket(player, setTitle);
m_protocolManager.sendServerPacket(player, setSubtitle);
} catch (Exception e) {
}
} else {
PacketContainer setTitle = m_protocolManager.createPacket(PacketType.Play.Server.SET_TITLE_TEXT);
setTitle.getChatComponents().write(0, title);
PacketContainer setSubtitle = m_protocolManager.createPacket(PacketType.Play.Server.SET_SUBTITLE_TEXT);
setSubtitle.getChatComponents().write(0, subtitle);
try {
m_protocolManager.sendServerPacket(player, setTitle);
m_protocolManager.sendServerPacket(player, setSubtitle);
} catch (Exception e) {
}
}
}
private void sendActionBar(Player player, WrappedChatComponent text) {
// Title packet format changed in 1.17
if (m_protoVersion < 755) {
PacketContainer setActionBar = m_protocolManager.createPacket(PacketType.Play.Server.TITLE);
setActionBar.getChatComponents().write(0, text);
setActionBar.getTitleActions().write(0, TitleAction.ACTIONBAR);
try {
m_protocolManager.sendServerPacket(player, setActionBar);
} catch (Exception e) {
}
} else {
PacketContainer setActionBar = m_protocolManager.createPacket(PacketType.Play.Server.SET_ACTION_BAR_TEXT);
setActionBar.getChatComponents().write(0, text);
try {
m_protocolManager.sendServerPacket(player, setActionBar);
} catch (Exception e) {
}
}
}
@EventHandler
@ -127,16 +72,30 @@ public class PlayerNotifier implements Listener {
int pop = m_plugin.playerWatcher().playersInRegion(event.newRegion).size();
Location center = event.newRegion.location();
int altitude = center.getBlockY();
if (m_protocolManager != null) {
ProtocolManager protocolManager = ProtocolLibrary.getProtocolManager();
if (protocolManager != null) {
PacketContainer chatMessage = protocolManager.createPacket(PacketType.Play.Server.TITLE);
String colorHex = "#" + String.format("%06X", event.newRegion.color().getColor().asRGB());
WrappedChatComponent titleComponent = WrappedChatComponent.fromJson("{text:\"" + event.newRegion.name() + "\", color: \""+colorHex+"\"}");
WrappedChatComponent subtitleComponent = WrappedChatComponent.fromJson("{text:\"Population: " + pop + " Altitude: "+ altitude + "\", color: \"#ffffff\"}");
WrappedChatComponent actionBarComponent = WrappedChatComponent.fromJson("{text:\"Now entering " + event.newRegion.name() + "\", color: \""+colorHex+"\"}");
sendTitle(event.player, titleComponent, subtitleComponent);
sendActionBar(event.player, actionBarComponent);
chatMessage.getChatComponents().write(0, WrappedChatComponent.fromJson("{text:\"" + event.newRegion.name() + "\", color: \""+colorHex+"\"}"));
try {
protocolManager.sendServerPacket(event.player, chatMessage);
} catch (Exception e) {
}
chatMessage = protocolManager.createPacket(PacketType.Play.Server.TITLE);
chatMessage.getChatComponents().write(0, WrappedChatComponent.fromJson("{text:\"Population: " + pop + " Altitude: "+ altitude + "\", color: \"#ffffff\"}"));
chatMessage.getTitleActions().write(0, TitleAction.SUBTITLE);
try {
protocolManager.sendServerPacket(event.player, chatMessage);
} catch (Exception e) {
}
chatMessage = protocolManager.createPacket(PacketType.Play.Server.TITLE);
chatMessage.getChatComponents().write(0, WrappedChatComponent.fromJson("{text:\"Now entering " + event.newRegion.name() + "\", color: \""+colorHex+"\"}"));
chatMessage.getTitleActions().write(0, TitleAction.ACTIONBAR);
try {
protocolManager.sendServerPacket(event.player, chatMessage);
} catch (Exception e) {
}
} else {
// Fallback to approximated colors
event.player.sendTitle(event.newRegion.coloredName(), "Population: " + pop + " Altitude: " + altitude);
}
}
@ -148,17 +107,28 @@ public class PlayerNotifier implements Listener {
if (event.region.markSeenByPlayer(event.player)) {
event.player.playSound(event.region.location(), Sound.UI_TOAST_CHALLENGE_COMPLETE, (float)1, (float)1);
if (m_protocolManager != null) {
String colorHex = "#" + String.format("%06X", event.region.color().getColor().asRGB());
WrappedChatComponent discoverTitle = WrappedChatComponent.fromJson("{text:\"Region discovered\", color: \""+colorHex+"\"}");
WrappedChatComponent discoverSubtitle = WrappedChatComponent.fromJson("{text:\"You discovered the region " + event.region.name() + "\", color: \""+colorHex+"\"}");
sendTitle(event.player, discoverTitle, discoverSubtitle);
ProtocolManager protocolManager = ProtocolLibrary.getProtocolManager();
if (protocolManager != null) {
PacketContainer chatMessage = protocolManager.createPacket(PacketType.Play.Server.TITLE);
String colorHex = "#" + String.format("%06X", event.region.color().getColor().asRGB());
chatMessage.getChatComponents().write(0, WrappedChatComponent.fromJson("{text:\"Region discovered\", color: \""+colorHex+"\"}"));
try {
protocolManager.sendServerPacket(event.player, chatMessage);
} catch (Exception e) {
}
chatMessage = protocolManager.createPacket(PacketType.Play.Server.TITLE);
chatMessage.getChatComponents().write(0, WrappedChatComponent.fromJson("{text:\"You discovered the region " + event.region.name() + "\", color: \""+colorHex+"\"}"));
chatMessage.getTitleActions().write(0, TitleAction.SUBTITLE);
try {
protocolManager.sendServerPacket(event.player, chatMessage);
} catch (Exception e) {
}
} else {
// Fallback to approximated colors
// TODO: Apply region color to the rest of the title and subtitle
// text
event.player.sendTitle("Region Discovered", "You discovered the region " + event.region.coloredName());
//FIXME: also show pop/alt subtitle
Title title = new Title.Builder().title("Region Discovered").subtitle(event.region.name()).build();
event.player.sendMessage("You discovered the region " + event.region.name());
event.player.sendTitle(title);
}
// TODO: Make this configurable and disablable

View File

@ -23,13 +23,8 @@ import org.bukkit.configuration.Configuration;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.configuration.serialization.ConfigurationSerialization;
import org.bukkit.World;
import org.dynmap.markers.MarkerAPI;
import org.bstats.bukkit.Metrics;
import org.bstats.charts.SingleLineChart;
import us.camin.regions.commands.RegionCommand;
import us.camin.regions.commands.RegionOpCommand;
import us.camin.regions.commands.RegionsCommand;
@ -78,7 +73,6 @@ public class Plugin extends JavaPlugin {
if (markerAPI != null) {
DynmapEventRelay regionHandler = new DynmapEventRelay (this, markerAPI);
getServer().getPluginManager().registerEvents(regionHandler, this);
log.info("Dynmap support enabled.");
} else {
log.info("Dynmap marker API not found. Disabling map support.");
}
@ -92,17 +86,6 @@ public class Plugin extends JavaPlugin {
getServer().getPluginManager().registerEvents(new PlayerInventoryTeleporter(this, m_regions), this);
getServer().getPluginManager().registerEvents(new RegionPostItemWatcher(this, m_regions), this);
getServer().getPluginManager().registerEvents(new RegionPostInteractionWatcher(this, m_regions), this);
// PluginID is from bstats.org for CaminusRegions
Metrics metrics = new Metrics(this, 11705);
metrics.addCustomChart(new SingleLineChart("regions", () -> {
int allRegions = 0;
for(World w : getServer().getWorlds()) {
allRegions += m_regions.regionsForWorld(w).size();
}
return allRegions;
}
));
}
public void loadRegions() {
@ -111,11 +94,7 @@ public class Plugin extends JavaPlugin {
this.getDataFolder().mkdir();
File regionConfigFile = new File(this.getDataFolder(), "regions.yml");
Configuration regionConf = YamlConfiguration.loadConfiguration(regionConfigFile);
try {
m_regions.loadRegions(regionConf);
} catch (Exception e) {
log.log(Level.SEVERE, "Could not load regions config! You risk overwriting and losing data!", e);
}
m_regions.loadRegions(regionConf);
}
public void saveRegions() {

View File

@ -151,8 +151,8 @@ public class Region {
public int getTravelCost(Region destination) {
int baseCost = getBaseTravelCost(destination);
if (destination.isHub()) {
// Travel *to* a hub is always 1
baseCost = 1;
// Travel *to* a hub is 50% cheaper, before charges applied
baseCost /= 2;
}
return Math.max(1, (int)(baseCost / (Math.min(4, m_charges + 1))));
}
@ -166,7 +166,7 @@ public class Region {
return 1;
}
double distance = teleportLocation().distance(destination.teleportLocation());
double blocksPerXP = 768;
double blocksPerXP = 500;
return Math.max(1, (int)(distance / blocksPerXP));
}

View File

@ -37,72 +37,13 @@ import us.camin.regions.events.RegionRemoveEvent;
import us.camin.regions.geometry.RegionSet;
import java.util.logging.Level;
import me.angeschossen.lands.api.land.Land;
import me.angeschossen.lands.api.land.Area;
import me.angeschossen.lands.api.flags.Flags;
import me.angeschossen.lands.api.integration.LandsIntegration;
public class RegionManager {
private final LandsIntegration m_lands;
public enum ValidationResult {
VALID,
UNKNOWN,
NO_PERMISSION,
TOO_CLOSE,
BAD_NAME,
}
public String validateRegionName(String name, Location location) {
Land thisLand = m_lands.getLand(location);
if (thisLand == null) {
return name;
} else {
return thisLand.getName();
}
}
public ValidationResult validateAnchorPoint(Player player, Location location, String proposedName) {
// Require basic permission
if (!player.hasPermission("regions.create")) {
return ValidationResult.NO_PERMISSION;
}
// Allow admins to create regions in unclaimed land
Area thisArea = m_lands.getAreaByLoc(location);
if (thisArea == null) {
if (proposedName == null) {
return ValidationResult.BAD_NAME;
} else if (player.hasPermission("regions.create.bypass")) {
return ValidationResult.VALID;
} else {
return ValidationResult.NO_PERMISSION;
}
}
// For claimed land, require claiming perms to place post
if (!thisArea.hasFlag(player, Flags.LAND_CLAIM, false)) {
return ValidationResult.NO_PERMISSION;
}
// Ensure this is the only region post for this region
for (Region region : regionsForWorld(location.getWorld())) {
Land thatLand = m_lands.getLand(region.interactLocation());
if (thatLand != null && thatLand.getId() == thisArea.getLand().getId()) {
return ValidationResult.TOO_CLOSE;
}
}
return ValidationResult.VALID;
}
Logger log = Logger.getLogger("Regions.RegionManager");
private Map<String, RegionSet> m_regions;
private Server m_server;
public RegionManager(Plugin plugin, Server server) {
m_server = server;
m_lands = new LandsIntegration(plugin);
log.setLevel(Level.ALL);
clear();
}

View File

@ -31,9 +31,6 @@ import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.inventory.meta.CompassMeta;
import org.bukkit.inventory.ItemFlag;
import org.bukkit.enchantments.Enchantment;
import org.bukkit.inventory.RecipeChoice;
import org.bukkit.inventory.ShapedRecipe;
import org.bukkit.NamespacedKey;
import org.bukkit.event.block.Action;
import us.camin.regions.ui.RegionPostBuilder;
@ -48,30 +45,6 @@ public class RegionPostItemWatcher implements Listener {
public RegionPostItemWatcher(Plugin plugin, RegionManager manager) {
m_manager = manager;
m_plugin = plugin;
// TODO: Make recipe-based creation of items configurable/disablable
NamespacedKey chargeKey = new NamespacedKey(m_plugin, "region_post_charge");
ShapedRecipe chargeRecipe = new ShapedRecipe(chargeKey, m_theChargeItem);
chargeRecipe.shape("DDD", "DGD", "DDD");
chargeRecipe.setIngredient('D', Material.PURPUR_BLOCK);
chargeRecipe.setIngredient('G', Material.QUARTZ);
NamespacedKey anchorKey = new NamespacedKey(m_plugin, "region_post_anchor");
ShapedRecipe anchorRecipe = new ShapedRecipe(anchorKey, m_theAnchor);
anchorRecipe.shape("DDD", "DGD", "DDD");
anchorRecipe.setIngredient('D', new RecipeChoice.ExactChoice(m_theChargeItem));
anchorRecipe.setIngredient('G', Material.SOUL_LANTERN);
NamespacedKey compassKey = new NamespacedKey(m_plugin, "region_post_compass");
ShapedRecipe compassRecipe = new ShapedRecipe(compassKey, m_theCompass);
// Uses four fewer charges, slightly cheaper.
compassRecipe.shape(" D ", " G ", " ");
compassRecipe.setIngredient('D', new RecipeChoice.ExactChoice(m_theChargeItem));
compassRecipe.setIngredient('G', Material.COMPASS);
m_plugin.getServer().addRecipe(chargeRecipe);
m_plugin.getServer().addRecipe(anchorRecipe);
m_plugin.getServer().addRecipe(compassRecipe);
}
static public ItemStack createCompass(Region r) {
@ -80,9 +53,6 @@ public class RegionPostItemWatcher implements Listener {
List<String> lore = new ArrayList<String>();
lore.add("Right click to locate the nearest Region Post");
if (r == null) {
meta.setDisplayName(ChatColor.DARK_PURPLE + "Region Compass");
lore.add("Tracking: " + ChatColor.MAGIC + "NOWHERE IN PARTICULAR");
lore.add("Coordinates: " + ChatColor.MAGIC + "0000" + ChatColor.RESET + ", " + ChatColor.MAGIC + "0000");
} else {
CompassMeta compassMeta = (CompassMeta)meta;
compassMeta.setDisplayName(ChatColor.DARK_PURPLE + "Region Compass (" + r.coloredName() + ChatColor.RESET + ChatColor.DARK_PURPLE + ")");
@ -94,7 +64,6 @@ public class RegionPostItemWatcher implements Listener {
meta.setLore(lore);
meta.addEnchant(Enchantment.SOUL_SPEED, 1, true);
meta.addItemFlags(ItemFlag.HIDE_ENCHANTS);
meta.setCustomModelData(93199);
stack.setItemMeta(meta);
return stack;
}
@ -108,7 +77,6 @@ public class RegionPostItemWatcher implements Listener {
meta.addItemFlags(ItemFlag.HIDE_ENCHANTS);
meta.addEnchant(Enchantment.SOUL_SPEED, 1, true);
meta.setDisplayName("Region Post Charge");
meta.setCustomModelData(93197);
stack.setItemMeta(meta);
return stack;
}
@ -122,7 +90,6 @@ public class RegionPostItemWatcher implements Listener {
meta.addEnchant(Enchantment.SOUL_SPEED, 1, true);
meta.setLore(lore);
meta.setDisplayName("Region Post Anchor");
meta.setCustomModelData(93198);
stack.setItemMeta(meta);
return stack;
}
@ -186,32 +153,28 @@ public class RegionPostItemWatcher implements Listener {
ItemStack compassItem = createCompass(nearest);
player.setItemInHand(compassItem);
player.sendMessage("Now tracking " + nearest.name());
} else if (!event.isCancelled() && isRegionCreateItem(handStack, player) && event.getAction() == Action.RIGHT_CLICK_BLOCK && !event.getClickedBlock().getType().isInteractable()) {
} else if (!event.isCancelled() && isRegionCreateItem(handStack, player) && event.getAction() == Action.RIGHT_CLICK_BLOCK && !event.getClickedBlock().getType().isInteractable() && player.hasPermission("regions.create")) {
event.setUseItemInHand(Event.Result.DENY);
event.setCancelled(true);
String postName = meta.getDisplayName();
if (postName.equals("") || postName.equals("Region Post Anchor")) {
postName = null;
}
postName = m_manager.validateRegionName(postName, event.getClickedBlock().getLocation());
RegionManager.ValidationResult result = m_manager.validateAnchorPoint(player, event.getClickedBlock().getLocation(), postName);
if (result == RegionManager.ValidationResult.VALID) {
Region r = new Region(postName, event.getClickedBlock().getRelative(event.getBlockFace()).getLocation());
m_plugin.getServer().getScheduler().runTask(m_plugin, () -> {
RegionPostBuilder builder = new RegionPostBuilder(r, m_plugin);
builder.build();
});
handStack.setAmount(handStack.getAmount()-1);
player.setItemInHand(handStack);
m_plugin.regionManager().addRegion(r);
m_plugin.saveRegions();
player.sendMessage("You established the region "+r.coloredName());
} else if (result == RegionManager.ValidationResult.TOO_CLOSE) {
player.sendMessage("You are too close to another region post!");
} else if (result == RegionManager.ValidationResult.BAD_NAME) {
player.sendMessage("You must first give this item a better name.");
if (meta.getDisplayName().equals("") || meta.getDisplayName().equals("Region Post Anchor")) {
player.sendMessage("You must first give this item a name!");
} else {
player.sendMessage("You aren't allowed to create a region here.");
Region nearest = m_manager.nearestRegion(event.getClickedBlock().getLocation());
if (nearest != null && event.getClickedBlock().getLocation().distance(nearest.interactLocation()) < 500) {
int distance = 500 - (int)event.getClickedBlock().getLocation().distance(nearest.interactLocation());
player.sendMessage("You are " + distance + " blocks too close to the region post for " + nearest.name() + ".");
} else {
Region r = new Region(meta.getDisplayName(), event.getClickedBlock().getRelative(event.getBlockFace()).getLocation());
m_plugin.getServer().getScheduler().runTask(m_plugin, () -> {
RegionPostBuilder builder = new RegionPostBuilder(r, m_plugin);
builder.build();
});
handStack.setAmount(handStack.getAmount()-1);
player.setItemInHand(handStack);
m_plugin.regionManager().addRegion(r);
m_plugin.saveRegions();
player.sendMessage("You established the region "+r.coloredName());
}
}
}
}

View File

@ -25,10 +25,26 @@ public class BorderMesh {
Map<Region, Set<Region>> m_neighbors;
public Collection<Region> neighbors(Region region) {
Collection<Region> ret = new ArrayList<Region>();
if (m_neighbors.containsKey(region)) {
return m_neighbors.get(region);
Collection<Region> allNeighbors = m_neighbors.get(region);
if (false /*isFrontier(region)*/) {
//log.trace("Region " + region.name() + " is a frontier");
for(Region neighbor : allNeighbors) {
if (!isFrontier(neighbor)) {
ret.add(neighbor);
} else {
//log.trace("Not linking " + region.name() + " to " + neighbor.name() + " as it is also a frontier");
}
}
} else {
//log.trace("Region " + region.name() + " is not a frontier");
for(Region neighbor : allNeighbors) {
ret.add(neighbor);
}
}
}
return new ArrayList<Region>();
return ret;
}
public BorderMesh(Collection<Region> regions) {
@ -42,24 +58,6 @@ public class BorderMesh {
public double y[];
public double z[];
public class Segment {
public Vector2D start;
public Vector2D end;
public Segment(Vector2D start, Vector2D end) {
this.start = start;
this.end = end;
}
}
public List<Segment> segments() {
List<Segment> ret = new ArrayList<Segment>();
for(int i = 0; i < x.length-1; i++) {
ret.add(new Segment(new Vector2D(x[i], z[i]), new Vector2D(x[i+1], z[i+1])));
}
ret.add(new Segment(new Vector2D(x[x.length-1], z[z.length-1]), new Vector2D(x[0], z[0])));
return ret;
}
public Polygon(List<Vector2D> points) {
x = new double[points.size()];
y = new double[points.size()];
@ -95,27 +93,6 @@ public class BorderMesh {
}
}
private int orientation(Vector2D p, Vector2D q, Vector2D r) {
int val = (int)((q.y - p.y) * (r.x - q.x) - (q.x - p.x) * (r.y - q.y));
if (val == 0) return 0; // colinear
return (val > 0) ? 1 : 2; // Clockwise or counter-clockwise
}
private boolean doIntersect(Vector2D p1, Vector2D q1, Vector2D p2, Vector2D q2) {
int o1 = orientation(p1, q1, p2);
int o2 = orientation(p1, q1, q2);
int o3 = orientation(p2, q2, p1);
int o4 = orientation(p2, q2, q1);
if (o1 != o2 && o3 != o4) {
return true;
}
// It probably isn't possible to generate a route that is colinear with a
// region border, so assume it isn't intersecting.
return false;
}
public Polygon polygonForRegion(Region r) {
return m_polygons.get(r);
}
@ -131,7 +108,7 @@ public class BorderMesh {
DelaunayTriangulator mesh = new DelaunayTriangulator(regionPoints);
try {
mesh.triangulate();
log.fine("Mesh triangulated!");
log.info("Mesh triangulated!");
} catch (NullPointerException e) {
log.warning("Got a null pointer when triangulating, skipping world.");
return false;
@ -167,13 +144,11 @@ public class BorderMesh {
}
}
HashMap<Region, Set<Region>> allNeighborSet = new HashMap<Region, Set<Region>>();
// Now we go back through our region mesh to generate vornoi points
for(Region region : m_regions) {
ArrayList<Vector2D> points = new ArrayList<Vector2D>();
HashSet<Region> allNeighbors = new HashSet<Region>();
log.fine("Executing voronoi transform...");
HashSet<Region> neighbors = new HashSet<Region>();
log.info("Executing voronoi transform...");
for(Triangle tri : triangleSoup) {
if (tri.region == region) {
for (Region neighbor : m_regions) {
@ -183,58 +158,46 @@ public class BorderMesh {
Location neighborLoc = neighbor.location();
Vector2D neighborCenter = new Vector2D(neighborLoc.getBlockX(), neighborLoc.getBlockZ());
if (vecEquals(tri.a, neighborCenter) || vecEquals(tri.b, neighborCenter) || vecEquals(tri.c, neighborCenter)) {
allNeighbors.add(neighbor);
neighbors.add(neighbor);
}
}
points.add(tri.circumcenter());
}
}
allNeighborSet.put(region, allNeighbors);
// Sort points into a renderable polygon based on direction to center
Location loc = region.location();
Vector2D regionCenter = new Vector2D(loc.getBlockX(), loc.getBlockZ());
points.sort((Vector2D a, Vector2D b) -> Double.compare(direction(a, regionCenter), direction(b, regionCenter)));
log.fine("Border for " + region.name() + " is defined by " + points.size() + " points");
log.info("Border for " + region.name() + " is defined by " + points.size() + " points");
Polygon polygon = new Polygon(points);
m_polygons.put(region, polygon);
}
// Now that we have the borders, we generate valid routes that cross
// zero or one border lines at most.
for(Region region : m_regions) {
Set<Region> routedNeighbors = new HashSet<Region>();
for(Region neighbor : allNeighborSet.get(region)) {
int crossings = 0;
Vector2D start = new Vector2D(region.location().getBlockX(), region.location().getBlockZ());
Vector2D neighborEnd = new Vector2D(neighbor.location().getBlockX(), neighbor.location().getBlockZ());
for(Region distantNeighbor : m_regions) {
if (distantNeighbor.equals(neighbor) || distantNeighbor.equals(region)) {
continue;
}
Polygon poly = m_polygons.get(distantNeighbor);
for(Polygon.Segment edge : poly.segments()) {
// Check if the line from region->neighbor intersects with
// any polygon
if (doIntersect(start, neighborEnd, edge.start, edge.end)) {
log.fine("Intersect " + distantNeighbor.name());
crossings++;
}
}
}
log.fine("Route: " + region.name() + " -> " + neighbor.name() + "\t" + crossings);
if (crossings == 1 || crossings == 0) {
routedNeighbors.add(neighbor);
}
}
m_neighbors.put(region, routedNeighbors);
m_neighbors.put(region, neighbors);
}
return true;
}
/*public boolean validNeighbors(Region regionA, Region regionB) {
Polygon polyA = polygonForRegion(regionA);
Polygon polyB = polygonForRegion(regionA);
Location center = region.location();
if (poly.contains(center.getBlockX(), center.getBlockZ())) {
return false;
} else {
return true;
}
}*/
public boolean isFrontier(Region region) {
Polygon poly = polygonForRegion(region);
Location center = region.location();
if (poly.contains(center.getBlockX(), center.getBlockZ())) {
return false;
} else {
return true;
}
}
private boolean vecEquals(Vector2D a, Vector2D b) {
return a.x == b.x && a.y == b.y;
}

View File

@ -92,19 +92,13 @@ public class PlayerInventoryTeleporter implements Listener {
if (event.getClickedInventory() == neighborInv) {
ItemMeta meta = event.getCurrentItem().getItemMeta();
Material mat = event.getCurrentItem().getType();
final String selectedName = meta.getDisplayName();
if (mat == Material.BEDROCK || mat == Material.COMPASS || mat == Material.LANTERN || mat == Material.SOUL_LANTERN) {
if (mat == Material.BEDROCK || mat == Material.COMPASS || mat == Material.LANTERN) {
return;
}
m_plugin.getServer().getScheduler().runTask(m_plugin, () -> event.getView().close());
Region nearest = m_manager.nearestRegion(player.getLocation());
if (selectedName == nearest.name()) {
return;
}
m_plugin.getServer().getScheduler().runTask(m_plugin, () -> event.getView().close());
final String selectedName = meta.getDisplayName();
final Optional<Region> destination = m_plugin.regionManager().regionsForWorld(player.getLocation().getWorld())
.stream()
.filter((r) -> r.name().equals(selectedName))
@ -219,7 +213,7 @@ public class PlayerInventoryTeleporter implements Listener {
}
ItemStack chargesItem = new ItemStack(event.region.charges() == 0 ? Material.SOUL_LANTERN : Material.LANTERN);
ItemMeta meta = chargesItem.getItemMeta();
meta.setDisplayName(ChatColor.BOLD + "Region Post Charges");
meta.setDisplayName(ChatColor.BOLD + "Region Post Configuration");
ArrayList<String> lore = new ArrayList<String>();
lore.add(ChatColor.WHITE + "Name: "+event.region.coloredName());
lore.add(ChatColor.WHITE + "Visits: " + ChatColor.YELLOW + event.region.visits());
@ -229,18 +223,7 @@ public class PlayerInventoryTeleporter implements Listener {
meta.addEnchant(Enchantment.SOUL_SPEED, 1, true);
chargesItem.setItemMeta(meta);
// 22 Is the middle of the bottom row
neighborInventory.setItem(23, chargesItem);
ItemStack iconItem = event.region.icon();
meta = iconItem.getItemMeta();
meta.setDisplayName(event.region.name());
lore = new ArrayList<String>();
lore.add(ChatColor.BOLD + "This is the region's current banner icon.");
lore.add(ChatColor.ITALIC + "Right-click the region post with a banner to change it");
meta.setLore(lore);
meta.addItemFlags(ItemFlag.HIDE_ENCHANTS);
iconItem.setItemMeta(meta);
neighborInventory.setItem(21, iconItem);
neighborInventory.setItem(22, chargesItem);
event.player.openInventory(neighborInventory);
}

View File

@ -41,7 +41,7 @@ public class PlayerTeleporter {
Random rand = new Random();
Vector travelVec = m_dest.toVector().subtract(m_src.toVector()).normalize();
double angleDelta = (Math.PI / 5) * (rand.nextGaussian() - 0.5) * (1-accuracy);
double angleDelta = (Math.PI / 3) * (rand.nextGaussian() - 0.5) * (1-accuracy);
travelVec.rotateAroundY(angleDelta);
double distanceToDest = m_src.distance(m_dest);

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -3,8 +3,8 @@ main: us.camin.regions.Plugin
author: Torrie Fischer <tdfischer@hackerbots.net>
website: http://hackerbots.net/
version: ${version}
api-version: 1.19
softdepend: [dynmap, ProtocolLib, Lands]
api-version: 1.16
softdepend: [dynmap, ProtocolLib]
commands:
regions:
description: "List available regions."
@ -21,7 +21,6 @@ permissions:
description: Allows use of all regions permissions
children:
regions.create: true
regions.create.bypass: true
regions.commands.*: true
regions.regen.*: true
regions.bypass.*: true
@ -38,9 +37,6 @@ permissions:
regions.create:
default: true
description: Create a region with a region item
regions.create.bypass:
default: op
description: Bypass anything preventing creation of a new region
regions.setbanner:
default: true
description: Allows setting a region post banner