Compare commits

...

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

24 changed files with 683 additions and 363 deletions

1
.gitignore vendored
View File

@ -1,3 +1,4 @@
target
test-server
bin
*.swp

15
.woodpecker.yml Normal file
View File

@ -0,0 +1,15 @@
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 +1,71 @@
# 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

BIN
docs/craft-region-items.gif Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 MiB

BIN
docs/create-region-post.gif Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 MiB

BIN
docs/discover-region-post.gif Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 MiB

BIN
docs/use-region-post.gif Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 MiB

35
pom.xml
View File

@ -4,7 +4,7 @@
<groupId>us.camin.regions</groupId>
<artifactId>Regions</artifactId>
<packaging>jar</packaging>
<version>0.2.99-rc1</version>
<version>0.4.0</version>
<name>regions</name>
<url>http://maven.apache.org</url>
<properties>
@ -12,14 +12,15 @@
</properties>
<dependencies>
<dependency>
<groupId>com.comphenix.protocol</groupId>
<artifactId>ProtocolLib</artifactId>
<version>4.6.0</version>
<groupId>org.bstats</groupId>
<artifactId>bstats-bukkit</artifactId>
<version>2.2.1</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.gmail.filoghost.holographicdisplays</groupId>
<artifactId>holographicdisplays-api</artifactId>
<version>2.4.0</version>
<groupId>com.comphenix.protocol</groupId>
<artifactId>ProtocolLib</artifactId>
<version>4.7.0-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
@ -45,17 +46,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>
@ -92,6 +93,10 @@
<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>
@ -172,6 +177,10 @@
<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 {
}
}
public void updatePolygons(World world) {
private void updatePolygons(World world) {
List<GenericMarker> oldMarkers = m_borderMarkers.get(world);
if (oldMarkers != null) {
for(GenericMarker marker : oldMarkers) {
@ -91,17 +91,10 @@ public class DynmapEventRelay implements Listener {
log.info("Could not generate polygon for region " + region.name());
continue;
}
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);
}
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);
// Add a line between each region, for teleportations
double thickness = Math.max(1, Math.log(geom.neighbors(region).size()) * 2.75);
@ -109,9 +102,13 @@ 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() };
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);
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);
}
}
}
@ -119,11 +116,17 @@ 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,9 +50,64 @@ 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
@ -72,32 +127,17 @@ public class PlayerNotifier implements Listener {
int pop = m_plugin.playerWatcher().playersInRegion(event.newRegion).size();
Location center = event.newRegion.location();
int altitude = center.getBlockY();
ProtocolManager protocolManager = ProtocolLibrary.getProtocolManager();
if (protocolManager != null) {
PacketContainer chatMessage = protocolManager.createPacket(PacketType.Play.Server.TITLE);
if (m_protocolManager != null) {
String colorHex = "#" + String.format("%06X", event.newRegion.color().getColor().asRGB());
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) {
}
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);
} else {
Title title = new Title.Builder().title(event.newRegion.name()).subtitle("Population: "+pop+" Altitude: " + altitude).build();
event.player.sendTitle(title);
// Fallback to approximated colors
event.player.sendTitle(event.newRegion.coloredName(), "Population: " + pop + " Altitude: " + altitude);
}
}
@ -108,29 +148,21 @@ 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);
ProtocolManager protocolManager = ProtocolLibrary.getProtocolManager();
if (protocolManager != null) {
PacketContainer chatMessage = protocolManager.createPacket(PacketType.Play.Server.TITLE);
if (m_protocolManager != null) {
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) {
}
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);
} else {
//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);
// 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());
}
// TODO: Make this configurable and disablable
event.player.giveExp(10);
}
for(Region region : nearby) {
nearbyText.append(" ");

View File

@ -23,8 +23,13 @@ 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;
@ -45,7 +50,6 @@ public class Plugin extends JavaPlugin {
Logger log = Logger.getLogger("Regions");
RegionManager m_regions;
PlayerWatcher m_playerWatcher;
RegionPostManager m_regionPosts;
public RegionManager regionManager() {
return m_regions;
@ -65,14 +69,6 @@ public class Plugin extends JavaPlugin {
getCommand("regions").setExecutor(new RegionsCommand(this));
getCommand("regionop").setExecutor(new RegionOpCommand(this));
boolean useHolograms = getServer().getPluginManager().isPluginEnabled("HolographicDisplays");
if (!useHolograms) {
log.info("HolographicDisplays not enabled. Region posts will not have holograms.");
}
m_regionPosts = new RegionPostManager(m_regions, this, useHolograms);
// TODO: Make holograms configurable. Disabled by default for now.
//getServer().getPluginManager().registerEvents(m_regionPosts, this);
loadRegions();
m_playerWatcher.recalculatePlayerRegions(true);
org.bukkit.plugin.Plugin mapPlugin = getServer().getPluginManager().getPlugin("dynmap");
@ -82,6 +78,7 @@ 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.");
}
@ -95,6 +92,17 @@ 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() {
@ -103,7 +111,11 @@ public class Plugin extends JavaPlugin {
this.getDataFolder().mkdir();
File regionConfigFile = new File(this.getDataFolder(), "regions.yml");
Configuration regionConf = YamlConfiguration.loadConfiguration(regionConfigFile);
m_regions.loadRegions(regionConf);
try {
m_regions.loadRegions(regionConf);
} catch (Exception e) {
log.log(Level.SEVERE, "Could not load regions config! You risk overwriting and losing data!", e);
}
}
public void saveRegions() {
@ -120,7 +132,6 @@ public class Plugin extends JavaPlugin {
}
public void onDisable() {
m_regionPosts.release();
saveRegions();
log.info("Plugin disabled");
}

View File

@ -28,6 +28,7 @@ import org.bukkit.inventory.ItemStack;
import org.bukkit.Material;
import org.bukkit.inventory.meta.BannerMeta;
import org.bukkit.material.MaterialData;
import org.bukkit.inventory.ItemFlag;
import us.camin.regions.config.RegionConfiguration;
import us.camin.regions.ui.Colors;
@ -42,36 +43,63 @@ public class Region {
private Location m_location;
private String m_name;
private List<Pattern> m_bannerPatterns = new ArrayList<Pattern>();
private DyeColor m_color = null;
private DyeColor m_color = DyeColor.values()[(int)(System.currentTimeMillis() % DyeColor.values().length)];
private List<UUID> m_seenPlayers = new ArrayList<UUID>();
private boolean m_isHub = false;
public Region(String name, Location location) {
m_location = location.toBlockLocation();
m_location = location.getBlock().getLocation();
m_name = name;
// Pick a random color
m_color = DyeColor.values()[(int)(System.currentTimeMillis() % DyeColor.values().length)];
}
public Region(String name, Location location, int visits, int charges, DyeColor color) {
m_location = location.toBlockLocation();
m_location = location.getBlock().getLocation();
m_name = name;
m_visits = visits;
m_charges = charges;
m_color = color;
}
private static DyeColor[] defaultColors = {
DyeColor.LIGHT_BLUE,
DyeColor.BLACK,
DyeColor.BLUE,
DyeColor.CYAN,
DyeColor.BLUE,
DyeColor.GRAY,
DyeColor.GREEN,
DyeColor.PURPLE,
DyeColor.RED,
DyeColor.ORANGE,
DyeColor.GRAY,
DyeColor.GREEN,
DyeColor.MAGENTA,
DyeColor.RED,
DyeColor.WHITE,
DyeColor.YELLOW,
};
private DyeColor defaultColorForName(String name) {
int colorCount = defaultColors.length;
int hashed = Math.abs(name.hashCode());
return defaultColors[hashed % (colorCount - 1)];
}
public Region(String name, World world, RegionConfiguration conf) {
if (conf.y == -1) {
Location defaultLoc = new Location(world, conf.x, 64, conf.z);
conf.y = world.getHighestBlockAt(defaultLoc).getY();
}
Location defaultLoc = new Location(world, conf.x, 64, conf.z);
conf.y = world.getHighestBlockAt(defaultLoc).getY();
}
m_name = name;
m_visits = conf.visits;
m_charges = conf.charges;
m_location = new Location(world, conf.x, conf.y, conf.z);
m_bannerPatterns = conf.patterns;
m_color = conf.color;
if (conf.color == null) {
m_color = defaultColorForName(name);
} else {
m_color = conf.color;
}
m_seenPlayers = conf.seenBy;
m_isHub = conf.isHub;
}
@ -120,6 +148,28 @@ public class Region {
m_charges += charges;
}
public int getTravelCost(Region destination) {
int baseCost = getBaseTravelCost(destination);
if (destination.isHub()) {
// Travel *to* a hub is always 1
baseCost = 1;
}
return Math.max(1, (int)(baseCost / (Math.min(4, m_charges + 1))));
}
public int getBaseTravelCost(Region destination) {
if (m_isHub && destination.isHub()) {
// Free travel between hubs
return 0;
} else if (m_isHub) {
// Max cost 1 level for travel *from* a hub
return 1;
}
double distance = teleportLocation().distance(destination.teleportLocation());
double blocksPerXP = 768;
return Math.max(1, (int)(distance / blocksPerXP));
}
int m_visits = 0;
int m_charges = 0;
@ -290,6 +340,8 @@ public class Region {
ItemStack item = new ItemStack(bannerIconMaterial());
BannerMeta bannerMeta = (BannerMeta)item.getItemMeta();
bannerMeta.setPatterns(bannerPatterns());
bannerMeta.addItemFlags(ItemFlag.HIDE_POTION_EFFECTS);
bannerMeta.addItemFlags(ItemFlag.HIDE_ATTRIBUTES);
item.setItemMeta(bannerMeta);
return item;
}

View File

@ -37,13 +37,72 @@ 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

@ -89,12 +89,13 @@ public class RegionPostInteractionWatcher implements Listener {
Location lanternRegion = nearest.interactLocation().add(0, 1, 0);
boolean isInteracted = false;
if (clickedBlock != null) {
isInteracted |= clickedBlock.getBlockKey() == interactRegion.toBlockKey();
isInteracted |= clickedBlock.getBlockKey() == lanternRegion.toBlockKey();
isInteracted |= nearest.interactLocation().add(1, 0, 0).toBlockKey() == clickedBlock.getBlockKey();
isInteracted |= nearest.interactLocation().add(-1, 0, 0).toBlockKey() == clickedBlock.getBlockKey();
isInteracted |= nearest.interactLocation().add(0, 0, 1).toBlockKey() == clickedBlock.getBlockKey();
isInteracted |= nearest.interactLocation().add(0, 0, -1).toBlockKey() == clickedBlock.getBlockKey();
isInteracted |= clickedBlock.getLocation().equals(interactRegion.getBlock().getLocation());
isInteracted |= clickedBlock.getLocation().equals(lanternRegion.getBlock().getLocation());
isInteracted |= nearest.interactLocation().add(1, 0, 0).getBlock().getLocation().equals(clickedBlock.getLocation());
isInteracted |= nearest.interactLocation().add(-1, 0, 0).getBlock().getLocation().equals(clickedBlock.getLocation());
isInteracted |= nearest.interactLocation().add(0, 0, 1).getBlock().getLocation().equals(clickedBlock.getLocation());
isInteracted |= nearest.interactLocation().add(0, 0, -1).getBlock().getLocation().equals(clickedBlock.getLocation());
}
if (isInteracted) {
event.setCancelled(true);
@ -103,16 +104,19 @@ public class RegionPostInteractionWatcher implements Listener {
player.sendMessage("You cannot use region posts at this time.");
return;
}
if (RegionPostItemWatcher.isChargeItem(handStack)) {
if (player.isSneaking() && RegionPostItemWatcher.isChargeItem(handStack) && player.hasPermission("regions.charge")) {
nearest.addCharges(1);
m_plugin.getServer().getScheduler().runTask(m_plugin, () -> {
RegionPostBuilder builder = new RegionPostBuilder(nearest, m_plugin);
builder.updateLantern();
});
m_plugin.saveRegions();
player.setItemInHand(handStack.subtract());
handStack.setAmount(handStack.getAmount()-1);
player.setItemInHand(handStack);
m_plugin.getServer().getPluginManager().callEvent(new PlayerAddRegionChargeEvent(player, nearest));
} else if (isBannerItem(handStack)) {
// TODO: Make this configurable and disablable
player.giveExp(1);
} else if (player.isSneaking() && isBannerItem(handStack) && player.hasPermission("regions.setbanner")) {
DyeColor bannerColor = DyeColor.getByDyeData(handStack.getData().getData());
BannerMeta bannerMeta = (BannerMeta)meta;
log.info("Setting banner color to " + bannerColor);
@ -124,10 +128,8 @@ public class RegionPostInteractionWatcher implements Listener {
RegionPostBuilder builder = new RegionPostBuilder(nearest, m_plugin);
builder.build();
});
} else if (nearest.charges() > 0 || player.hasPermission("regions.bypass.charges")) {
m_plugin.getServer().getPluginManager().callEvent(new PlayerPostInteractEvent(player, nearest));
} else {
player.sendMessage("This region post is not charged. Right click on it while holding cobblestone.");
m_plugin.getServer().getPluginManager().callEvent(new PlayerPostInteractEvent(player, nearest));
}
}
}

View File

@ -22,6 +22,7 @@ import org.bukkit.event.Listener;
import org.bukkit.event.Event;
import org.bukkit.Location;
import org.bukkit.entity.Player;
import org.bukkit.ChatColor;
import org.bukkit.event.EventHandler;
import org.bukkit.event.player.PlayerInteractEvent;
import org.bukkit.inventory.ItemStack;
@ -30,6 +31,9 @@ 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;
@ -44,16 +48,53 @@ 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() {
static public ItemStack createCompass(Region r) {
ItemStack stack = new ItemStack(Material.COMPASS);
ItemMeta meta = stack.getItemMeta();
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 + ")");
compassMeta.setLodestone(r.location());
compassMeta.setLodestoneTracked(false);
lore.add("Tracking: " + r.coloredName());
lore.add("Coordinates: " + r.location().getX() + ", " + r.location().getY());
}
meta.setLore(lore);
meta.addEnchant(Enchantment.SOUL_SPEED, 1, true);
meta.addItemFlags(ItemFlag.HIDE_ENCHANTS);
meta.setCustomModelData(93199);
stack.setItemMeta(meta);
return stack;
}
@ -67,11 +108,12 @@ 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;
}
static public ItemStack createCreateItem() {
static public ItemStack createAnchor() {
ItemStack stack = new ItemStack(Material.LANTERN);
ItemMeta meta = stack.getItemMeta();
List<String> lore = new ArrayList<String>();
@ -79,12 +121,14 @@ public class RegionPostItemWatcher implements Listener {
meta.addItemFlags(ItemFlag.HIDE_ENCHANTS);
meta.addEnchant(Enchantment.SOUL_SPEED, 1, true);
meta.setLore(lore);
meta.setDisplayName("Region Post Anchor");
meta.setCustomModelData(93198);
stack.setItemMeta(meta);
return stack;
}
private ItemStack m_theCompass = createCompass();
private ItemStack m_theItem = createCreateItem();
private ItemStack m_theCompass = createCompass(null);
private ItemStack m_theAnchor = createAnchor();
private static ItemStack m_theChargeItem = createChargeItem();
public static boolean isChargeItem(ItemStack stack) {
@ -96,10 +140,10 @@ public class RegionPostItemWatcher implements Listener {
return true;
}
if (stack.getType() == m_theItem.getType()) {
if (stack.getType() == m_theCompass.getType()) {
ItemMeta meta = stack.getItemMeta();
ItemMeta theItemMeta = m_theItem.getItemMeta();
if (meta.getItemFlags() == theItemMeta.getItemFlags()) {
ItemMeta theItemMeta = m_theCompass.getItemMeta();
if (meta.getLore() != null && meta.getLore().get(0).equals(theItemMeta.getLore().get(0))) {
return true;
}
}
@ -108,14 +152,14 @@ public class RegionPostItemWatcher implements Listener {
}
public boolean isRegionCreateItem(ItemStack stack, Player p) {
if (stack.isSimilar(m_theItem)) {
if (stack.isSimilar(m_theAnchor)) {
return true;
}
if (stack.getType() == m_theItem.getType()) {
if (stack.getType() == m_theAnchor.getType()) {
ItemMeta meta = stack.getItemMeta();
ItemMeta theItemMeta = m_theItem.getItemMeta();
if (meta.getLore().equals(theItemMeta.getLore())) {
ItemMeta theItemMeta = m_theAnchor.getItemMeta();
if (meta.getLore() != null && meta.getLore().equals(theItemMeta.getLore())) {
return true;
}
}
@ -134,44 +178,40 @@ public class RegionPostItemWatcher implements Listener {
if (isRegionCompass(handStack) && event.getAction() == Action.RIGHT_CLICK_AIR) {
event.setCancelled(true);
ItemStack compassItem = new ItemStack(Material.COMPASS);
Region nearest = m_manager.nearestRegion(player.getLocation());
if (nearest == null) {
player.sendMessage("There are no regions in this world!");
return;
}
CompassMeta compassMeta = (CompassMeta)compassItem.getItemMeta();
compassMeta.setDisplayName(nearest.name());
compassMeta.setLodestone(nearest.location());
compassMeta.setLodestoneTracked(false);
ArrayList<String> newLore = new ArrayList<String>();
newLore.add("Right click to locate the nearest Region Post");
newLore.add("Tracking: " + nearest.name());
compassMeta.setLore(newLore);
compassItem.setItemMeta(compassMeta);
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()) {
event.setUseItemInHand(Event.Result.DENY);
event.setCancelled(true);
if (meta.getDisplayName().equals("")) {
player.sendMessage("You must first give this item a name!");
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.");
} else {
Region nearest = m_manager.nearestRegion(player.getLocation());
if (nearest != null && player.getLocation().distance(nearest.interactLocation()) <= 500) {
player.sendMessage("You are too close to the region post for " + nearest.name());
} else {
Region r = new Region(meta.getDisplayName(), event.getClickedBlock().getRelative(event.getBlockFace()).getLocation());
//player.teleport(r.teleportLocation());
m_plugin.getServer().getScheduler().runTask(m_plugin, () -> {
RegionPostBuilder builder = new RegionPostBuilder(r, m_plugin);
builder.build();
});
player.setItemInHand(handStack.subtract());
m_plugin.regionManager().addRegion(r);
m_plugin.saveRegions();
player.sendMessage("You established the region "+r.coloredName());
}
player.sendMessage("You aren't allowed to create a region here.");
}
}
}

View File

@ -1,141 +0,0 @@
package us.camin.regions;
/**
* This file is part of Regions
*
* Regions is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Regions is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Regions. If not, see <http://www.gnu.org/licenses/>.
*
*/
import org.bukkit.event.Listener;
import org.bukkit.event.EventHandler;
import org.bukkit.Location;
import java.util.HashMap;
import java.util.ArrayList;
import java.util.logging.Logger;
import com.gmail.filoghost.holographicdisplays.api.HologramsAPI;
import us.camin.regions.events.PlayerNearRegionPostEvent;
import us.camin.regions.events.RegionCreateEvent;
import us.camin.regions.events.RegionRemoveEvent;
import us.camin.regions.ui.RegionPostBuilder;
import com.gmail.filoghost.holographicdisplays.api.Hologram;
public class RegionPostManager implements Listener {
Logger log = Logger.getLogger("Regions.RegionPostManager");
Plugin m_plugin;
RegionManager m_regions;
boolean m_useHolograms;
HashMap<Region, Hologram> m_regionHolograms;
//HashMap<Region, ArmorStand> m_armorStands;
public RegionPostManager(RegionManager regions, Plugin plugin, boolean useHolograms) {
//m_armorStands = new HashMap<Region, ArmorStand>();
m_regions = regions;
m_plugin = plugin;
m_useHolograms = useHolograms;
m_regionHolograms = new HashMap<>();
}
@EventHandler
public void onRegionCreate(RegionCreateEvent event) {
Location loc = event.region.location();
if (loc.getWorld().isChunkLoaded((int)loc.getX(), (int)loc.getY())) {
createHologram(event.region);
queueRebuild(event.region);
}
}
@EventHandler
public void onPlayerNearRegionPost(PlayerNearRegionPostEvent event) {
createHologram(event.region);
}
private void queueRebuild(Region region) {
if (region != null) {
log.info("Rebuilding region post for " + region.name());
m_plugin.getServer().getScheduler().runTask(m_plugin, () -> {
RegionPostBuilder builder = new RegionPostBuilder(region, m_plugin);
builder.build();
});
}
}
@EventHandler
public void onRegionDestroy(RegionRemoveEvent event) {
destroyHologram(event.region);
}
public void release() {
for(Region r : new ArrayList<>(m_regionHolograms.keySet())) {
destroyHologram(r);
}
/*for(Region r : new ArrayList<>(m_armorStands.keySet())) {
destroyHologram(r);
}*/
}
private void createHologram(Region r) {
if (!m_useHolograms) {
return;
}
Hologram hologram = m_regionHolograms.get(r);
if (hologram == null) {
hologram = HologramsAPI.createHologram(m_plugin, r.teleportLocation().clone().add(0.5, 0, 0.5));
hologram.appendTextLine(r.coloredName());
m_regionHolograms.put(r, hologram);
}
/*boolean needsRegen = false;
if (m_armorStands.containsKey(r)) {
// If we have an armor stand, but it isn't valid, regen
needsRegen = !m_armorStands.get(r).isValid();
} else {
// If we don't have an armor stand at all, regen
needsRegen = true;
}
if (needsRegen) {
log.info("Creating hologram for " + r.name());
Location markerLocation = r.teleportLocation().clone().add(0.5, 0, 0.5);
ArmorStand stand = (ArmorStand) r.location().getWorld().spawnEntity(markerLocation, EntityType.ARMOR_STAND);
stand.setVisible(false);
stand.setCustomName(r.coloredName());
stand.setMarker(true);
stand.setSmall(true);
stand.setRemoveWhenFarAway(true);
stand.setCustomNameVisible(true);
stand.setInvulnerable(true);
stand.setSilent(true);
m_armorStands.put(r, stand);
}*/
}
private void destroyHologram(Region r) {
if (!m_useHolograms) {
return;
}
Hologram hologram = m_regionHolograms.get(r);
if (hologram != null) {
hologram.delete();
}
/*log.info("Destroying hologram for " + r.name());
ArmorStand stand = m_armorStands.get(r);
if (stand != null && stand.isValid()) {
stand.remove();
}
m_armorStands.remove(r);*/
}
}

View File

@ -70,7 +70,7 @@ public class RegionCommand implements CommandExecutor, TabCompleter {
return true;
}
String subCommand = split[0];
if (subCommand.equals("create") && p.hasPermission("regions.create")) {
if (subCommand.equals("create") && p.hasPermission("regions.commands.create")) {
if (split.length <= 1) {
p.sendMessage("Must specify a region name");
return true;
@ -85,7 +85,7 @@ public class RegionCommand implements CommandExecutor, TabCompleter {
p.teleport(r.teleportLocation());
m_plugin.regionManager().addRegion(r);
m_plugin.saveRegions();
} else if (subCommand.equals("remove") && p.hasPermission("regions.remove")) {
} else if (subCommand.equals("remove") && p.hasPermission("regions.commands.remove")) {
Region r = m_plugin.regionManager().nearestRegion(p.getLocation());
if (r == null) {
p.sendMessage("There are no regions in this world.");
@ -94,7 +94,7 @@ public class RegionCommand implements CommandExecutor, TabCompleter {
m_plugin.regionManager().removeRegion(r);
p.sendMessage("Deleted region " + r.coloredName());
m_plugin.saveRegions();
} else if (subCommand.equals("regen") && p.hasPermission("regions.regen")) {
} else if (subCommand.equals("regen") && p.hasPermission("regions.commands.regen")) {
Region r = m_plugin.regionManager().nearestRegion(p.getLocation());
if (r == null) {
p.sendMessage("There are no regions in this world.");
@ -106,7 +106,7 @@ public class RegionCommand implements CommandExecutor, TabCompleter {
p.sendMessage("Region post regenerated.");
});
}
} else if (subCommand.equals("regenall") && p.hasPermission("regions.regen.all")) {
} else if (subCommand.equals("regenall") && p.hasPermission("regions.commands.regen.all")) {
for(Region r : m_plugin.regionManager().regionsForWorld(p.getLocation().getWorld())) {
m_plugin.getServer().getScheduler().runTask(m_plugin, () -> {
RegionPostBuilder builder = new RegionPostBuilder(r, m_plugin);

View File

@ -62,7 +62,7 @@ public class RegionOpCommand implements CommandExecutor, TabCompleter {
});
} else if (subCommand.equals("compass") && sender.hasPermission("regions.give-items.compass")) {
Player player = (Player)sender;
ItemStack compassItem = RegionPostItemWatcher.createCreateItem();
ItemStack compassItem = RegionPostItemWatcher.createCompass(m_plugin.regionManager().nearestRegion(player.getLocation()));
if (split.length > 1) {
compassItem.setAmount(Integer.parseInt(split[1]));
}
@ -72,11 +72,11 @@ public class RegionOpCommand implements CommandExecutor, TabCompleter {
}
} else if (subCommand.equals("item") && sender.hasPermission("regions.give-items.creator")) {
Player player = (Player)sender;
ItemStack createItem = RegionPostItemWatcher.createCreateItem();
ItemStack anchorStack = RegionPostItemWatcher.createAnchor();
if (split.length > 1) {
createItem.setAmount(Integer.parseInt(split[1]));
anchorStack.setAmount(Integer.parseInt(split[1]));
}
HashMap<Integer, ItemStack> rejected = player.getInventory().addItem(createItem);
HashMap<Integer, ItemStack> rejected = player.getInventory().addItem(anchorStack);
for(ItemStack item : rejected.values()) {
player.getLocation().getWorld().dropItem(player.getLocation(), item);
}

View File

@ -16,7 +16,7 @@ import us.camin.regions.Region;
import java.util.UUID;
public class RegionConfiguration implements ConfigurationSerializable {
public int x;
public int x;
public int y;
public int z;
public int visits;
@ -24,7 +24,7 @@ public class RegionConfiguration implements ConfigurationSerializable {
public boolean isHub;
public List<Pattern> patterns;
public List<UUID> seenBy;
public DyeColor color = DyeColor.YELLOW;
public DyeColor color = null;
public RegionConfiguration(Region region) {
Location loc = region.location();
@ -47,7 +47,10 @@ public class RegionConfiguration implements ConfigurationSerializable {
visits = (Integer)confSection.getOrDefault("visits", 0);
charges = (Integer)confSection.getOrDefault("charges", 0);
patterns = (List<Pattern>)confSection.getOrDefault("banner", new ArrayList<Pattern>());
color = DyeColor.valueOf((String)confSection.getOrDefault("color", "YELLOW"));
String colorStr = (String)confSection.getOrDefault("color", null);
if (colorStr != null) {
color = DyeColor.valueOf(colorStr);
}
seenBy = new ArrayList<UUID>();
List<String> strList = (List<String>)confSection.getOrDefault("seenBy", new ArrayList<String>());
for(String s : strList) {

View File

@ -25,26 +25,10 @@ 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)) {
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 m_neighbors.get(region);
}
return ret;
return new ArrayList<Region>();
}
public BorderMesh(Collection<Region> regions) {
@ -58,6 +42,24 @@ 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()];
@ -93,6 +95,27 @@ 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);
}
@ -108,12 +131,21 @@ public class BorderMesh {
DelaunayTriangulator mesh = new DelaunayTriangulator(regionPoints);
try {
mesh.triangulate();
log.info("Mesh triangulated!");
log.fine("Mesh triangulated!");
} catch (NullPointerException e) {
log.warning("Got a null pointer when triangulating, skipping world.");
return false;
} catch (NotEnoughPointsException e) {
log.info("Not enough points to triangulate mesh. Add more regions!");
log.info("Not enough points to triangulate mesh, all regions will be connected to each other. Add more regions!");
for(Region region : m_regions) {
HashSet<Region> neighbors = new HashSet<Region>();
for(Region neighbor : m_regions) {
if (neighbor != region) {
neighbors.add(neighbor);
}
}
m_neighbors.put(region, neighbors);
}
return false;
}
@ -135,11 +167,13 @@ 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> neighbors = new HashSet<Region>();
log.info("Executing voronoi transform...");
HashSet<Region> allNeighbors = new HashSet<Region>();
log.fine("Executing voronoi transform...");
for(Triangle tri : triangleSoup) {
if (tri.region == region) {
for (Region neighbor : m_regions) {
@ -149,46 +183,58 @@ 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)) {
neighbors.add(neighbor);
allNeighbors.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.info("Border for " + region.name() + " is defined by " + points.size() + " points");
log.fine("Border for " + region.name() + " is defined by " + points.size() + " points");
Polygon polygon = new Polygon(points);
m_polygons.put(region, polygon);
m_neighbors.put(region, neighbors);
}
// 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);
}
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

@ -22,6 +22,7 @@ import java.util.Map;
import java.util.HashMap;
import java.util.ArrayList;
import java.util.Optional;
import java.util.Random;
import org.bukkit.event.Listener;
import org.bukkit.event.EventHandler;
import org.bukkit.ChatColor;
@ -29,7 +30,10 @@ import org.bukkit.event.Event.Result;
import org.bukkit.DyeColor;
import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.Sound;
import org.bukkit.Particle;
import org.bukkit.entity.Player;
import org.bukkit.block.BlockFace;
import org.bukkit.block.Block;
import org.bukkit.Location;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack;
@ -40,6 +44,7 @@ import org.bukkit.enchantments.Enchantment;
import org.bukkit.inventory.ItemFlag;
import org.bukkit.Material;
import org.bukkit.material.MaterialData;
import org.bukkit.util.Vector;
import us.camin.regions.Plugin;
import us.camin.regions.Region;
@ -87,34 +92,43 @@ public class PlayerInventoryTeleporter implements Listener {
if (event.getClickedInventory() == neighborInv) {
ItemMeta meta = event.getCurrentItem().getItemMeta();
Material mat = event.getCurrentItem().getType();
if (mat == Material.BEDROCK || mat == Material.COMPASS || mat == Material.LANTERN) {
final String selectedName = meta.getDisplayName();
if (mat == Material.BEDROCK || mat == Material.COMPASS || mat == Material.LANTERN || mat == Material.SOUL_LANTERN) {
return;
}
m_plugin.getServer().getScheduler().runTask(m_plugin, () -> event.getView().close());
Region nearest = m_manager.nearestRegion(player.getLocation());
final String selectedName = meta.getDisplayName();
if (selectedName == nearest.name()) {
return;
}
m_plugin.getServer().getScheduler().runTask(m_plugin, () -> event.getView().close());
final Optional<Region> destination = m_plugin.regionManager().regionsForWorld(player.getLocation().getWorld())
.stream()
.filter((r) -> r.name().equals(selectedName))
.findFirst();
if (destination.isPresent()) {
player.sendMessage("Teleporting you there now...");
nearest.addCharges(-1);
destination.get().addCharges(1);
Location targetLocation = destination.get().teleportLocation();
int cost = nearest.getTravelCost(destination.get());
int chargesConsumed = Math.min(nearest.charges(), (int)cost - player.getLevel());
double payment = player.getLevel() + chargesConsumed;
double accuracy = 0.8 + (payment / cost) * 0.2;
if (chargesConsumed > 0) {
nearest.addCharges(-chargesConsumed);
player.sendMessage("You don't have enough XP. "+ chargesConsumed + " charges were consumed.");
}
player.giveExpLevels(-(int)cost);
m_plugin.getServer().getScheduler().runTask(m_plugin, () -> {
RegionPostBuilder builder = new RegionPostBuilder(nearest, m_plugin);
builder.updateLantern();
});
m_plugin.saveRegions();
new PlayerTeleporter(player, nearest.teleportLocation(), destination.get().teleportLocation(), m_plugin).teleport().thenRun(() -> {
new PlayerTeleporter(player, nearest.teleportLocation(), targetLocation, m_plugin, accuracy).teleport().thenRun(() -> {
RegionPostBuilder builder = new RegionPostBuilder(destination.get(), m_plugin);
builder.updateLantern();
});
if (nearest.charges() == 0) {
nearest.location().getWorld().playSound(player.getLocation(), Sound.ENTITY_ITEM_BREAK, (float)1.0, (float)1.0);
//player.sendMessage("You've consumed this region post's last charge! You won't be able to return without recharging it.");
}
} else {
player.sendMessage("There is no region with that name. This is a bug.");
}
@ -169,6 +183,15 @@ public class PlayerInventoryTeleporter implements Listener {
meta.addItemFlags(ItemFlag.HIDE_ENCHANTS);
meta.addEnchant(Enchantment.SOUL_SPEED, 1, true);
}
int cost = event.region.getTravelCost(region);
int baseCost = event.region.getBaseTravelCost(region);
if (cost != baseCost) {
lore.add(ChatColor.WHITE + "Travel cost: " + ChatColor.YELLOW + ChatColor.STRIKETHROUGH + Math.round(baseCost) + ChatColor.RESET + ChatColor.YELLOW + ChatColor.BOLD + " " + Math.round(cost) + " levels");
} else if (cost > 0) {
lore.add(ChatColor.WHITE + "Travel cost: " + ChatColor.YELLOW + Math.round(cost) + " levels");
} else {
lore.add(ChatColor.WHITE + "Travel cost: " + ChatColor.GREEN + "Free!");
}
lore.add(ChatColor.WHITE + "Distance: " + ChatColor.YELLOW + Math.round(region.location().distance(event.region.location())));
lore.add(ChatColor.WHITE + "Population: " + ChatColor.YELLOW + m_plugin.playerWatcher().playersInRegion(region).size());
lore.add(ChatColor.WHITE + "Altitude: " + ChatColor.YELLOW + altitude);
@ -180,6 +203,9 @@ public class PlayerInventoryTeleporter implements Listener {
}
}
lore.add("Nearby connections: " + String.join(", ", neighborNames));
if (event.player.getLevel() < cost) {
lore.add("" + ChatColor.RED + ChatColor.BOLD + "You don't have enough XP! Travel may be dangerous...");
}
} else {
lore.add("" + ChatColor.BOLD + ChatColor.GOLD + "You haven't discovered this location yet!");
lore.add(ChatColor.WHITE + "Distance: " + ChatColor.YELLOW + ChatColor.MAGIC + Math.round(region.location().distance(event.region.location())));
@ -193,7 +219,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 Configuration");
meta.setDisplayName(ChatColor.BOLD + "Region Post Charges");
ArrayList<String> lore = new ArrayList<String>();
lore.add(ChatColor.WHITE + "Name: "+event.region.coloredName());
lore.add(ChatColor.WHITE + "Visits: " + ChatColor.YELLOW + event.region.visits());
@ -203,7 +229,18 @@ 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(22, chargesItem);
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);
event.player.openInventory(neighborInventory);
}

View File

@ -6,8 +6,12 @@ import org.bukkit.scheduler.BukkitTask;
import org.bukkit.Particle;
import org.bukkit.event.player.PlayerTeleportEvent.TeleportCause;
import org.bukkit.scheduler.BukkitScheduler;
import org.bukkit.ChatColor;
import org.bukkit.World;
import org.bukkit.Sound;
import org.bukkit.block.BlockFace;
import org.bukkit.block.Block;
import java.util.Random;
import io.papermc.lib.PaperLib;
import us.camin.regions.Plugin;
@ -15,6 +19,7 @@ import java.util.logging.Logger;
import org.bukkit.potion.PotionEffectType;
import org.bukkit.potion.PotionEffect;
import java.util.concurrent.CompletableFuture;
import org.bukkit.util.Vector;
public class PlayerTeleporter {
Logger log = Logger.getLogger("Regions.PlayerTeleporter");
@ -23,12 +28,30 @@ public class PlayerTeleporter {
private Location m_src;
private Location m_dest;
private Plugin m_plugin;
private double m_accuracy;
public PlayerTeleporter(Player p, Location src, Location dest, Plugin plugin) {
public PlayerTeleporter(Player p, Location src, Location dest, Plugin plugin, double accuracy) {
this.m_player = p;
this.m_src = src;
this.m_dest = dest;
this.m_plugin = plugin;
this.m_accuracy = accuracy;
if (m_accuracy < 1) {
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);
travelVec.rotateAroundY(angleDelta);
double distanceToDest = m_src.distance(m_dest);
double distanceDelta = (distanceToDest/3) * (rand.nextGaussian() - 0.5) * (1 - accuracy);
double targetDistance = distanceToDest - distanceDelta;
travelVec.multiply(targetDistance);
m_dest = m_src.clone().add(travelVec);
}
}
public CompletableFuture<Void> teleport() {
@ -37,6 +60,22 @@ public class PlayerTeleporter {
BukkitTask hoverGenerator = scheduler.runTaskTimer(m_plugin, () -> applyHoverEffect(), 0, 10);
BukkitTask particleGenerator = scheduler.runTaskTimer(m_plugin, () -> spawnHoverParticles(), 0, 1);
BukkitTask noiseGenerator = scheduler.runTaskTimer(m_plugin, () -> {
if (m_accuracy < 1) {
m_player.getLocation().getWorld().playSound(m_player.getLocation(), Sound.ENTITY_GENERIC_EXPLODE, (float)0.5, (float)1.0);
m_player.getLocation().getWorld().spawnParticle(Particle.EXPLOSION_LARGE, m_player.getLocation(), 3, 1, 1, 1);
}
}, 20, 15);
if (m_accuracy < 1) {
m_player.sendMessage("Teleporting you there-ish now...");
m_plugin.getServer().getScheduler().runTaskLater(m_plugin, () -> {
m_player.sendMessage("" + ChatColor.RED + ChatColor.BOLD + "Oh no!" + ChatColor.RESET + ChatColor.YELLOW + " The region post is malfunctioning!");
}, 40);
} else {
m_player.sendMessage("Teleporting you there now...");
}
playTeleportSound();
m_plugin.getServer().getScheduler().runTaskLater(m_plugin, () -> {
@ -46,11 +85,23 @@ public class PlayerTeleporter {
m_plugin.getServer().getScheduler().runTaskLater(m_plugin, () -> {
spawnTeleportStartParticles();
PaperLib.teleportAsync(m_player, m_dest, TeleportCause.COMMAND).thenAccept(res -> {
Block targetBlock = m_dest.getBlock();
if (!targetBlock.isPassable() && !targetBlock.getRelative(BlockFace.DOWN).isPassable()) {
while (!(targetBlock.isPassable() && targetBlock.getRelative(BlockFace.UP).isEmpty() && !targetBlock.getRelative(BlockFace.DOWN).isPassable())) {
targetBlock = targetBlock.getRelative(BlockFace.UP);
}
m_dest = targetBlock.getLocation().add(0.5, 0, 0.5);
m_player.teleport(m_dest, TeleportCause.COMMAND);
}
m_player.addPotionEffect(new PotionEffect(PotionEffectType.BLINDNESS, 60, 1, false, false, false));
m_player.addPotionEffect(new PotionEffect(PotionEffectType.SLOW, 40, 1, false, false, false));
if (m_accuracy < 1) {
m_player.addPotionEffect(new PotionEffect(PotionEffectType.SLOW_FALLING, 20 * 5, 1, false, true, false));
}
spawnEndParticles();
particleGenerator.cancel();
hoverGenerator.cancel();
noiseGenerator.cancel();
ret.complete(null);
});
}, 20 * 6);
@ -63,11 +114,22 @@ public class PlayerTeleporter {
}
void spawnEndParticles() {
World world = m_player.getLocation().getWorld();
final World world = m_player.getLocation().getWorld();
world.playSound(m_dest, Sound.BLOCK_PORTAL_TRAVEL, (float)0.5, (float)1.0);
world.spawnParticle(Particle.CLOUD, m_dest, 70, 1, 1, 1);
world.spawnParticle(Particle.SPELL_MOB, m_dest, 5, 2, 2, 2);
world.spawnParticle(Particle.PORTAL, m_player.getLocation(), 70, 1, 1, 1);
if (m_accuracy < 1) {
BukkitTask fireGenerator = m_plugin.getServer().getScheduler().runTaskTimer(m_plugin, () -> {
world.spawnParticle(Particle.FLAME, m_player.getLocation(), 80, 1, 1, 1, 0);
}, 0, 8);
m_plugin.getServer().getScheduler().runTaskLater(m_plugin, () -> {
fireGenerator.cancel();
}, 20 * 8);
m_player.sendMessage("" + ChatColor.RED + ChatColor.BOLD + "Ouch!" + ChatColor.RESET + ChatColor.YELLOW + " You didn't have enough XP and the region post malfunctioned.");
m_player.damage(5 * (1.0-m_accuracy));
world.playSound(m_player.getLocation(), Sound.ENTITY_GENERIC_EXPLODE, (float)1.0, (float)1.0);
}
}
void applyHoverEffect() {
@ -84,5 +146,8 @@ public class PlayerTeleporter {
m_src.getWorld().playSound(m_src, Sound.BLOCK_PORTAL_TRIGGER, (float)0.5, (float)0.6);
m_src.getWorld().playSound(m_src, Sound.BLOCK_RESPAWN_ANCHOR_DEPLETE, (float)0.5, (float)1.0);
m_src.getWorld().playSound(m_dest, Sound.BLOCK_RESPAWN_ANCHOR_CHARGE, (float)0.5, (float)1.0);
if (m_accuracy < 1) {
m_src.getWorld().playSound(m_src, Sound.ENTITY_ITEM_BREAK, (float)1.0, (float)1.0);
}
}
}

Binary file not shown.

After

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.16
softdepend: [dynmap, HolographicDisplays, ProtocolLib]
api-version: 1.19
softdepend: [dynmap, ProtocolLib, Lands]
commands:
regions:
description: "List available regions."
@ -21,7 +21,8 @@ permissions:
description: Allows use of all regions permissions
children:
regions.create: true
regions.remove: true
regions.create.bypass: true
regions.commands.*: true
regions.regen.*: true
regions.bypass.*: true
regions.give-items.*: true
@ -35,15 +36,30 @@ permissions:
default: true
description: Use region posts
regions.create:
default: true
description: Create a region with a region item
regions.create.bypass:
default: op
description: Create a region
regions.remove:
description: Bypass anything preventing creation of a new region
regions.setbanner:
default: true
description: Allows setting a region post banner
regions.charge:
default: true
description: Allows charging a region post with a charge item
regions.commands.*:
default: op
children:
regions.commands.remove: true
regions.commands.regen: true
regions.commands.regen.all: true
regions.commands.remove:
default: op
description: Remove a region
regions.regen:
regions.commands.regen:
default: op
description: Regenerates a region post
regions.regen.all:
regions.commands.regen.all:
default: op
description: Regenerates all region posts, including in unloaded chunks
regions.bypass.*: