9 Commits

12 changed files with 206 additions and 47 deletions

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

18
pom.xml
View File

@ -4,13 +4,19 @@
<groupId>us.camin.regions</groupId>
<artifactId>Regions</artifactId>
<packaging>jar</packaging>
<version>0.2.99-rc4</version>
<version>0.3.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>
@ -45,12 +51,6 @@
<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 +87,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>

View File

@ -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

@ -35,7 +35,6 @@ import com.comphenix.protocol.PacketType;
import com.comphenix.protocol.wrappers.EnumWrappers.TitleAction;
import com.comphenix.protocol.wrappers.WrappedChatComponent;
import com.destroystokyo.paper.Title;
import java.util.logging.Logger;
import us.camin.regions.events.PlayerMoveInEvent;
@ -126,9 +125,7 @@ public class PlayerNotifier implements Listener {
}
} 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);
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;
@ -86,6 +91,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() {

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,32 @@ 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.GLOWSTONE_DUST);
chargeRecipe.setIngredient('G', Material.GHAST_TEAR);
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.LANTERN);
NamespacedKey compassKey = new NamespacedKey(m_plugin, "region_post_compass");
ShapedRecipe compassRecipe = new ShapedRecipe(compassKey, m_theCompass);
// Uses four fewer charges, slightly cheaper.
// TODO: Maybe we just want this to be glowstone instead of effectively 4
// ghast tears?
compassRecipe.shape(" D ", "DGD", " D ");
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 +82,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 + ")");

View File

@ -24,23 +24,31 @@ public class BorderMesh {
Map<Region, Polygon> m_polygons;
Map<Region, Set<Region>> m_neighbors;
// TODO: Probably need to cache the neighbors after doing all this
// intersection work! Should be generated during triangulate()
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");
for(Region neighbor : allNeighbors) {
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)) {
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)) {
crossings++;
}
}
}
} else {
//log.trace("Region " + region.name() + " is not a frontier");
for(Region neighbor : allNeighbors) {
ret.add(neighbor);
if (crossings == 1 || crossings == 0) {
ret.add(neighbor);
}
}
}
@ -58,6 +66,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 +119,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);
}
@ -188,16 +235,6 @@ public class BorderMesh {
}
}*/
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

@ -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);