Compare commits

...

24 Commits

Author SHA1 Message Date
a3a75bb8ee items: cheaper recipe, plus custom model data for future resource pack work
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2023-02-18 15:05:28 +01:00
61f4dc90dc Bump version 2022-09-18 15:03:27 +02:00
aafeb9fe34 geometry: verbose-- 2022-09-18 15:02:10 +02:00
6d8b291b2f ui: display banner instructions in UI 2022-09-18 15:01:58 +02:00
244bb32d70 plugin: handle failure to load more gracefully 2022-09-18 15:01:14 +02:00
d247953546 regionmanager: basic lands plugin integration 2022-09-18 14:11:43 +02:00
669fab22cd regionpostitemwatcher: rewrite region charge recipe to be cheaper 2022-09-18 12:31:41 +02:00
221539001e rip pl3xmap 2022-09-18 12:31:20 +02:00
61d0af1a05 pom: version bump 2022-09-18 12:26:11 +02:00
75087b23e1 region: jump cost to hub is always 1, but spokes are more expensive 2022-09-11 22:28:54 +02:00
8af0909110 playernotifier: fix 1.17 protocol support 2021-07-09 10:05:12 -07:00
9944a7bcdb dynmapeventrelay: fix method visibility 2021-07-08 07:56:53 -07:00
e21115cc65 geometry: bordermesh: fix bug with one-way routes, move route calculation into triangulate()
Fixes #19
2021-07-08 07:55:50 -07:00
51fdc80542 pl3xmap: use lantern icons for region posts 2021-07-08 07:49:45 -07:00
ff70dd255b plugin: Add some preliminary pl3xmap support 2021-07-08 00:34:12 -07:00
b5a28e40cf Release 0.3.0 2021-07-07 21:57:12 -07:00
4bfce1fd8d version: rc6 2021-06-15 14:46:57 -07:00
81960d14ce playernotifier: drop last usage of deprecated paperlib Title system 2021-06-15 14:46:31 -07:00
4bbada298f readme++ 2021-06-15 14:45:53 -07:00
e3afbdc3c7 plugin: add bstats telemetry before 0.3 release for feedback 2021-06-15 13:29:43 -07:00
034527c208 regionpostitemwatcher: implement crafting recipes for anchors, charges, and compasses 2021-06-15 13:08:10 -07:00
092ed9c6d0 geometry: bordermesh: fix region linking to prevent routes that run through unrelated regions 2021-06-15 13:07:29 -07:00
ac14a78d7f ui: playerteleporter: reduce the rotation error from a misfire that is just plain mean on long jumps 2021-06-15 13:06:59 -07:00
eaa8313cd1 dynmap: update dynmap to fix borders, show costs on routes, and use a special icon for hubs 2021-06-15 13:06:09 -07:00
18 changed files with 449 additions and 142 deletions

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

30
pom.xml
View File

@ -4,17 +4,23 @@
<groupId>us.camin.regions</groupId>
<artifactId>Regions</artifactId>
<packaging>jar</packaging>
<version>0.2.99-rc4</version>
<version>0.4.0</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.6.0</version>
<version>4.7.0-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
@ -40,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>
@ -87,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>
@ -167,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,30 +127,16 @@ 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 {
// Fallback to approximated colors
event.player.sendTitle(event.newRegion.coloredName(), "Population: " + pop + " Altitude: " + altitude);
}
}
@ -107,28 +148,17 @@ 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

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;
@ -73,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.");
}
@ -86,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() {
@ -94,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() {

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 50% cheaper, before charges applied
baseCost /= 2;
// Travel *to* a hub is always 1
baseCost = 1;
}
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 = 500;
double blocksPerXP = 768;
return Math.max(1, (int)(distance / blocksPerXP));
}

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

@ -31,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;
@ -45,6 +48,30 @@ 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) {
@ -53,6 +80,9 @@ 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 + ")");
@ -64,6 +94,7 @@ 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;
}
@ -77,6 +108,7 @@ 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;
}
@ -90,6 +122,7 @@ 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;
}
@ -153,28 +186,32 @@ 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() && player.hasPermission("regions.create")) {
} 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("") || meta.getDisplayName().equals("Region Post Anchor")) {
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(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());
}
player.sendMessage("You aren't allowed to create a region here.");
}
}
}

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,7 +131,7 @@ 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;
@ -144,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) {
@ -158,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

@ -92,13 +92,19 @@ 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))
@ -213,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());
@ -223,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

@ -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 / 3) * (rand.nextGaussian() - 0.5) * (1-accuracy);
double angleDelta = (Math.PI / 5) * (rand.nextGaussian() - 0.5) * (1-accuracy);
travelVec.rotateAroundY(angleDelta);
double distanceToDest = m_src.distance(m_dest);

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, ProtocolLib]
api-version: 1.19
softdepend: [dynmap, ProtocolLib, Lands]
commands:
regions:
description: "List available regions."
@ -21,6 +21,7 @@ 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
@ -37,6 +38,9 @@ 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