From 27cec3bcad30ce7887931e9f3e0e05c58b22e357 Mon Sep 17 00:00:00 2001
From: Runemoro <runemoro@users.noreply.github.com>
Date: Wed, 17 Jan 2018 00:44:44 -0500
Subject: [PATCH] Destination system and registry rewrite

 - Major simplifincations to the TileEntityRift class and destination classes
 - Use a graph in the RiftRegistry for simpler tracking of rift sources and targets
---
 build.gradle                                  |   1 +
 .../java/org/dimdev/ddutils/EntityUtils.java  |   8 +-
 .../org/dimdev/ddutils/RotatedLocation.java   |   2 +-
 .../java/org/dimdev/ddutils/TypeFilter.java   |  17 +
 .../java/org/dimdev/ddutils/nbt/NBTUtils.java |   3 +-
 .../dimdev/dimdoors/ddutils/GraphUtils.java   |  20 +
 .../dimdev/dimdoors/shared/CommonProxy.java   |   3 +-
 .../dimdev/dimdoors/shared/EventHandler.java  |   6 +-
 .../dimdoors/shared/VirtualLocation.java      |   4 +-
 .../shared/blocks/BlockDimensionalDoor.java   |   5 +-
 .../blocks/BlockDimensionalDoorGold.java      |  16 +-
 .../blocks/BlockDimensionalDoorIron.java      |   6 +-
 .../blocks/BlockDimensionalDoorPersonal.java  |   5 +-
 .../blocks/BlockDimensionalTrapdoorWood.java  |   2 +-
 .../dimdoors/shared/blocks/IRiftProvider.java |   3 -
 .../shared/commands/CommandDimTeleport.java   |   4 +-
 .../shared/commands/CommandPocket.java        |   4 +-
 .../shared/items/ItemDimensionalDoorGold.java |   1 -
 .../shared/items/ItemRiftSignature.java       |   4 +-
 .../items/ItemStabilizedRiftSignature.java    |   2 +-
 .../dimdoors/shared/pockets/Pocket.java       | 130 +++---
 .../shared/pockets/PocketGenerator.java       |   4 +-
 .../shared/pockets/PocketRegistry.java        |  14 +-
 .../shared/pockets/PocketTemplate.java        |  10 +-
 .../{ => pockets}/SchematicHandler.java       |   4 +-
 .../shared/rifts/RiftDestination.java         |  44 +-
 .../dimdoors/shared/rifts/RiftRegistry.java   | 292 ------------
 .../dimdoors/shared/rifts/TileEntityRift.java | 306 ++++---------
 .../shared/rifts/WeightedRiftDestination.java |  58 ---
 .../AvailableLinkDestination.java             | 114 ++---
 .../rifts/destinations/EscapeDestination.java |  11 +-
 .../rifts/destinations/GlobalDestination.java |   9 +-
 .../rifts/destinations/LimboDestination.java  |   4 +-
 .../destinations/LinkingDestination.java      |  60 +++
 .../rifts/destinations/LocalDestination.java  |   9 +-
 .../destinations/NewPublicDestination.java    |  45 --
 .../PocketEntranceDestination.java            |  17 +-
 .../destinations/PocketExitDestination.java   |   6 +-
 .../destinations/PrivateDestination.java      |  47 +-
 .../PrivatePocketExitDestination.java         |  58 +--
 .../destinations/PublicPocketDestination.java |  33 ++
 .../destinations/RelativeDestination.java     |   9 +-
 .../LinkProperties.java}                      |   5 +-
 .../rifts/registry/PlayerRiftPointer.java     |  13 +
 .../rifts/registry/PocketEntrancePointer.java |  12 +
 .../shared/rifts/registry/RegistryVertex.java |  26 ++
 .../dimdoors/shared/rifts/registry/Rift.java  |  57 +++
 .../rifts/registry/RiftPlaceholder.java       |   3 +
 .../shared/rifts/registry/RiftRegistry.java   | 416 ++++++++++++++++++
 .../tools/PocketSchematicGenerator.java       |  17 +-
 .../world/limbodimension/BiomeLimbo.java      |   1 -
 .../world/pocketdimension/BiomeBlank.java     |   1 -
 52 files changed, 1033 insertions(+), 918 deletions(-)
 create mode 100644 src/main/java/org/dimdev/ddutils/TypeFilter.java
 create mode 100644 src/main/java/org/dimdev/dimdoors/ddutils/GraphUtils.java
 rename src/main/java/org/dimdev/dimdoors/shared/{ => pockets}/SchematicHandler.java (99%)
 delete mode 100644 src/main/java/org/dimdev/dimdoors/shared/rifts/RiftRegistry.java
 delete mode 100644 src/main/java/org/dimdev/dimdoors/shared/rifts/WeightedRiftDestination.java
 create mode 100644 src/main/java/org/dimdev/dimdoors/shared/rifts/destinations/LinkingDestination.java
 delete mode 100644 src/main/java/org/dimdev/dimdoors/shared/rifts/destinations/NewPublicDestination.java
 create mode 100644 src/main/java/org/dimdev/dimdoors/shared/rifts/destinations/PublicPocketDestination.java
 rename src/main/java/org/dimdev/dimdoors/shared/rifts/{AvailableLink.java => registry/LinkProperties.java} (86%)
 create mode 100644 src/main/java/org/dimdev/dimdoors/shared/rifts/registry/PlayerRiftPointer.java
 create mode 100644 src/main/java/org/dimdev/dimdoors/shared/rifts/registry/PocketEntrancePointer.java
 create mode 100644 src/main/java/org/dimdev/dimdoors/shared/rifts/registry/RegistryVertex.java
 create mode 100644 src/main/java/org/dimdev/dimdoors/shared/rifts/registry/Rift.java
 create mode 100644 src/main/java/org/dimdev/dimdoors/shared/rifts/registry/RiftPlaceholder.java
 create mode 100644 src/main/java/org/dimdev/dimdoors/shared/rifts/registry/RiftRegistry.java

diff --git a/build.gradle b/build.gradle
index ab0042d7..cfa59951 100644
--- a/build.gradle
+++ b/build.gradle
@@ -64,6 +64,7 @@ configurations {
 
 dependencies {
     embed 'com.flowpowered:flow-math:1.0.3'
+    embed 'org.jgrapht:jgrapht-core:1.1.0'
     compile 'com.github.DimensionalDevelopment:AnnotatedNBT:-SNAPSHOT'
 }
 
diff --git a/src/main/java/org/dimdev/ddutils/EntityUtils.java b/src/main/java/org/dimdev/ddutils/EntityUtils.java
index c746578e..67192595 100644
--- a/src/main/java/org/dimdev/ddutils/EntityUtils.java
+++ b/src/main/java/org/dimdev/ddutils/EntityUtils.java
@@ -7,9 +7,11 @@ import net.minecraft.entity.item.EntityItem;
 import net.minecraft.entity.player.EntityPlayer;
 import net.minecraft.entity.projectile.*;
 
+import java.util.UUID;
+
 public final class EntityUtils {
 
-    public static String getEntityOwnerUUID(Entity entity) { // TODO: make this recursive
+    public static UUID getEntityOwnerUUID(Entity entity) { // TODO: make this recursive
         if (entity instanceof EntityThrowable) entity = ((EntityThrowable) entity).getThrower();
         if (entity instanceof EntityArrow) entity = ((EntityArrow) entity).shootingEntity;
         if (entity instanceof EntityFireball) entity = ((EntityFireball) entity).shootingEntity;
@@ -25,8 +27,8 @@ public final class EntityUtils {
             if (player != null) entity = player;
         }
 
-        if (entity instanceof IEntityOwnable && ((IEntityOwnable) entity).getOwnerId() != null) return ((IEntityOwnable) entity).getOwnerId().toString();
-        if (entity instanceof EntityPlayer) return entity.getUniqueID().toString(); // ownable players shouldn't be a problem, but just in case we have a slave mod, check their owner's uuid first to send them to their owner's pocket :)
+        if (entity instanceof IEntityOwnable && ((IEntityOwnable) entity).getOwnerId() != null) return ((IEntityOwnable) entity).getOwnerId();
+        if (entity instanceof EntityPlayer) return entity.getUniqueID(); // ownable players shouldn't be a problem, but just in case we have a slave mod, check their owner's uuid first to send them to their owner's pocket :)
         return null;
     }
 }
diff --git a/src/main/java/org/dimdev/ddutils/RotatedLocation.java b/src/main/java/org/dimdev/ddutils/RotatedLocation.java
index c2404b63..1b9b85e9 100644
--- a/src/main/java/org/dimdev/ddutils/RotatedLocation.java
+++ b/src/main/java/org/dimdev/ddutils/RotatedLocation.java
@@ -8,7 +8,7 @@ import org.dimdev.annotatednbt.Saved;
 import org.dimdev.annotatednbt.NBTSerializable;
 
 @ToString @AllArgsConstructor @NoArgsConstructor
-@NBTSerializable public class RotatedLocation implements INBTStorable {
+@NBTSerializable public class RotatedLocation implements INBTStorable { // TODO: extend Location
     @Getter @Saved /*private*/ Location location;
     @Getter @Saved /*private*/ float yaw;
     @Getter @Saved /*private*/ float pitch;
diff --git a/src/main/java/org/dimdev/ddutils/TypeFilter.java b/src/main/java/org/dimdev/ddutils/TypeFilter.java
new file mode 100644
index 00000000..7d9bc987
--- /dev/null
+++ b/src/main/java/org/dimdev/ddutils/TypeFilter.java
@@ -0,0 +1,17 @@
+package org.dimdev.ddutils;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+public final class TypeFilter {
+    public static <T extends U, U> List<T> filter(Collection<U> list, Class<T> filterClass) {
+        List<T> filtered = new ArrayList<>();
+        for (U e : list) {
+            if (filterClass.isAssignableFrom(e.getClass())) {
+                filtered.add(filterClass.cast(e));
+            }
+        }
+        return filtered;
+    }
+}
diff --git a/src/main/java/org/dimdev/ddutils/nbt/NBTUtils.java b/src/main/java/org/dimdev/ddutils/nbt/NBTUtils.java
index 0403cd79..bda8c7cf 100644
--- a/src/main/java/org/dimdev/ddutils/nbt/NBTUtils.java
+++ b/src/main/java/org/dimdev/ddutils/nbt/NBTUtils.java
@@ -19,7 +19,7 @@ public final class NBTUtils {
         }
     }
 
-    public static void readFromNBT(Object obj, NBTTagCompound nbt) {
+    public static <T> T readFromNBT(T obj, NBTTagCompound nbt) {
         try {
             Class<?> callingClass = Class.forName(new Exception().getStackTrace()[1].getClassName());
             Class<?> nbtWriter = Class.forName(callingClass.getPackage().getName() + "." + callingClass.getSimpleName() + "NBTWriter");
@@ -28,5 +28,6 @@ public final class NBTUtils {
         } catch (ClassNotFoundException|NoSuchMethodException|IllegalAccessException|InvocationTargetException e) {
             throw new RuntimeException(e);
         }
+        return obj;
     }
 }
diff --git a/src/main/java/org/dimdev/dimdoors/ddutils/GraphUtils.java b/src/main/java/org/dimdev/dimdoors/ddutils/GraphUtils.java
new file mode 100644
index 00000000..ff9e809f
--- /dev/null
+++ b/src/main/java/org/dimdev/dimdoors/ddutils/GraphUtils.java
@@ -0,0 +1,20 @@
+package org.dimdev.dimdoors.ddutils;
+
+import org.jgrapht.Graph;
+
+public final class GraphUtils {
+    public static <V, E> void replaceVertex(Graph<V, E> graph, V vertex, V replace) {
+        graph.addVertex(replace);
+        for (E edge : graph.outgoingEdgesOf(vertex)) graph.addEdge(replace, graph.getEdgeTarget(edge), edge);
+        for (E edge : graph.incomingEdgesOf(vertex)) graph.addEdge(graph.getEdgeSource(edge), replace, edge);
+        graph.removeVertex(vertex);
+    }
+
+    public static <V, E> V followPointer(Graph<V, E> graph, V pointer) {
+        if (pointer != null) {
+            E edge = graph.outgoingEdgesOf(pointer).stream().findFirst().orElse(null);
+            return graph.getEdgeTarget(edge);
+        }
+        return null;
+    }
+}
diff --git a/src/main/java/org/dimdev/dimdoors/shared/CommonProxy.java b/src/main/java/org/dimdev/dimdoors/shared/CommonProxy.java
index b65c7c2c..7b2b41b6 100644
--- a/src/main/java/org/dimdev/dimdoors/shared/CommonProxy.java
+++ b/src/main/java/org/dimdev/dimdoors/shared/CommonProxy.java
@@ -7,6 +7,7 @@ import org.dimdev.dimdoors.DimDoors;
 import org.dimdev.dimdoors.shared.blocks.ModBlocks;
 import org.dimdev.dimdoors.shared.entities.EntityMonolith;
 import org.dimdev.dimdoors.shared.items.ModItems;
+import org.dimdev.dimdoors.shared.pockets.SchematicHandler;
 import org.dimdev.dimdoors.shared.rifts.*;
 import org.dimdev.dimdoors.shared.rifts.destinations.*;
 import org.dimdev.dimdoors.shared.sound.ModSounds;
@@ -44,7 +45,7 @@ public abstract class CommonProxy {
         RiftDestination.destinationRegistry.put("global", GlobalDestination.class);
         RiftDestination.destinationRegistry.put("limbo", LimboDestination.class);
         RiftDestination.destinationRegistry.put("local", LocalDestination.class);
-        RiftDestination.destinationRegistry.put("new_public", NewPublicDestination.class);
+        RiftDestination.destinationRegistry.put("public_pocket", PublicPocketDestination.class);
         RiftDestination.destinationRegistry.put("pocket_entrance", PocketEntranceDestination.class);
         RiftDestination.destinationRegistry.put("pocket_exit", PocketExitDestination.class);
         RiftDestination.destinationRegistry.put("private", PrivateDestination.class);
diff --git a/src/main/java/org/dimdev/dimdoors/shared/EventHandler.java b/src/main/java/org/dimdev/dimdoors/shared/EventHandler.java
index 0afd537d..eeb83a61 100644
--- a/src/main/java/org/dimdev/dimdoors/shared/EventHandler.java
+++ b/src/main/java/org/dimdev/dimdoors/shared/EventHandler.java
@@ -10,7 +10,7 @@ import net.minecraftforge.fml.common.eventhandler.EventPriority;
 import net.minecraftforge.fml.common.eventhandler.SubscribeEvent;
 import net.minecraftforge.fml.common.gameevent.PlayerEvent;
 import org.dimdev.dimdoors.shared.pockets.PocketRegistry;
-import org.dimdev.dimdoors.shared.rifts.RiftRegistry;
+import org.dimdev.dimdoors.shared.rifts.registry.RiftRegistry;
 import org.dimdev.dimdoors.shared.world.ModDimensions;
 
 public final class EventHandler {
@@ -34,7 +34,7 @@ public final class EventHandler {
             if (!world.isRemote
                 && !player.isDead
                 && ModDimensions.isDimDoorsPocketDimension(world)
-                && !PocketRegistry.getForDim(dim).isPlayerAllowedToBeHere(player, player.getPosition())) {
+                && !PocketRegistry.instance(dim).isPlayerAllowedToBeHere(player, player.getPosition())) {
                 // TODO: make the world circular
             }
         }
@@ -44,7 +44,7 @@ public final class EventHandler {
     public static void onDimensionChange(PlayerEvent.PlayerChangedDimensionEvent event) {
         // TODO: PocketLib compatibility
         if (ModDimensions.isDimDoorsPocketDimension(event.fromDim) && !ModDimensions.isDimDoorsPocketDimension(event.toDim)) {
-            RiftRegistry.setOverworldRift(event.player.getCachedUniqueIdString(), null);
+            RiftRegistry.instance().setOverworldRift(event.player.getUniqueID(), null);
         }
     }
 }
diff --git a/src/main/java/org/dimdev/dimdoors/shared/VirtualLocation.java b/src/main/java/org/dimdev/dimdoors/shared/VirtualLocation.java
index 18cfee90..b62be6c7 100644
--- a/src/main/java/org/dimdev/dimdoors/shared/VirtualLocation.java
+++ b/src/main/java/org/dimdev/dimdoors/shared/VirtualLocation.java
@@ -25,11 +25,11 @@ import org.dimdev.dimdoors.shared.world.limbodimension.WorldProviderLimbo;
     public static VirtualLocation fromLocation(Location location) {
         VirtualLocation virtualLocation = null;
         if (ModDimensions.isDimDoorsPocketDimension(location.getDim())) {
-            Pocket pocket = PocketRegistry.getForDim(location.getDim()).getPocketAt(location.getPos());
+            Pocket pocket = PocketRegistry.instance(location.getDim()).getPocketAt(location.getPos());
             if (pocket != null) {
                 virtualLocation = pocket.getVirtualLocation(); // TODO: pocket-relative coordinates
             } else {
-                virtualLocation = new VirtualLocation(0, 0, 0, 0); // TODO: door was placed in a pocket dim but outside of a pocket...
+                virtualLocation = null; // TODO: door was placed in a pocket dim but outside of a pocket...
             }
         } else if (location.getWorld().provider instanceof WorldProviderLimbo) {
             virtualLocation = new VirtualLocation(location.getDim(), location.getX(), location.getZ(), Config.getMaxDungeonDepth());
diff --git a/src/main/java/org/dimdev/dimdoors/shared/blocks/BlockDimensionalDoor.java b/src/main/java/org/dimdev/dimdoors/shared/blocks/BlockDimensionalDoor.java
index 3bdaf81d..49725de2 100644
--- a/src/main/java/org/dimdev/dimdoors/shared/blocks/BlockDimensionalDoor.java
+++ b/src/main/java/org/dimdev/dimdoors/shared/blocks/BlockDimensionalDoor.java
@@ -15,7 +15,8 @@ import net.minecraft.util.EnumFacing;
 import net.minecraft.util.EnumHand;
 import net.minecraft.util.math.BlockPos;
 import net.minecraft.world.World;
-import org.dimdev.dimdoors.shared.rifts.RiftRegistry;
+import org.dimdev.ddutils.Location;
+import org.dimdev.dimdoors.shared.rifts.registry.RiftRegistry;
 import org.dimdev.dimdoors.shared.tileentities.TileEntityEntranceRift;
 import org.dimdev.dimdoors.shared.tileentities.TileEntityFloatingRift;
 
@@ -130,7 +131,7 @@ public abstract class BlockDimensionalDoor extends BlockDoor implements IRiftPro
         TileEntityEntranceRift rift = getRift(world, pos, state);
         super.breakBlock(world, pos, state);
         if (world.isRemote) return;
-        if (rift.isPlaceRiftOnBreak() || rift.isRegistered() && RiftRegistry.getRiftInfo(rift.getLocation()).getSources().size() > 0 && !rift.isAlwaysDelete()) {
+        if (rift.isPlaceRiftOnBreak() || rift.isRegistered() && RiftRegistry.instance().getSources(new Location(rift.getWorld(), rift.getPos())).size() > 0 && !rift.isAlwaysDelete()) {
             world.setBlockState(rift.getPos(), ModBlocks.RIFT.getDefaultState());
             TileEntityFloatingRift newRift = (TileEntityFloatingRift) world.getTileEntity(pos);
             newRift.copyFrom(rift);
diff --git a/src/main/java/org/dimdev/dimdoors/shared/blocks/BlockDimensionalDoorGold.java b/src/main/java/org/dimdev/dimdoors/shared/blocks/BlockDimensionalDoorGold.java
index a6c75577..1df15342 100644
--- a/src/main/java/org/dimdev/dimdoors/shared/blocks/BlockDimensionalDoorGold.java
+++ b/src/main/java/org/dimdev/dimdoors/shared/blocks/BlockDimensionalDoorGold.java
@@ -3,10 +3,8 @@ package org.dimdev.dimdoors.shared.blocks;
 import net.minecraft.block.state.IBlockState;
 import org.dimdev.dimdoors.DimDoors;
 import org.dimdev.dimdoors.shared.items.ModItems;
-import org.dimdev.dimdoors.shared.rifts.AvailableLink;
-import org.dimdev.dimdoors.shared.rifts.WeightedRiftDestination;
+import org.dimdev.dimdoors.shared.rifts.registry.LinkProperties;
 import org.dimdev.dimdoors.shared.rifts.destinations.AvailableLinkDestination;
-import org.dimdev.dimdoors.shared.rifts.destinations.NewPublicDestination;
 import org.dimdev.dimdoors.shared.tileentities.TileEntityEntranceRift;
 import net.minecraft.block.material.Material;
 import net.minecraft.item.Item;
@@ -37,21 +35,19 @@ public class BlockDimensionalDoorGold extends BlockDimensionalDoor {
 
     @Override
     public void setupRift(TileEntityEntranceRift rift) {
-        AvailableLink link = AvailableLink.builder()
+        rift.setProperties(LinkProperties.builder()
                 .groups(new HashSet<>(Arrays.asList(0, 1)))
                 .linksRemaining(1)
-                .replaceDestination(UUID.randomUUID()).build();
-        rift.addAvailableLink(link);
-        AvailableLinkDestination destination = AvailableLinkDestination.builder()
+                .replaceDestination(UUID.randomUUID()).build());
+        rift.setDestination(AvailableLinkDestination.builder()
                 .acceptedGroups(Collections.singleton(0))
                 .coordFactor(1)
                 .negativeDepthFactor(10000)
                 .positiveDepthFactor(80)
                 .weightMaximum(100)
-                .linkId(link.id)
                 .noLink(false)
-                .newRiftWeight(1).build();
-        rift.addWeightedDestination(new WeightedRiftDestination(destination, 1, 0, null, link.replaceDestination));
+                .noLinkBack(false)
+                .newRiftWeight(1).build());
     }
 
     @Override
diff --git a/src/main/java/org/dimdev/dimdoors/shared/blocks/BlockDimensionalDoorIron.java b/src/main/java/org/dimdev/dimdoors/shared/blocks/BlockDimensionalDoorIron.java
index b8b027cd..547506ef 100644
--- a/src/main/java/org/dimdev/dimdoors/shared/blocks/BlockDimensionalDoorIron.java
+++ b/src/main/java/org/dimdev/dimdoors/shared/blocks/BlockDimensionalDoorIron.java
@@ -4,7 +4,7 @@ import net.minecraft.block.state.IBlockState;
 import net.minecraft.init.Blocks;
 import org.dimdev.dimdoors.DimDoors;
 import org.dimdev.dimdoors.shared.items.ModItems;
-import org.dimdev.dimdoors.shared.rifts.destinations.NewPublicDestination;
+import org.dimdev.dimdoors.shared.rifts.destinations.PublicPocketDestination;
 import org.dimdev.dimdoors.shared.tileentities.TileEntityEntranceRift;
 import net.minecraft.block.material.Material;
 import net.minecraft.item.Item;
@@ -36,8 +36,8 @@ public class BlockDimensionalDoorIron extends BlockDimensionalDoor {
 
     @Override
     public void setupRift(TileEntityEntranceRift rift) {
-        NewPublicDestination destination = NewPublicDestination.builder().build();
-        rift.setSingleDestination(destination);
+        PublicPocketDestination destination = PublicPocketDestination.builder().build();
+        rift.setDestination(destination);
     }
 
     @Override
diff --git a/src/main/java/org/dimdev/dimdoors/shared/blocks/BlockDimensionalDoorPersonal.java b/src/main/java/org/dimdev/dimdoors/shared/blocks/BlockDimensionalDoorPersonal.java
index c65bcc19..1022ae00 100644
--- a/src/main/java/org/dimdev/dimdoors/shared/blocks/BlockDimensionalDoorPersonal.java
+++ b/src/main/java/org/dimdev/dimdoors/shared/blocks/BlockDimensionalDoorPersonal.java
@@ -37,11 +37,10 @@ public class BlockDimensionalDoorPersonal extends BlockDimensionalDoor {
     @Override
     public void setupRift(TileEntityEntranceRift rift) {
         if (rift.getWorld().provider instanceof WorldProviderPersonalPocket) {
-            rift.setSingleDestination(new PrivatePocketExitDestination()); // exit
+            rift.setDestination(new PrivatePocketExitDestination()); // exit
         } else {
-            rift.setSingleDestination(new PrivateDestination()); // entrances
+            rift.setDestination(new PrivateDestination()); // entrances
         }
-        rift.setChaosWeight(0); // TODO: generated schematic exits too
     }
 
     @Override
diff --git a/src/main/java/org/dimdev/dimdoors/shared/blocks/BlockDimensionalTrapdoorWood.java b/src/main/java/org/dimdev/dimdoors/shared/blocks/BlockDimensionalTrapdoorWood.java
index 5b1e5d42..d704b2ea 100644
--- a/src/main/java/org/dimdev/dimdoors/shared/blocks/BlockDimensionalTrapdoorWood.java
+++ b/src/main/java/org/dimdev/dimdoors/shared/blocks/BlockDimensionalTrapdoorWood.java
@@ -32,7 +32,7 @@ public class BlockDimensionalTrapdoorWood extends BlockDimensionalTrapdoor {
 
     @Override
     public void setupRift(TileEntityEntranceRift rift) {
-        rift.setSingleDestination(new EscapeDestination());
+        rift.setDestination(new EscapeDestination());
     }
 
     @Override public boolean canBePlacedOnRift() {
diff --git a/src/main/java/org/dimdev/dimdoors/shared/blocks/IRiftProvider.java b/src/main/java/org/dimdev/dimdoors/shared/blocks/IRiftProvider.java
index cf596d58..21002db9 100644
--- a/src/main/java/org/dimdev/dimdoors/shared/blocks/IRiftProvider.java
+++ b/src/main/java/org/dimdev/dimdoors/shared/blocks/IRiftProvider.java
@@ -25,9 +25,6 @@ public interface IRiftProvider<T extends TileEntityRift> extends ITileEntityProv
         if (world.isRemote) return;
         T rift = getRift(world, pos, state);
 
-        // Set the rift's virtual position
-        rift.setVirtualLocation(VirtualLocation.fromLocation(new Location(world, pos)));
-
         // Configure the rift to its default functionality
         setupRift(rift);
 
diff --git a/src/main/java/org/dimdev/dimdoors/shared/commands/CommandDimTeleport.java b/src/main/java/org/dimdev/dimdoors/shared/commands/CommandDimTeleport.java
index d29bad84..66c7a4f7 100644
--- a/src/main/java/org/dimdev/dimdoors/shared/commands/CommandDimTeleport.java
+++ b/src/main/java/org/dimdev/dimdoors/shared/commands/CommandDimTeleport.java
@@ -19,7 +19,7 @@ import net.minecraft.util.math.BlockPos;
 import net.minecraft.util.text.TextComponentString;
 import net.minecraftforge.common.DimensionManager;
 
-public class CommandDimTeleport extends CommandBase { // TODO: localization
+public class CommandDimTeleport extends CommandBase { // TODO: localization, CommandException
 
     private final List<String> aliases;
 
@@ -47,7 +47,7 @@ public class CommandDimTeleport extends CommandBase { // TODO: localization
 
     @Override
     public void execute(MinecraftServer server, ICommandSender sender, String[] args) throws CommandException {
-        // Check correct number of arguments
+        // Check that the number of arguments is correct
         if (args.length < 4 || args.length > 6) {
             sender.sendMessage(new TextComponentString("[DimDoors] Usage: /" + getUsage(sender)));
             return;
diff --git a/src/main/java/org/dimdev/dimdoors/shared/commands/CommandPocket.java b/src/main/java/org/dimdev/dimdoors/shared/commands/CommandPocket.java
index 48d071a8..054d0ede 100644
--- a/src/main/java/org/dimdev/dimdoors/shared/commands/CommandPocket.java
+++ b/src/main/java/org/dimdev/dimdoors/shared/commands/CommandPocket.java
@@ -1,8 +1,6 @@
 package org.dimdev.dimdoors.shared.commands;
 
-import net.minecraft.util.EnumFacing;
 import org.dimdev.dimdoors.DimDoors;
-import org.dimdev.dimdoors.shared.*;
 import org.dimdev.dimdoors.shared.pockets.*;
 import org.dimdev.dimdoors.shared.rifts.TileEntityRift;
 import org.dimdev.ddutils.Location;
@@ -48,7 +46,7 @@ public class CommandPocket extends CommandBase {
 
     @Override
     public void execute(MinecraftServer server, ICommandSender sender, String[] args) throws CommandException { // TODO: more pocket commands (replace pocket, get ID, teleport to pocket, etc.)
-        // Check correct number of arguments
+        // Check that the number of arguments is correct
         if (args.length < 2 || args.length > 3) {
             sender.sendMessage(new TextComponentString("[DimDoors] Usage: /" + getUsage(sender)));
             return;
diff --git a/src/main/java/org/dimdev/dimdoors/shared/items/ItemDimensionalDoorGold.java b/src/main/java/org/dimdev/dimdoors/shared/items/ItemDimensionalDoorGold.java
index 427928a7..297f3bb2 100644
--- a/src/main/java/org/dimdev/dimdoors/shared/items/ItemDimensionalDoorGold.java
+++ b/src/main/java/org/dimdev/dimdoors/shared/items/ItemDimensionalDoorGold.java
@@ -7,7 +7,6 @@ import org.dimdev.dimdoors.shared.blocks.BlockDimensionalDoorGold;
 import org.dimdev.dimdoors.shared.blocks.ModBlocks;
 import org.dimdev.ddutils.I18nUtils;
 import net.minecraft.client.util.ITooltipFlag;
-import net.minecraft.item.ItemDoor;
 import net.minecraft.item.ItemStack;
 import net.minecraft.util.ResourceLocation;
 import net.minecraft.world.World;
diff --git a/src/main/java/org/dimdev/dimdoors/shared/items/ItemRiftSignature.java b/src/main/java/org/dimdev/dimdoors/shared/items/ItemRiftSignature.java
index 809b1b8a..d9e453e8 100644
--- a/src/main/java/org/dimdev/dimdoors/shared/items/ItemRiftSignature.java
+++ b/src/main/java/org/dimdev/dimdoors/shared/items/ItemRiftSignature.java
@@ -65,7 +65,7 @@ public class ItemRiftSignature extends Item {
                 World sourceWorld = target.getLocation().getWorld();
                 sourceWorld.setBlockState(target.getLocation().getPos(), ModBlocks.RIFT.getDefaultState());
                 TileEntityRift rift1 = (TileEntityRift) target.getLocation().getTileEntity();
-                rift1.setSingleDestination(new GlobalDestination(new Location(world, pos)));
+                rift1.setDestination(new GlobalDestination(new Location(world, pos)));
                 rift1.register();
                 rift1.setRotation(target.getYaw(), 0);
             }
@@ -73,7 +73,7 @@ public class ItemRiftSignature extends Item {
             // Place a rift at the target point
             world.setBlockState(pos, ModBlocks.RIFT.getDefaultState());
             TileEntityRift rift2 = (TileEntityRift) world.getTileEntity(pos);
-            rift2.setSingleDestination(new GlobalDestination(target.getLocation()));
+            rift2.setDestination(new GlobalDestination(target.getLocation()));
             rift2.setRotation(player.rotationYaw, 0);
             rift2.register();
 
diff --git a/src/main/java/org/dimdev/dimdoors/shared/items/ItemStabilizedRiftSignature.java b/src/main/java/org/dimdev/dimdoors/shared/items/ItemStabilizedRiftSignature.java
index e5360a21..91b27b4e 100644
--- a/src/main/java/org/dimdev/dimdoors/shared/items/ItemStabilizedRiftSignature.java
+++ b/src/main/java/org/dimdev/dimdoors/shared/items/ItemStabilizedRiftSignature.java
@@ -72,7 +72,7 @@ public class ItemStabilizedRiftSignature extends Item { // TODO: common supercla
             // Place a rift at the source point
             world.setBlockState(pos, ModBlocks.RIFT.getDefaultState());
             TileEntityRift rift2 = (TileEntityRift) world.getTileEntity(pos);
-            rift2.setSingleDestination(new GlobalDestination(target.getLocation()));
+            rift2.setDestination(new GlobalDestination(target.getLocation()));
             rift2.setRotation(player.rotationYaw, 0);
             rift2.register();
 
diff --git a/src/main/java/org/dimdev/dimdoors/shared/pockets/Pocket.java b/src/main/java/org/dimdev/dimdoors/shared/pockets/Pocket.java
index 410117fc..e92e8c77 100644
--- a/src/main/java/org/dimdev/dimdoors/shared/pockets/Pocket.java
+++ b/src/main/java/org/dimdev/dimdoors/shared/pockets/Pocket.java
@@ -1,34 +1,37 @@
 package org.dimdev.dimdoors.shared.pockets;
 
-import org.dimdev.ddutils.nbt.INBTStorable;
-import org.dimdev.ddutils.nbt.NBTUtils;
-import org.dimdev.annotatednbt.Saved;
-import org.dimdev.annotatednbt.NBTSerializable;
-import org.dimdev.dimdoors.shared.VirtualLocation;
-import org.dimdev.dimdoors.shared.rifts.*;
-import org.dimdev.dimdoors.shared.rifts.destinations.PocketEntranceDestination;
-import org.dimdev.dimdoors.shared.rifts.destinations.PocketExitDestination;
-import org.dimdev.dimdoors.shared.tileentities.TileEntityEntranceRift;
-import org.dimdev.ddutils.Location;
-
-import java.util.*;
-
-import org.dimdev.ddutils.math.MathUtils;
 import lombok.Getter;
 import lombok.Setter;
-import net.minecraft.nbt.*;
+import net.minecraft.nbt.NBTTagCompound;
 import net.minecraft.tileentity.TileEntity;
 import net.minecraft.util.math.BlockPos;
+import org.dimdev.annotatednbt.NBTSerializable;
+import org.dimdev.annotatednbt.Saved;
+import org.dimdev.ddutils.Location;
+import org.dimdev.ddutils.math.MathUtils;
+import org.dimdev.ddutils.nbt.INBTStorable;
+import org.dimdev.ddutils.nbt.NBTUtils;
+import org.dimdev.dimdoors.shared.VirtualLocation;
+import org.dimdev.dimdoors.shared.rifts.RiftDestination;
+import org.dimdev.dimdoors.shared.rifts.TileEntityRift;
+import org.dimdev.dimdoors.shared.rifts.destinations.PocketEntranceDestination;
+import org.dimdev.dimdoors.shared.rifts.destinations.PocketExitDestination;
+import org.dimdev.dimdoors.shared.rifts.registry.LinkProperties;
+import org.dimdev.dimdoors.shared.tileentities.TileEntityEntranceRift;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
 
 @NBTSerializable public class Pocket implements INBTStorable { // TODO: better visibilities
 
-    @Saved @Getter /*package-private*/ int id;
-    @Saved @Getter /*package-private*/ int x; // Grid x TODO: rename to gridX and gridY
-    @Saved @Getter /*package-private*/ int z; // Grid y
-    @Saved @Getter @Setter /*package-private*/ int size; // In chunks TODO: non chunk-based size, better bounds such as minX, minZ, maxX, maxZ, etc.
-    @Saved @Getter @Setter /*package-private*/ VirtualLocation virtualLocation; // The non-pocket dimension from which this dungeon was created
-    @Saved @Getter @Setter /*package-private*/ Location entrance;
-    @Saved @Getter /*package-private*/ List<Location> riftLocations;
+    @Saved @Getter protected int id;
+    @Saved @Getter protected int x; // Grid x TODO: rename to gridX and gridY, or just convert to non-grid dependant coordinates
+    @Saved @Getter protected int z; // Grid y
+    @Saved @Getter @Setter protected int size; // In chunks TODO: non chunk-based size, better bounds such as minX, minZ, maxX, maxZ, etc.
+    @Saved @Getter @Setter protected VirtualLocation virtualLocation;
+    @Saved @Getter @Setter protected Location entrance; // TODO: move this to the rift registry (pocketlib)
+    @Saved @Getter protected List<Location> riftLocations; // TODO: convert to a list of all tile entities (for chests, and to make it independant of pocketlib)
 
     @Getter int dim; // Not saved
 
@@ -44,11 +47,12 @@ import net.minecraft.util.math.BlockPos;
 
     // TODO: make these static?
     @Override public void readFromNBT(NBTTagCompound nbt) { NBTUtils.readFromNBT(this, nbt); }
+
     @Override public NBTTagCompound writeToNBT(NBTTagCompound nbt) { return NBTUtils.writeToNBT(this, nbt); }
 
     boolean isInBounds(BlockPos pos) {
         // pocket bounds
-        int gridSize = PocketRegistry.getForDim(dim).getGridSize();
+        int gridSize = PocketRegistry.instance(dim).getGridSize();
         int minX = x * gridSize;
         int minZ = z * gridSize;
         int maxX = minX + (size + 1) * 16;
@@ -72,89 +76,57 @@ import net.minecraft.util.math.BlockPos;
     public void setup() { // Always call after creating a pocket except when building the pocket
         List<TileEntityRift> rifts = getRifts();
 
-        HashMap<Integer, Float> entranceIndexWeights = new HashMap<>();
+        HashMap<TileEntityRift, Float> entranceIndexWeights = new HashMap<>();
 
-        int index = 0;
         for (TileEntityRift rift : rifts) { // Find an entrance
-            for (WeightedRiftDestination weightedPocketEntranceDest : rift.getDestinations()) {
-                if (weightedPocketEntranceDest.getDestination() instanceof PocketEntranceDestination) {
-                    entranceIndexWeights.put(index, weightedPocketEntranceDest.getWeight());
-                    rift.markDirty();
-                    index++;
-                }
+            if (rift.getDestination() instanceof PocketEntranceDestination) {
+                entranceIndexWeights.put(rift, ((PocketEntranceDestination) rift.getDestination()).getWeight());
+                rift.markDirty();
             }
         }
+
         if (entranceIndexWeights.size() == 0) return;
-        int selectedEntranceIndex = MathUtils.weightedRandom(entranceIndexWeights);
+        TileEntityRift selectedEntrance = MathUtils.weightedRandom(entranceIndexWeights);
 
         // Replace entrances with appropriate destinations
-        index = 0;
         for (TileEntityRift rift : rifts) {
-            ListIterator<WeightedRiftDestination> destIterator = rift.getDestinations().listIterator();
-            while (destIterator.hasNext()) {
-                WeightedRiftDestination wdest = destIterator.next();
-                RiftDestination dest = wdest.getDestination();
-                if (dest instanceof PocketEntranceDestination) {
-                    destIterator.remove();
-                    if (index == selectedEntranceIndex) {
-                        entrance = new Location(rift.getWorld(), rift.getPos());
-                        PocketRegistry.getForDim(dim).markDirty();
-                        List<WeightedRiftDestination> ifDestinations = ((PocketEntranceDestination) dest).getIfDestinations();
-                        for (WeightedRiftDestination ifDestination : ifDestinations) {
-                            destIterator.add(new WeightedRiftDestination(ifDestination.getDestination(), ifDestination.getWeight() / wdest.getWeight(), ifDestination.getGroup()));
-                            destIterator.previous(); // An entrance destination shouldn't be in an if/otherwise destination, but just in case, pass over it too
-                        }
-                    } else {
-                        List<WeightedRiftDestination> otherwiseDestinations = ((PocketEntranceDestination) dest).getOtherwiseDestinations();
-                        for (WeightedRiftDestination otherwiseDestination : otherwiseDestinations) {
-                            destIterator.add(new WeightedRiftDestination(otherwiseDestination.getDestination(), otherwiseDestination.getWeight() / wdest.getWeight(), otherwiseDestination.getGroup()));
-                            destIterator.previous(); // An entrance destination shouldn't be in an if/otherwise destination, but just in case, pass over it too
-                        }
-                    }
-                    index++;
+            RiftDestination dest = rift.getDestination();
+            if (dest instanceof PocketEntranceDestination) {
+                if (rift == selectedEntrance) {
+                    entrance = new Location(rift.getWorld(), rift.getPos());
+                    PocketRegistry.instance(dim).markDirty();
+                    rift.setDestination(((PocketEntranceDestination) dest).getIfDestination());
+                } else {
+                    rift.setDestination(((PocketEntranceDestination) dest).getOtherwiseDestination());
                 }
             }
         }
 
-        // set virtual locations and register rifts
+        // register the rifts
         for (TileEntityRift rift : rifts) {
-            rift.setVirtualLocation(virtualLocation);
             rift.register();
         }
     }
 
-    public void linkPocketTo(RiftDestination linkTo, RiftDestination oldDest, AvailableLink availableLink) {
+    public void linkPocketTo(RiftDestination linkTo, LinkProperties linkProperties) {
         List<TileEntityRift> rifts = getRifts();
 
         // Link pocket exits back
         for (TileEntityRift rift : rifts) {
-            ListIterator<WeightedRiftDestination> destIterator = rift.getDestinations().listIterator();
-            while (destIterator.hasNext()) {
-                WeightedRiftDestination wdest = destIterator.next();
-                RiftDestination dest = wdest.getDestination();
-                if (dest instanceof PocketExitDestination) {
-                    destIterator.remove();
-                    if (rift.isRegistered()) dest.unregister(rift);
-                    if (availableLink != null) rift.addAvailableLink(availableLink.toBuilder().build());
-                    if (linkTo != null) destIterator.add(new WeightedRiftDestination(linkTo, wdest.getWeight(), wdest.getGroup(), oldDest));
-                    if (rift.isRegistered()) linkTo.register(rift);
-                    if (rift instanceof TileEntityEntranceRift && !rift.isAlwaysDelete()) {
-                        ((TileEntityEntranceRift) rift).setPlaceRiftOnBreak(true); // We modified the door's state
-                    }
-                    rift.markDirty();
+            RiftDestination dest = rift.getDestination();
+            if (dest instanceof PocketExitDestination) {
+                if (linkProperties != null) rift.setProperties(linkProperties);
+                rift.setDestination(linkTo);
+                if (rift instanceof TileEntityEntranceRift && !rift.isAlwaysDelete()) {
+                    ((TileEntityEntranceRift) rift).setPlaceRiftOnBreak(true); // We modified the door's state
                 }
+                rift.markDirty();
             }
         }
     }
 
-    public void unlinkPocket() {
-        // TODO
-    }
-
     public BlockPos getOrigin() {
-        int gridSize = PocketRegistry.getForDim(dim).getGridSize();
+        int gridSize = PocketRegistry.instance(dim).getGridSize();
         return new BlockPos(x * gridSize * 16, 0, z * gridSize * 16); // TODO: configurable yBase?
     }
-
-    // TODO: method to erase a pocket
 }
diff --git a/src/main/java/org/dimdev/dimdoors/shared/pockets/PocketGenerator.java b/src/main/java/org/dimdev/dimdoors/shared/pockets/PocketGenerator.java
index b4b20981..2364a869 100644
--- a/src/main/java/org/dimdev/dimdoors/shared/pockets/PocketGenerator.java
+++ b/src/main/java/org/dimdev/dimdoors/shared/pockets/PocketGenerator.java
@@ -11,9 +11,9 @@ public final class PocketGenerator {
     public static Pocket generatePocketFromTemplate(int dim, PocketTemplate pocketTemplate, VirtualLocation virtualLocation) {
         DimDoors.log.info("Generating pocket from template " + pocketTemplate.getName() + " at virtual location " + virtualLocation);
 
-        PocketRegistry registry = PocketRegistry.getForDim(dim);
+        PocketRegistry registry = PocketRegistry.instance(dim);
         Pocket pocket = registry.newPocket();
-        pocketTemplate.place(pocket, 0); // TODO: config option for yBase
+        pocketTemplate.place(pocket);
         pocket.setVirtualLocation(virtualLocation);
         return pocket;
     }
diff --git a/src/main/java/org/dimdev/dimdoors/shared/pockets/PocketRegistry.java b/src/main/java/org/dimdev/dimdoors/shared/pockets/PocketRegistry.java
index d40de5bd..62261cdc 100644
--- a/src/main/java/org/dimdev/dimdoors/shared/pockets/PocketRegistry.java
+++ b/src/main/java/org/dimdev/dimdoors/shared/pockets/PocketRegistry.java
@@ -13,6 +13,7 @@ import org.dimdev.dimdoors.shared.world.ModDimensions;
 
 import java.util.HashMap;
 import java.util.Map;
+import java.util.UUID;
 
 import lombok.Getter;
 import net.minecraft.entity.player.EntityPlayerMP;
@@ -21,7 +22,8 @@ import net.minecraft.util.math.BlockPos;
 import net.minecraft.world.storage.MapStorage;
 import net.minecraft.world.storage.WorldSavedData;
 
-@NBTSerializable public class PocketRegistry extends WorldSavedData { // TODO: unregister pocket entrances, private pocket entrances/exits
+@NBTSerializable public class
+PocketRegistry extends WorldSavedData { // TODO: unregister pocket entrances, private pocket entrances/exits
 
     private static final String DATA_NAME = DimDoors.MODID + "_pockets";
     @Getter private static final int DATA_VERSION = 0; // IMPORTANT: Update this and upgradeRegistry when making changes.
@@ -30,7 +32,7 @@ import net.minecraft.world.storage.WorldSavedData;
     @Saved @Getter /*package-private*/ int maxPocketSize;
     @Saved @Getter /*package-private*/ int privatePocketSize;
     @Saved @Getter /*package-private*/ int publicPocketSize;
-    @Saved /*package-private*/ BiMap<String, Integer> privatePocketMap; // Player UUID -> Pocket ID, in pocket dim only
+    @Saved /*package-private*/ BiMap<UUID, Integer> privatePocketMap; // Player UUID -> Pocket ID, in pocket dim only TODO: move this out of pocketlib
     @Saved @Getter /*package-private*/ Map<Integer, Pocket> pockets; // TODO: remove getter?
     @Saved @Getter /*package-private*/ int nextID;
 
@@ -44,7 +46,7 @@ import net.minecraft.world.storage.WorldSavedData;
         super(s);
     }
 
-    public static PocketRegistry getForDim(int dim) {
+    public static PocketRegistry instance(int dim) {
         if (!ModDimensions.isDimDoorsPocketDimension(dim)) throw new UnsupportedOperationException("PocketRegistry is only available for pocket dimensions!");
 
         MapStorage storage = WorldUtils.getWorld(dim).getPerWorldStorage();
@@ -155,17 +157,17 @@ import net.minecraft.world.storage.WorldSavedData;
     }
 
     // TODO: these should be per-map rather than per-world
-    public int getPrivatePocketID(String playerUUID) {
+    public int getPrivatePocketID(UUID playerUUID) {
         Integer id = privatePocketMap.get(playerUUID);
         if (id == null) return -1;
         return id;
     }
 
-    public String getPrivatePocketOwner(int id) {
+    public UUID getPrivatePocketOwner(int id) {
         return privatePocketMap.inverse().get(id);
     }
 
-    public void setPrivatePocketID(String playerUUID, int id) {
+    public void setPrivatePocketID(UUID playerUUID, int id) {
         privatePocketMap.put(playerUUID, id);
         markDirty();
     }
diff --git a/src/main/java/org/dimdev/dimdoors/shared/pockets/PocketTemplate.java b/src/main/java/org/dimdev/dimdoors/shared/pockets/PocketTemplate.java
index 7e4d2937..a51d243a 100644
--- a/src/main/java/org/dimdev/dimdoors/shared/pockets/PocketTemplate.java
+++ b/src/main/java/org/dimdev/dimdoors/shared/pockets/PocketTemplate.java
@@ -17,7 +17,6 @@ import net.minecraft.util.math.BlockPos;
 import net.minecraft.world.WorldServer;
 
 /**
- *
  * @author Robijnvogel
  */
 @AllArgsConstructor @RequiredArgsConstructor// TODO: use @Builder?
@@ -33,22 +32,23 @@ public class PocketTemplate {
 
     public float getWeight(int depth) {
         if (depth < 0) return 100; // TODO: get rid of this later
-        if (maxDepth - minDepth + 1 != weights.length) throw new IllegalStateException("This PocetTemplate wasn't set up correctly!");
+        if (maxDepth - minDepth + 1 != weights.length) throw new IllegalStateException("This PocketTemplate wasn't set up correctly!");
         if (depth < minDepth) return 0;
         if (depth > maxDepth) return weights[weights.length - 1];
         return weights[depth - minDepth];
     }
 
-    public void place(Pocket pocket, int yBase) {
+    public void place(Pocket pocket) {
         pocket.setSize(size);
-        int gridSize = PocketRegistry.getForDim(pocket.dim).getGridSize();
+        int gridSize = PocketRegistry.instance(pocket.dim).getGridSize();
         int dim = pocket.dim;
         int xBase = pocket.getX() * gridSize * 16;
+        int yBase = 0;
         int zBase = pocket.getZ() * gridSize * 16;
         DimDoors.log.info("Placing new pocket using schematic " + schematic.schematicName + " at x = " + xBase + ", z = " + zBase);
 
         WorldServer world = WorldUtils.getWorld(dim);
-        Schematic.place(schematic, world, xBase, yBase, zBase);
+        Schematic.place(schematic, world, xBase, 0, zBase);
 
         // Set pocket riftLocations
         pocket.riftLocations = new ArrayList<>();
diff --git a/src/main/java/org/dimdev/dimdoors/shared/SchematicHandler.java b/src/main/java/org/dimdev/dimdoors/shared/pockets/SchematicHandler.java
similarity index 99%
rename from src/main/java/org/dimdev/dimdoors/shared/SchematicHandler.java
rename to src/main/java/org/dimdev/dimdoors/shared/pockets/SchematicHandler.java
index c991f89f..f3c01c56 100644
--- a/src/main/java/org/dimdev/dimdoors/shared/SchematicHandler.java
+++ b/src/main/java/org/dimdev/dimdoors/shared/pockets/SchematicHandler.java
@@ -1,6 +1,6 @@
-package org.dimdev.dimdoors.shared;
+package org.dimdev.dimdoors.shared.pockets;
 
-import org.dimdev.dimdoors.shared.pockets.PocketTemplate;
+import org.dimdev.dimdoors.shared.Config;
 import org.dimdev.ddutils.math.MathUtils;
 import org.dimdev.ddutils.schem.Schematic;
 import com.google.gson.JsonArray;
diff --git a/src/main/java/org/dimdev/dimdoors/shared/rifts/RiftDestination.java b/src/main/java/org/dimdev/dimdoors/shared/rifts/RiftDestination.java
index 627ced93..c4b69b2a 100644
--- a/src/main/java/org/dimdev/dimdoors/shared/rifts/RiftDestination.java
+++ b/src/main/java/org/dimdev/dimdoors/shared/rifts/RiftDestination.java
@@ -3,23 +3,23 @@ package org.dimdev.dimdoors.shared.rifts;
 import com.google.common.collect.BiMap;
 import com.google.common.collect.HashBiMap;
 import org.dimdev.ddutils.Location;
+import org.dimdev.ddutils.RGBA;
+import org.dimdev.ddutils.RotatedLocation;
 import org.dimdev.ddutils.nbt.INBTStorable;
 import net.minecraft.entity.Entity;
 import net.minecraft.nbt.NBTTagCompound;
 import lombok.*;  // Don't change import order! (Gradle bug): https://stackoverflow.com/questions/26557133/
+import org.dimdev.dimdoors.shared.rifts.registry.RiftRegistry;
 
 import java.lang.reflect.InvocationTargetException;
+import java.util.Set;
 
-@Getter @EqualsAndHashCode @ToString
+@EqualsAndHashCode @ToString
 public abstract class RiftDestination implements INBTStorable {
 
-    /*private*/ public static final BiMap<String, Class<? extends RiftDestination>> destinationRegistry = HashBiMap.create(); // TODO: move to RiftDestinationRegistry
-    //private String type;
-    protected WeightedRiftDestination weightedDestination;
+    public static final BiMap<String, Class<? extends RiftDestination>> destinationRegistry = HashBiMap.create(); // TODO: move to RiftDestinationRegistry
 
-    public RiftDestination() {
-      //type = destinationRegistry.inverse().get(getClass());
-    }
+    public RiftDestination() {}
 
     public static RiftDestination readDestinationNBT(NBTTagCompound nbt) {
         String type = nbt.getString("type");
@@ -28,7 +28,6 @@ public abstract class RiftDestination implements INBTStorable {
         try {
             RiftDestination destination = destinationClass.getConstructor().newInstance();
             destination.readFromNBT(nbt);
-            //destination.type = type;
             return destination;
         } catch (NoSuchMethodException | IllegalAccessException e) {
             throw new RuntimeException("The class registered for type " + type + " must have a public no-args constructor.", e);
@@ -38,7 +37,7 @@ public abstract class RiftDestination implements INBTStorable {
     }
 
     @Override
-    public void readFromNBT(NBTTagCompound nbt) { }
+    public void readFromNBT(NBTTagCompound nbt) {}
 
     @Override
     public NBTTagCompound writeToNBT(NBTTagCompound nbt) {
@@ -48,19 +47,30 @@ public abstract class RiftDestination implements INBTStorable {
         return nbt;
     }
 
-    public Location getReferencedRift(Location rift) { // TODO: change to getReferencedRifts
+    public abstract boolean teleport(RotatedLocation rift, Entity entity);
+
+    public Location getFixedTarget(Location location) {
         return null;
     }
 
-    public void register(TileEntityRift rift) {
-        Location loc = getReferencedRift(rift.getLocation());
-        if (loc != null) RiftRegistry.addLink(rift.getLocation(), loc);
+    public void register(Location location) {
+        RiftRegistry.instance().addLink(location, getFixedTarget(location));
     }
 
-    public void unregister(TileEntityRift rift) {
-        Location loc = getReferencedRift(rift.getLocation());
-        if (loc != null) RiftRegistry.removeLink(rift.getLocation(), loc);
+    public void unregister(Location location) {
+        RiftRegistry.instance().removeLink(location, getFixedTarget(location));
     }
 
-    public abstract boolean teleport(TileEntityRift rift, Entity entity);
+    public boolean keepAfterTargetGone(Location location) {
+        return true;
+    }
+
+    public RGBA getColor(Location location) {
+        Location target = getFixedTarget(location);
+        if (target != null && RiftRegistry.instance().isRiftAt(target)) {
+            Set<Location> otherRiftTargets = RiftRegistry.instance().getTargets(target);
+            if (otherRiftTargets.size() == 1 && otherRiftTargets.contains(location)) return new RGBA(0, 1, 0, 1);
+        }
+        return new RGBA(1, 0, 0, 1);
+    }
 }
diff --git a/src/main/java/org/dimdev/dimdoors/shared/rifts/RiftRegistry.java b/src/main/java/org/dimdev/dimdoors/shared/rifts/RiftRegistry.java
deleted file mode 100644
index 172e483c..00000000
--- a/src/main/java/org/dimdev/dimdoors/shared/rifts/RiftRegistry.java
+++ /dev/null
@@ -1,292 +0,0 @@
-package org.dimdev.dimdoors.shared.rifts;
-
-import com.google.common.collect.ConcurrentHashMultiset;
-import com.google.common.collect.Multiset;
-import lombok.*;
-import lombok.experimental.Wither;
-import net.minecraft.nbt.NBTTagCompound;
-import net.minecraft.world.World;
-import net.minecraft.world.storage.MapStorage;
-import net.minecraft.world.storage.WorldSavedData;
-import net.minecraftforge.common.DimensionManager;
-import org.dimdev.ddutils.Location;
-import org.dimdev.ddutils.WorldUtils;
-import org.dimdev.ddutils.nbt.INBTStorable;
-import org.dimdev.ddutils.nbt.NBTUtils;
-import org.dimdev.annotatednbt.Saved;
-import org.dimdev.annotatednbt.NBTSerializable;
-import org.dimdev.dimdoors.DimDoors;
-import org.dimdev.dimdoors.shared.VirtualLocation;
-import org.dimdev.dimdoors.shared.world.ModDimensions;
-
-import java.util.*;
-
-@NBTSerializable public class RiftRegistry extends WorldSavedData {
-
-    private static final String DATA_NAME = DimDoors.MODID + "_rifts";
-    @Getter private static final int DATA_VERSION = 0; // IMPORTANT: Update this and upgradeRegistry when making changes.
-
-    @Saved @Getter protected /*final*/ Map<Location, RiftInfo> rifts = new HashMap<>(); // TODO: convert to a static directed graph, but store links per-world
-
-    @Saved @Getter protected /*final*/ Map<String, Location> privatePocketEntrances = new HashMap<>(); // Player UUID -> last rift used to exit pocket TODO: split into PrivatePocketRiftRegistry subclass
-    @Saved @Getter protected /*final*/ Map<String, List<Location>> privatePocketEntranceLists = new HashMap<>(); // Player UUID -> private pocket entrances TODO: split into PrivatePocketRiftRegistry subclass
-    @Saved @Getter protected /*final*/ Map<String, Location> privatePocketExits = new HashMap<>(); // Player UUID -> last rift used to enter pocket
-    @Saved @Getter protected /*final*/ Map<String, Location> overworldRifts = new HashMap<>();
-
-    @Getter private int dim;
-    private World world;
-
-    @AllArgsConstructor @EqualsAndHashCode @Builder(toBuilder = true)
-    @NBTSerializable public static class RiftInfo implements INBTStorable {
-        // IntelliJ warnings are wrong, Builder needs these initializers!
-        @Saved @Getter public VirtualLocation virtualLocation;
-        @Saved @Getter @Wither public Location location;
-        @Saved @Getter public boolean isEntrance;
-        @Builder.Default @Getter public Set<AvailableLink> availableLinks = new HashSet<>();
-        @Builder.Default @Getter public Multiset<Location> sources = ConcurrentHashMultiset.create();
-        @Builder.Default @Getter public Multiset<Location> destinations = ConcurrentHashMultiset.create();
-
-        public RiftInfo() {
-            availableLinks = new HashSet<>();
-            sources = ConcurrentHashMultiset.create();
-            destinations = ConcurrentHashMultiset.create();
-        }
-
-        @Override public void readFromNBT(NBTTagCompound nbt) { NBTUtils.readFromNBT(this, nbt); }
-        @Override public NBTTagCompound writeToNBT(NBTTagCompound nbt) { return NBTUtils.writeToNBT(this, nbt); }
-    }
-
-    public RiftRegistry() {
-        super(DATA_NAME);
-    }
-
-    public RiftRegistry(String s) {
-        super(s);
-    }
-
-    public static RiftRegistry getForDim(int dim) {
-        MapStorage storage = WorldUtils.getWorld(dim).getPerWorldStorage();
-        RiftRegistry instance = (RiftRegistry) storage.getOrLoadData(RiftRegistry.class, DATA_NAME);
-
-        if (instance == null) {
-            instance = new RiftRegistry();
-            instance.initNewRegistry();
-            storage.setData(DATA_NAME, instance);
-        }
-
-        instance.world = WorldUtils.getWorld(dim);
-        instance.dim = dim;
-        return instance;
-    }
-
-    public void initNewRegistry() {
-        // Nothing to do
-    }
-
-    @Override
-    public void readFromNBT(NBTTagCompound nbt) {
-        Integer version = nbt.getInteger("version");
-        if (version == null || version != DATA_VERSION) {
-            if (upgradeRegistry(nbt, version == null ? -1 : version)) {
-                markDirty();
-            } else {
-                DimDoors.log.warn("Failed to upgrade the pocket registry, you'll have to recreate your world!");
-                throw new RuntimeException("Couldn't upgrade registry"); // TODO: better exceptions
-            }
-        }
-
-        NBTUtils.readFromNBT(this, nbt);
-    }
-
-    private static boolean upgradeRegistry(@SuppressWarnings("unused") NBTTagCompound nbt, int oldVersion) {
-        if (oldVersion > DATA_VERSION) throw new RuntimeException("Upgrade the mod!"); // TODO: better exceptions
-        switch (oldVersion) {
-            case -1: // No version tag
-                return false;
-            case 0:
-                // Upgrade to 1 or return false
-            case 1:
-                // Upgrade to 2 or return false
-            case 2:
-                // Upgrade to 3 or return false
-                // ...
-        }
-        return true;
-    }
-
-    @Override
-    public NBTTagCompound writeToNBT(NBTTagCompound nbt) {
-        nbt.setInteger("version", DATA_VERSION);
-        return NBTUtils.writeToNBT(this, nbt);
-    }
-
-    public static RiftInfo getRiftInfo(Location rift) {
-        return getRegistry(rift).rifts.get(rift);
-    }
-
-    public static void addRift(Location rift) {
-        DimDoors.log.info("Rift added at " + rift);
-        RiftRegistry registry = getRegistry(rift);
-        registry.rifts.computeIfAbsent(rift, k -> new RiftInfo());
-        registry.markDirty();
-    }
-
-    public static void removeRift(Location rift) {
-        DimDoors.log.info("Rift removed at " + rift);
-        RiftRegistry registry = getRegistry(rift);
-        RiftInfo oldRift = registry.rifts.remove(rift);
-        if (oldRift == null) return;
-        List<TileEntityRift> updateQueue = new ArrayList<>();
-        for (Location source : oldRift.sources) {
-            RiftRegistry sourceRegistry = getRegistry(source);
-            sourceRegistry.rifts.get(source).destinations.remove(rift);
-            sourceRegistry.markDirty();
-            TileEntityRift riftEntity = (TileEntityRift) sourceRegistry.world.getTileEntity(source.getPos());
-            riftEntity.destinationGone(rift);
-            updateQueue.add(riftEntity);
-        }
-        for (Location destination : oldRift.destinations) {
-            RiftRegistry destinationRegistry = getRegistry(destination);
-            destinationRegistry.rifts.get(destination).sources.remove(rift);
-            destinationRegistry.markDirty();
-            TileEntityRift riftEntity = (TileEntityRift) destinationRegistry.world.getTileEntity(destination.getPos());
-            updateQueue.add(riftEntity);
-            //riftEntity.allSourcesGone(); // TODO
-        }
-        for (TileEntityRift riftEntity : updateQueue) {
-            //riftEntity.updateColor();
-            riftEntity.markDirty();
-        }
-        getForDim(ModDimensions.getPrivateDim()).privatePocketEntrances.entrySet().removeIf(e -> e.getValue().equals(rift));
-        getForDim(0).overworldRifts.entrySet().removeIf(e -> e.getValue().equals(rift));
-        registry.markDirty();
-    }
-
-    public static void addLink(Location from, Location to) {
-        DimDoors.log.info("Link added " + from + " -> " + to);
-        RiftRegistry registryFrom = getRegistry(from);
-        RiftRegistry registryTo = getRegistry(to);
-        RiftInfo riftInfoFrom = registryFrom.rifts.computeIfAbsent(from, k -> new RiftInfo());
-        RiftInfo riftInfoTo = registryTo.rifts.computeIfAbsent(to, k -> new RiftInfo());
-        riftInfoFrom.destinations.add(to);
-        registryFrom.markDirty();
-        riftInfoTo.sources.add(from);
-        registryTo.markDirty();
-        if (to.getTileEntity() instanceof TileEntityRift) ((TileEntityRift) to.getTileEntity()).updateColor();
-        if (from.getTileEntity() instanceof TileEntityRift) ((TileEntityRift) from.getTileEntity()).updateColor();
-    }
-
-    public static void removeLink(Location from, Location to) {
-        DimDoors.log.info("Link removed " + from + " -> " + to);
-        RiftRegistry registryFrom = getRegistry(from);
-        RiftRegistry registryTo = getRegistry(to);
-        registryFrom.rifts.get(from).destinations.remove(to);
-        registryTo.rifts.get(to).sources.remove(from);
-        registryFrom.markDirty();
-        registryTo.markDirty();
-        if (to.getTileEntity() instanceof TileEntityRift) ((TileEntityRift) to.getTileEntity()).updateColor();
-        if (from.getTileEntity() instanceof TileEntityRift) ((TileEntityRift) from.getTileEntity()).updateColor();
-    }
-
-    public static void addAvailableLink(Location rift, AvailableLink link) { // TODO cache rifts with availableLinks
-        DimDoors.log.info("AvailableLink added at " + rift);
-        RiftRegistry registry = getRegistry(rift);
-        registry.rifts.get(rift).availableLinks.add(link);
-        registry.markDirty();
-    }
-
-    public static void removeAvailableLink(Location rift, AvailableLink link) {
-        DimDoors.log.info("AvailableLink removed at " + rift);
-        RiftRegistry registry = getRegistry(rift);
-        registry.rifts.get(rift).availableLinks.remove(link);
-        registry.markDirty();
-    }
-
-    public static void clearAvailableLinks(Location rift) {
-        DimDoors.log.info("AvailableLink cleared at " + rift);
-        RiftRegistry registry = getRegistry(rift);
-        registry.rifts.get(rift).availableLinks.clear();
-        registry.markDirty();
-    }
-
-    /*
-    public static void removeAvailableLinkByID(Location rift, int id) {
-        DimDoors.log.info("AvailableLink with id " + id + " removed at " + rift);
-        RiftRegistry registry = getRegistry(rift);
-        for (AvailableLinkInfo link : registry.rifts.get(rift).availableLinks) {
-            if (link.id.equals(id)) {
-                removeAvailableLink(rift, link);
-                return;
-            }
-        }
-    }*/
-
-    public static RiftRegistry getRegistry(Location rift) {
-        return getForDim(rift.getDim());
-    }
-
-    public Location getPrivatePocketEntrance(String playerUUID) {
-        Location entrance = privatePocketEntrances.get(playerUUID);
-        List<Location> entrances = privatePocketEntranceLists.computeIfAbsent(playerUUID, k -> new ArrayList<>());
-        while ((entrance == null || !(entrance.getTileEntity() instanceof TileEntityRift)) && entrances.size() > 0) {
-            if (entrance != null) entrances.remove(entrance);
-            if (entrances.size() > 0) entrance = entrances.get(0);
-        }
-        privatePocketEntrances.put(playerUUID, entrance);
-        return entrance;
-    }
-
-    public void addPrivatePocketEntrance(String playerUUID, Location rift) {
-        DimDoors.log.info("Private pocket entrance added for " + playerUUID + " at " + rift);
-        privatePocketEntranceLists.computeIfAbsent(playerUUID, k -> new ArrayList<>()).add(rift);
-    }
-
-    public void setPrivatePocketEntrance(String playerUUID, Location rift) {
-        DimDoors.log.info("Last private pocket entrance set for " + playerUUID + " at " + rift);
-        privatePocketEntrances.put(playerUUID, rift);
-        markDirty();
-    }
-
-    public Location getPrivatePocketExit(String playerUUID) {
-        return privatePocketExits.get(playerUUID);
-    }
-
-    public void setPrivatePocketExit(String playerUUID, Location rift) {
-        DimDoors.log.info("Last private pocket exit set for " + playerUUID + " at " + rift);
-        if (rift != null) {
-            privatePocketExits.put(playerUUID, rift);
-        } else {
-            privatePocketExits.remove(playerUUID);
-        }
-        markDirty();
-    }
-
-    public static Location getOverworldRift(String playerUUID) { // TODO: since this is per-world, move to different registry?
-        return getForDim(0).overworldRifts.get(playerUUID); // store in overworld, since that's where per-world player data is stored
-    }
-
-    public static void setOverworldRift(String playerUUID, Location rift) {
-        DimDoors.log.info("Overworld rift set for " + playerUUID + " at " + rift);
-        if (rift != null) {
-            getForDim(0).overworldRifts.put(playerUUID, rift);
-        } else {
-            getForDim(0).overworldRifts.remove(playerUUID);
-        }
-        getForDim(0).markDirty();
-    }
-
-    public static List<AvailableLink> getAvailableLinks() { // TODO: cache this
-        List<AvailableLink> availableLinks = new ArrayList<>();
-        for (int dim: DimensionManager.getStaticDimensionIDs()) { // TODO: don't create worlds
-            RiftRegistry registry = getForDim(dim);
-            for (Map.Entry<Location, RiftInfo> rift : registry.rifts.entrySet()) {
-                for (AvailableLink availableLink : rift.getValue().availableLinks) {
-                    availableLinks.add(availableLink.withRift(rift.getKey()));
-                }
-            }
-        }
-        return availableLinks;
-    }
-
-    // TODO: rebuildRifts() function that scans the world and rebuilds the rift regestry
-}
diff --git a/src/main/java/org/dimdev/dimdoors/shared/rifts/TileEntityRift.java b/src/main/java/org/dimdev/dimdoors/shared/rifts/TileEntityRift.java
index 8cc081b0..44ea656d 100644
--- a/src/main/java/org/dimdev/dimdoors/shared/rifts/TileEntityRift.java
+++ b/src/main/java/org/dimdev/dimdoors/shared/rifts/TileEntityRift.java
@@ -1,22 +1,5 @@
 package org.dimdev.dimdoors.shared.rifts;
 
-import org.dimdev.ddutils.nbt.NBTUtils;
-import org.dimdev.annotatednbt.Saved;
-import org.dimdev.annotatednbt.NBTSerializable;
-import org.dimdev.ddutils.RGBA;
-import org.dimdev.dimdoors.DimDoors;
-import org.dimdev.dimdoors.shared.VirtualLocation;
-import org.dimdev.dimdoors.shared.blocks.BlockDimensionalDoor;
-import org.dimdev.dimdoors.shared.blocks.BlockFloatingRift;
-import org.dimdev.dimdoors.shared.pockets.Pocket;
-import org.dimdev.dimdoors.shared.pockets.PocketRegistry;
-import org.dimdev.ddutils.EntityUtils;
-import org.dimdev.ddutils.Location;
-import org.dimdev.ddutils.math.MathUtils;
-import org.dimdev.ddutils.TeleportUtils;
-import org.dimdev.ddutils.WorldUtils;
-import org.dimdev.dimdoors.shared.rifts.destinations.*;
-import org.dimdev.dimdoors.shared.world.ModDimensions;
 import lombok.Getter;
 import net.minecraft.block.state.IBlockState;
 import net.minecraft.entity.Entity;
@@ -30,55 +13,53 @@ import net.minecraft.util.math.BlockPos;
 import net.minecraft.world.World;
 import net.minecraftforge.fml.relauncher.Side;
 import net.minecraftforge.fml.relauncher.SideOnly;
+import org.dimdev.annotatednbt.NBTSerializable;
+import org.dimdev.annotatednbt.Saved;
+import org.dimdev.ddutils.*;
+import org.dimdev.ddutils.nbt.NBTUtils;
+import org.dimdev.dimdoors.DimDoors;
+import org.dimdev.dimdoors.shared.blocks.BlockDimensionalDoor;
+import org.dimdev.dimdoors.shared.blocks.BlockFloatingRift;
+import org.dimdev.dimdoors.shared.rifts.registry.LinkProperties;
+import org.dimdev.dimdoors.shared.rifts.registry.Rift;
+import org.dimdev.dimdoors.shared.rifts.registry.RiftRegistry;
+import org.dimdev.dimdoors.shared.world.ModDimensions;
 
 import javax.annotation.Nonnull;
-import java.util.*;
 
 @NBTSerializable public abstract class TileEntityRift extends TileEntity implements ITickable { // TODO: implement ITeleportSource and ITeleportDestination
 
-    @Saved@Getter protected VirtualLocation virtualLocation;
-    @Saved @Nonnull @Getter protected List<WeightedRiftDestination> destinations; // Not using a set because we can have duplicate destinations. Maybe use Multiset from Guava?
-    @Saved @Getter protected boolean makeDestinationPermanent;
-    @Saved @Getter protected boolean preserveRotation;
+    @Saved @Nonnull @Getter protected RiftDestination destination;
+    @Saved @Getter protected boolean relativeRotation;
     @Saved @Getter protected float yaw;
     @Saved @Getter protected float pitch;
     @Saved @Getter protected boolean alwaysDelete; // Delete the rift when an entrances rift is broken even if the state was changed or destinations link there.
-    @Saved @Getter protected float chaosWeight;
     @Saved @Getter protected boolean forcedColor;
-    @Saved @Getter protected RGBA color = null; // TODO: update AnnotatedNBT to be able to save these
-    @Saved @Getter protected Set<AvailableLink> availableLinks;
-    // TODO: option to convert to door on teleportTo?
+    @Saved @Getter protected RGBA color = null;
+    @Saved @Getter protected LinkProperties properties;
 
     protected boolean riftStateChanged; // not saved
 
     public TileEntityRift() {
-        destinations = new ArrayList<>();
-        makeDestinationPermanent = true;
-        preserveRotation = true;
+        relativeRotation = true;
         pitch = 0;
         alwaysDelete = false;
-        chaosWeight = 1;
-        availableLinks = new HashSet<>();
     }
 
     public void copyFrom(TileEntityRift oldRift) {
-        virtualLocation = oldRift.virtualLocation;
-        destinations = oldRift.destinations;
-        makeDestinationPermanent = oldRift.makeDestinationPermanent;
-        preserveRotation = oldRift.preserveRotation;
+        relativeRotation = oldRift.relativeRotation;
         yaw = oldRift.yaw;
         pitch = oldRift.pitch;
-        chaosWeight = oldRift.chaosWeight;
+        properties = oldRift.properties;
         if (oldRift.isFloating() != isFloating()) updateType();
 
         markDirty();
     }
 
+    // NBT
     @Override public void readFromNBT(NBTTagCompound nbt) { super.readFromNBT(nbt); NBTUtils.readFromNBT(this, nbt); }
-    @Override public NBTTagCompound writeToNBT(NBTTagCompound nbt) {
-        nbt = super.writeToNBT(nbt);
-        return NBTUtils.writeToNBT(this, nbt);
-    }
+
+    @Override public NBTTagCompound writeToNBT(NBTTagCompound nbt) { nbt = super.writeToNBT(nbt); return NBTUtils.writeToNBT(this, nbt); }
 
     @Override
     public NBTTagCompound getUpdateTag() {
@@ -101,6 +82,7 @@ import java.util.*;
         deserializeNBT(pkt.getNbtCompound());
     }
 
+    // Tile entity properties
 
     // Use vanilla behavior of refreshing only when block changes, not state (otherwise, opening the door would destroy the tile entity)
     @Override
@@ -108,71 +90,30 @@ import java.util.*;
         // newState is not accurate if we change the state during onBlockBreak
         newSate = world.getBlockState(pos);
         return oldState.getBlock() != newSate.getBlock() &&
-                !(oldState.getBlock() instanceof BlockDimensionalDoor
-                  && newSate.getBlock() instanceof BlockFloatingRift);
+               !(oldState.getBlock() instanceof BlockDimensionalDoor
+                 && newSate.getBlock() instanceof BlockFloatingRift);
     }
 
     // Modification functions
-    public void setVirtualLocation(VirtualLocation virtualLocation) {
-        this.virtualLocation = virtualLocation;
-        updateType();
-        // TODO: update available link virtual locations
-        markDirty();
-    }
 
     public void setRotation(float yaw, float pitch) {
         this.yaw = yaw;
         this.pitch = pitch;
-        preserveRotation = false;
         markDirty();
     }
 
-    public void clearRotation() {
-        preserveRotation = true;
-    }
-
-    public void addWeightedDestination(WeightedRiftDestination destination) {
-        destinations.add(destination);
+    public void setRelativeRotation(boolean relativeRotation) {
+        this.relativeRotation = relativeRotation;
         markDirty();
     }
 
-    public void addDestination(RiftDestination destination, float weight, int group) {
-        riftStateChanged = true;
-        destinations.add(new WeightedRiftDestination(destination, weight, group));
-        if (isRegistered()) destination.register(this);
-        markDirty();
-    }
-
-    public void addDestination(RiftDestination destination, float weight, int group, RiftDestination oldDestination) {
-        riftStateChanged = true;
-        destinations.add(new WeightedRiftDestination(destination, weight, group, oldDestination));
-        if (isRegistered()) destination.register(this);
-        markDirty();
-    }
-
-    public void removeDestination(WeightedRiftDestination dest) {
-        riftStateChanged = true;
-        destinations.remove(dest);
-        if (isRegistered()) dest.getDestination().unregister(this);
-        markDirty();
-    }
-
-    public void clearDestinations() {
-        if (isRegistered()) for (WeightedRiftDestination wdest : destinations) {
-            wdest.getDestination().unregister(this);
+    public void setDestination(RiftDestination destination) {
+        if (this.destination != null) {
+            this.destination.unregister(new Location(world, pos));
         }
-        destinations.clear();
-        markDirty();
-    }
-
-    public void setSingleDestination(RiftDestination destination) {
-        clearDestinations();
-        addDestination(destination, 1, 0);
-    }
-
-    public void setChaosWeight(float chaosWeight) {
-        this.chaosWeight = chaosWeight;
+        this.destination = destination;
         markDirty();
+        updateColor();
     }
 
     public void setColor(RGBA color) {
@@ -181,15 +122,9 @@ import java.util.*;
         markDirty();
     }
 
-    public void registerAvailableLink(AvailableLink link) {
-        if (!isRegistered()) return;
-        RiftRegistry.addAvailableLink(getLocation(), link);
-    }
-
-    public void addAvailableLink(AvailableLink link) {
-        availableLinks.add(link);
-        link.rift = getLocation();
-        registerAvailableLink(link);
+    public void setProperties(LinkProperties properties) {
+        this.properties = properties;
+        updateProperties();
         markDirty();
     }
 
@@ -198,107 +133,65 @@ import java.util.*;
         markDirty();
     }
 
-    public void makeDestinationPermanent(WeightedRiftDestination weightedDestination, Location destLoc) {
-        riftStateChanged = true;
-        RiftDestination newDest;
-        if (WorldUtils.getDim(world) == destLoc.getDim()) {
-            newDest = new LocalDestination(destLoc.getPos()); // TODO: RelativeDestination instead?
-        } else {
-            newDest = new GlobalDestination(destLoc);
-        }
-        removeDestination(weightedDestination);
-        addDestination(newDest, weightedDestination.getWeight(), weightedDestination.getGroup(), weightedDestination.getDestination());
-        markDirty();
-    }
+    // Registry TODO: merge most of these into one single updateRegistry() method
 
-    // Registry
     public boolean isRegistered() {
-        return world != null && RiftRegistry.getRiftInfo(new Location(world, pos)) != null;
+        return RiftRegistry.instance().isRiftAt(new Location(world, pos));
     }
 
     public void register() {
         if (isRegistered()) return;
         Location loc = new Location(world, pos);
-        RiftRegistry.addRift(loc);
-        RiftRegistry.getRiftInfo(loc).virtualLocation = virtualLocation;
-        for (WeightedRiftDestination weightedDest : destinations) {
-            weightedDest.getDestination().register(this);
-        }
-        for (AvailableLink link : availableLinks) {
-            registerAvailableLink(link);
-        }
+        RiftRegistry.instance().addRift(loc);
+        destination.register(new Location(world, pos));
+        updateProperties();
         updateColor();
     }
 
+    public void updateProperties() {
+        if (isRegistered()) RiftRegistry.instance().setProperties(new Location(world, pos), properties);
+        markDirty();
+    }
+
     public void unregister() {
-        if (!isRegistered()) return;
-        RiftRegistry.removeRift(new Location(world, pos)); // TODO: unregister destinations
-        if (ModDimensions.isDimDoorsPocketDimension(world)) {
-            PocketRegistry pocketRegistry = PocketRegistry.getForDim(WorldUtils.getDim(world));
-            Pocket pocket = pocketRegistry.getPocketAt(pos);
-            if (pocket != null && pocket.getEntrance() != null && pocket.getEntrance().getPos().equals(pos)) {
-                pocket.setEntrance(null);
-                pocketRegistry.markDirty();
-            }
+        if (isRegistered()) {
+            RiftRegistry.instance().removeRift(new Location(world, pos));
         }
-        // TODO: inform pocket that entrances was destroyed (we'll probably need an isPrivate field on the pocket)
     }
 
     public void updateType() {
         if (!isRegistered()) return;
-        RiftRegistry.getRiftInfo(getLocation()).isEntrance = !isFloating();
-        RiftRegistry.getForDim(getLocation().getDim()).markDirty();
+        Rift rift = RiftRegistry.instance().getRift(new Location(world, pos));
+        rift.isFloating = isFloating();
+        rift.markDirty();
     }
 
-    public void destinationGone(Location loc) {
-        ListIterator<WeightedRiftDestination> wdestIterator = destinations.listIterator();
-        while (wdestIterator.hasNext()) {
-            WeightedRiftDestination wdest = wdestIterator.next();
-            RiftDestination dest = wdest.getDestination();
-            if (loc.equals(dest.getReferencedRift(getLocation()))) {
-                wdestIterator.remove(); // TODO: unregister*
-                RiftDestination oldDest = wdest.getOldDestination();
-                if (oldDest != null) {
-                    wdestIterator.add(new WeightedRiftDestination(oldDest, wdest.getWeight(), wdest.getGroup()));
-                    if (isRegistered()) oldDest.register(this);
-                }
-            }
-        }
-        destinations.removeIf(weightedRiftDestination -> loc.equals(weightedRiftDestination.getDestination().getReferencedRift(getLocation())));
-        markDirty();
+    public void targetGone(Location loc) {
+        if (!destination.keepAfterTargetGone(loc)) setDestination(null);
+        updateColor();
+    }
+
+    public void sourceGone(Location loc) {
+        updateColor();
     }
 
     // Teleport logic
     public boolean teleport(Entity entity) {
         riftStateChanged = false;
 
-        // Check that the rift has destinations
-        if (destinations.size() == 0) {
-            DimDoors.chat(entity, "This rift has no destinations!");
+        // Check that the rift has as destination
+        if (destination == null) {
+            DimDoors.chat(entity, "This rift has no destination!");
             return false;
         }
 
-        // Get a random destination based on the weights
-        Map<WeightedRiftDestination, Float> weightMap = new HashMap<>(); // TODO: cache this, faster implementation of single rift
-        for (WeightedRiftDestination destination : destinations) {
-            weightMap.put(destination, destination.getWeight());
-        }
-        WeightedRiftDestination weightedDestination = MathUtils.weightedRandom(weightMap);
-
-        // Remove destinations from other groups if makeDestinationPermanent is true
-        if(makeDestinationPermanent) {
-            destinations.removeIf(wdest -> wdest.getGroup() != weightedDestination.getGroup());
-            markDirty();
-        }
-
         // Attempt a teleport
         try {
-            if (weightedDestination.getDestination().teleport(this, entity)) {
-                // Set last used rift if necessary
-                // TODO: What about player-owned entities? We should store their exit rift separately to avoid having problems if they enter different rifts
-                // TODO: use entity UUID rather than player UUID!
-                if (entity instanceof EntityPlayer && !ModDimensions.isDimDoorsPocketDimension(WorldUtils.getDim(world))) {
-                    RiftRegistry.setOverworldRift(EntityUtils.getEntityOwnerUUID(entity), new Location(world, pos));
+            if (destination.teleport(new RotatedLocation(new Location(world, pos), yaw, pitch), entity)) {
+                // Set last used rift for players (don't set for other entities to avoid filling the registry too much)
+                // TODO: it should maybe be set for some non-player entities too
+                if (!ModDimensions.isDimDoorsPocketDimension(WorldUtils.getDim(world)) && entity instanceof EntityPlayer) {
+                    RiftRegistry.instance().setOverworldRift(entity.getUniqueID(), new Location(world, pos));
                 }
                 return true;
             }
@@ -309,45 +202,30 @@ import java.util.*;
         return false;
     }
 
-    public void teleportTo(Entity entity) { // TODO: new velocity angle if !preserveRotation?
-        float newYaw = entity.rotationYaw;
-        float newPitch = entity.rotationYaw;
-        if (!preserveRotation) {
-            newYaw = yaw;
-            newPitch = pitch;
+    public void teleportTo(Entity entity, float fromYaw, float fromPitch) {
+        if (relativeRotation) {
+            TeleportUtils.teleport(entity, new Location(world, pos), yaw + entity.rotationYaw - fromYaw, pitch + entity.rotationPitch - fromPitch);
+        } else {
+            TeleportUtils.teleport(entity, new Location(world, pos), yaw, pitch);
         }
-        TeleportUtils.teleport(entity, new Location(world, pos), newYaw, newPitch);
     }
 
-    public void updateColor() { // TODO: have the registry call this method too
+    public void teleportTo(Entity entity) {
+        TeleportUtils.teleport(entity, new Location(world, pos), yaw, pitch);
+    }
+
+    public void updateColor() {
+        if (forcedColor) return;
         if (!isRegistered()) {
             color = new RGBA(0, 0, 0, 1);
-            return;
-        }
-        if (destinations.size() == 0) {
+        } else if (destination == null) {
             color = new RGBA(0.7f, 0.7f, 0.7f, 1);
-            return;
-        }
-        boolean safe = true;
-        for (WeightedRiftDestination weightedDestination : destinations) {
-            boolean destSafe = false;
-            RiftDestination destination = weightedDestination.getDestination();
-            if (destination instanceof PrivateDestination
-                || destination instanceof PocketExitDestination
-                || destination instanceof PrivatePocketExitDestination) destSafe = true;
-
-            if (!destSafe && destination.getReferencedRift(getLocation()) != null) {
-                RiftRegistry.RiftInfo riftInfo = RiftRegistry.getRiftInfo(destination.getReferencedRift(getLocation()));
-                destSafe = riftInfo != null
-                    && riftInfo.destinations.size() == 1
-                    && riftInfo.destinations.iterator().next().equals(getLocation());
-            }
-            safe &= destSafe;
-        }
-        if (safe) {
-            color = new RGBA(0, 1, 0, 1);
         } else {
-            color = new RGBA(1, 0, 0, 1);
+            RGBA newColor = destination.getColor(new Location(world, pos));
+            if (!color.equals(newColor)) {
+                color = newColor;
+                markDirty();
+            }
         }
     }
 
@@ -358,25 +236,5 @@ import java.util.*;
     }
 
     // Info
-    protected abstract boolean isFloating(); // TODO: make non-abstract?
-
-    public Location getLocation() {
-        return new Location(world, pos);
-    }
-
-    public WeightedRiftDestination getDestination(UUID id) {
-        for (WeightedRiftDestination wdest : destinations) {
-            if (wdest.getId().equals(id)) {
-                return wdest;
-            }
-        }
-        return null;
-    }
-
-    public AvailableLink getAvailableLink(UUID linkId) {
-        for (AvailableLink link : availableLinks) {
-            if (link.id.equals(linkId)) return link;
-        }
-        return null;
-    }
+    protected abstract boolean isFloating();
 }
diff --git a/src/main/java/org/dimdev/dimdoors/shared/rifts/WeightedRiftDestination.java b/src/main/java/org/dimdev/dimdoors/shared/rifts/WeightedRiftDestination.java
deleted file mode 100644
index 926f8286..00000000
--- a/src/main/java/org/dimdev/dimdoors/shared/rifts/WeightedRiftDestination.java
+++ /dev/null
@@ -1,58 +0,0 @@
-package org.dimdev.dimdoors.shared.rifts;
-
-import lombok.AllArgsConstructor;
-import org.dimdev.ddutils.nbt.INBTStorable;
-import lombok.Getter;
-import net.minecraft.nbt.NBTTagCompound;
-
-import java.util.UUID;
-
-public class WeightedRiftDestination implements INBTStorable { // TODO: generics
-    @Getter private RiftDestination destination;
-    @Getter private float weight;
-    @Getter private int group;
-    @Getter private RiftDestination oldDestination; // TODO: move to RiftDestination?
-    @Getter private UUID id;
-
-    public WeightedRiftDestination() {
-        id = UUID.randomUUID();
-    }
-
-    public WeightedRiftDestination(RiftDestination destination, float weight, int group, RiftDestination oldDestination) {
-        this();
-        this.destination = destination;
-        this.weight = weight;
-        this.group = group;
-        this.oldDestination = oldDestination;
-        if (destination != null) destination.weightedDestination = this;
-        if (oldDestination != null) oldDestination.weightedDestination = this;
-    }
-
-    public WeightedRiftDestination(RiftDestination destination, float weight, int group, RiftDestination oldDestination, UUID id) {
-        this(destination, weight, group, oldDestination);
-        this.id = id;
-    }
-
-    public WeightedRiftDestination(RiftDestination destination, float weight, int group) {
-        this(destination, weight, group, null);
-    }
-
-    @Override
-    public void readFromNBT(NBTTagCompound nbt) {
-        destination = RiftDestination.readDestinationNBT(nbt); // TODO: subtag?
-        weight = nbt.getFloat("weight");
-        group = nbt.getInteger("group");
-        if (nbt.hasKey("oldDestination")) oldDestination = RiftDestination.readDestinationNBT(nbt.getCompoundTag("oldDestination"));
-        if (destination != null) destination.weightedDestination = this;
-        if (oldDestination != null) oldDestination.weightedDestination = this;
-    }
-
-    @Override
-    public NBTTagCompound writeToNBT(NBTTagCompound nbt) {
-        nbt = destination.writeToNBT(nbt);
-        nbt.setFloat("weight", weight);
-        nbt.setInteger("group", group);
-        if (oldDestination != null) nbt.setTag("oldDestination", oldDestination.writeToNBT(new NBTTagCompound()));
-        return nbt;
-    }
-}
diff --git a/src/main/java/org/dimdev/dimdoors/shared/rifts/destinations/AvailableLinkDestination.java b/src/main/java/org/dimdev/dimdoors/shared/rifts/destinations/AvailableLinkDestination.java
index adf0ad49..69c03975 100644
--- a/src/main/java/org/dimdev/dimdoors/shared/rifts/destinations/AvailableLinkDestination.java
+++ b/src/main/java/org/dimdev/dimdoors/shared/rifts/destinations/AvailableLinkDestination.java
@@ -11,6 +11,8 @@ import net.minecraft.util.math.BlockPos;
 import net.minecraft.world.World;
 import org.dimdev.annotatednbt.NBTSerializable;
 import org.dimdev.annotatednbt.Saved;
+import org.dimdev.ddutils.Location;
+import org.dimdev.ddutils.RotatedLocation;
 import org.dimdev.ddutils.WorldUtils;
 import org.dimdev.ddutils.math.MathUtils;
 import org.dimdev.ddutils.nbt.NBTUtils;
@@ -18,10 +20,10 @@ import org.dimdev.dimdoors.shared.VirtualLocation;
 import org.dimdev.dimdoors.shared.blocks.ModBlocks;
 import org.dimdev.dimdoors.shared.pockets.Pocket;
 import org.dimdev.dimdoors.shared.pockets.PocketGenerator;
-import org.dimdev.dimdoors.shared.rifts.AvailableLink;
-import org.dimdev.dimdoors.shared.rifts.RiftDestination;
-import org.dimdev.dimdoors.shared.rifts.RiftRegistry;
-import org.dimdev.dimdoors.shared.rifts.TileEntityRift;
+import org.dimdev.dimdoors.shared.rifts.*;
+import org.dimdev.dimdoors.shared.rifts.registry.LinkProperties;
+import org.dimdev.dimdoors.shared.rifts.registry.Rift;
+import org.dimdev.dimdoors.shared.rifts.registry.RiftRegistry;
 import org.dimdev.dimdoors.shared.tileentities.TileEntityFloatingRift;
 
 import java.util.HashMap;
@@ -30,7 +32,7 @@ import java.util.Set;
 import java.util.UUID;
 
 @Getter @AllArgsConstructor @Builder(toBuilder = true) @ToString
-@NBTSerializable public class AvailableLinkDestination extends RiftDestination { // TODO: increase link count on unregister
+@NBTSerializable public class AvailableLinkDestination extends RiftDestination {
     @Saved protected float newRiftWeight;
     @Saved protected double weightMaximum;
     @Saved protected double coordFactor;
@@ -39,7 +41,7 @@ import java.util.UUID;
     @Saved protected Set<Integer> acceptedGroups; // TODO: this should be immutable
     @Saved protected boolean noLink;
     @Builder.Default @Saved protected boolean noLinkBack;
-    @Builder.Default @Saved protected UUID linkId = UUID.randomUUID();
+    // TODO: better depth calculation
 
     public AvailableLinkDestination() {}
 
@@ -47,25 +49,23 @@ import java.util.UUID;
     @Override public NBTTagCompound writeToNBT(NBTTagCompound nbt) { nbt = super.writeToNBT(nbt); return NBTUtils.writeToNBT(this, nbt); }
 
     @Override
-    public boolean teleport(TileEntityRift rift, Entity entity) {
-        if (rift.getVirtualLocation() == null) return false;
-        AvailableLink thisLink = rift.getAvailableLink(linkId);
-        thisLink.linksRemaining--;
-        RiftRegistry.getRegistry(rift.getLocation()).markDirty();
-        Map<AvailableLink, Float> possibleDestWeightMap = new HashMap<>();
-        if (newRiftWeight > 0) possibleDestWeightMap.put(null, newRiftWeight);
+    public boolean teleport(RotatedLocation location, Entity entity) {
+        VirtualLocation virtualLocationHere = VirtualLocation.fromLocation(location.getLocation());
 
-        for (AvailableLink link : RiftRegistry.getAvailableLinks()) {
-            RiftRegistry.RiftInfo otherRift = RiftRegistry.getRiftInfo(link.rift);
-            double otherWeight = otherRift.isEntrance ? link.entranceWeight : link.floatingWeight;
-            if (otherWeight == 0 || Sets.intersection(acceptedGroups, link.groups).isEmpty()) continue;
+        Map<Location, Float> riftWeights = new HashMap<>();
+        if (newRiftWeight > 0) riftWeights.put(null, newRiftWeight);
+
+        for (Rift otherRift : RiftRegistry.instance().getRifts()) {
+            VirtualLocation otherVirtualLocation = VirtualLocation.fromLocation(otherRift.location);
+            double otherWeight = otherRift.isFloating ? otherRift.properties.floatingWeight : otherRift.properties.entranceWeight;
+            if (otherWeight == 0 || Sets.intersection(acceptedGroups, otherRift.properties.groups).isEmpty()) continue;
 
             // Calculate the distance as sqrt((coordFactor * coordDistance)^2 + (depthFactor * depthDifference)^2)
-            if (otherRift.virtualLocation == null || link.linksRemaining == 0) continue;
-            double depthDifference = otherRift.virtualLocation.getDepth() - rift.getVirtualLocation().getDepth();
-            double coordDistance = Math.sqrt(sq(rift.getVirtualLocation().getX() - otherRift.virtualLocation.getX())
-                                             + sq(rift.getVirtualLocation().getZ() - otherRift.virtualLocation.getZ()));
-            double depthFactor = depthDifference > 0 ? positiveDepthFactor : negativeDepthFactor; // TODO: (|depthDiff| - depthFavor * depthDiff)?
+            if (otherVirtualLocation == null || otherRift.properties.linksRemaining == 0) continue;
+            double depthDifference = otherVirtualLocation.getDepth() - virtualLocationHere.getDepth();
+            double coordDistance = Math.sqrt(sq(otherVirtualLocation.getX() - virtualLocationHere.getX())
+                                             + sq(otherVirtualLocation.getZ() - virtualLocationHere.getZ()));
+            double depthFactor = depthDifference > 0 ? positiveDepthFactor : negativeDepthFactor;
             double distance = sq(coordFactor * coordDistance) + sq(depthFactor * depthDifference);
 
             // Calculate the weight as 4m/pi w/(m^2/d + d)^2. This is similar to how gravitational/electromagnetic attraction
@@ -77,18 +77,18 @@ import java.util.UUID;
             // of 1 is equivalent to having a total link weight of 1 distributed equally across all layers.
             // TODO: We might want an a larger than 1 to make the function closer to 1/d^2
             double weight = 4 * weightMaximum / Math.PI * otherWeight / sq(sq(weightMaximum) / distance + distance);
-            possibleDestWeightMap.put(link, (float) weight);
+            riftWeights.put(otherRift.location, (float) weight);
         }
 
-        AvailableLink selectedLink;
-        if (possibleDestWeightMap.size() == 0) {
+        Location selectedLink;
+        if (riftWeights.size() == 0) {
             if (newRiftWeight == -1) {
                 selectedLink = null;
             } else {
                 return false;
             }
         } else {
-            selectedLink = MathUtils.weightedRandom(possibleDestWeightMap);
+            selectedLink = MathUtils.weightedRandom(riftWeights);
         }
 
         // Check if we have to generate a new rift
@@ -121,10 +121,10 @@ import java.util.UUID;
             depth /= depth > 0 ? positiveDepthFactor : negativeDepthFactor;
             double x = Math.cos(theta) * Math.cos(phi) * distance / coordFactor;
             double z = Math.cos(theta) * Math.sin(phi) * distance / coordFactor;
-            VirtualLocation virtualLocation = new VirtualLocation(rift.getVirtualLocation().getDim(),
-                    rift.getVirtualLocation().getX() + (int) Math.round(x),
-                    rift.getVirtualLocation().getZ() + (int) Math.round(z),
-                    rift.getVirtualLocation().getDepth() + (int) Math.round(depth));
+            VirtualLocation virtualLocation = new VirtualLocation(virtualLocationHere.getDim(),
+                    virtualLocationHere.getX() + (int) Math.round(x),
+                    virtualLocationHere.getZ() + (int) Math.round(z),
+                    virtualLocationHere.getDepth() + (int) Math.round(depth));
 
             if (virtualLocation.getDepth() <= 0) {
                 // This will lead to the overworld
@@ -132,43 +132,51 @@ import java.util.UUID;
                 BlockPos pos = world.getTopSolidOrLiquidBlock(new BlockPos(virtualLocation.getX(), 0, virtualLocation.getZ()));
                 world.setBlockState(pos, ModBlocks.RIFT.getDefaultState());
 
+                TileEntityRift thisRift = (TileEntityRift) location.getLocation().getTileEntity();
                 TileEntityFloatingRift riftEntity = (TileEntityFloatingRift) world.getTileEntity(pos);
                 // TODO: Should the rift not be configured like the other link
-                rift.markDirty();
-                AvailableLink newLink = thisLink.toBuilder().linksRemaining(0).id(UUID.randomUUID()).build();
-                riftEntity.addAvailableLink(newLink);
-                if (!noLinkBack) riftEntity.addDestination(new GlobalDestination(rift.getLocation()), 1, 0, toBuilder().linkId(newLink.id).build());
-                if (!noLink) rift.makeDestinationPermanent(weightedDestination, riftEntity.getLocation());
-                riftEntity.teleportTo(entity);
+                riftEntity.setProperties(thisRift.getProperties().toBuilder().linksRemaining(1).id(UUID.randomUUID()).build());
+
+                if (!noLinkBack && !riftEntity.getProperties().oneWay) linkRifts(selectedLink, location.getLocation());
+                if (!noLink) linkRifts(location.getLocation(), selectedLink);
+                riftEntity.teleportTo(entity, thisRift.getYaw(), thisRift.getPitch());
             } else {
                 // Make a new dungeon pocket
                 //Pocket pocket = PocketGenerator.generateDungeonPocket(virtualLocation);
                 Pocket pocket = PocketGenerator.generatePublicPocket(virtualLocation);
                 pocket.setup();
-                rift.markDirty();
-                AvailableLink newLink = thisLink.toBuilder().linksRemaining(0).build();
-                pocket.linkPocketTo(new GlobalDestination(/*noLinkBack ? null :*/ rift.getLocation()), toBuilder().linkId(newLink.id).build(), newLink); // TODO: linkId
-                if (!noLink) rift.makeDestinationPermanent(weightedDestination, pocket.getEntrance());
-                ((TileEntityRift) pocket.getEntrance().getTileEntity()).teleportTo(entity);
+
+                // Link the pocket back
+                TileEntityRift thisRift = (TileEntityRift) location.getLocation().getTileEntity();
+                TileEntityRift riftEntity = (TileEntityRift) pocket.getEntrance().getTileEntity();
+                LinkProperties newLink = thisRift.getProperties().toBuilder().linksRemaining(0).id(UUID.randomUUID()).build();
+                pocket.linkPocketTo(new GlobalDestination(!noLinkBack && !riftEntity.getProperties().oneWay ? location.getLocation() : null), newLink); // TODO: linkId
+
+                // Link the rift if necessary and teleport the entity
+                if (!noLink) linkRifts(location.getLocation(), selectedLink);
+                ((TileEntityRift) pocket.getEntrance().getTileEntity()).teleportTo(entity, location.getYaw(), location.getPitch());
             }
         } else {
             // An existing rift was selected
-            TileEntityRift riftEntity = (TileEntityRift) selectedLink.rift.getTileEntity();
+            TileEntityRift riftEntity = (TileEntityRift) selectedLink.getTileEntity();
 
-            selectedLink.linksRemaining--;
-            RiftRegistry.getRegistry(riftEntity.getLocation()).markDirty();
-
-            // Link the selected rift back if necessary
-            if (selectedLink.replaceDestination != null) {
-                riftEntity.makeDestinationPermanent(riftEntity.getDestination(selectedLink.replaceDestination), rift.getLocation());
-            }
-
-            // Link this rift if necessary and teleport the entity
-            if (!noLink) rift.makeDestinationPermanent(weightedDestination, selectedLink.rift);
-            riftEntity.teleportTo(entity);
+            // Link the rifts if necessary and teleport the entity
+            if (!noLink) linkRifts(location.getLocation(), selectedLink);
+            if (!noLinkBack && !riftEntity.getProperties().oneWay) linkRifts(selectedLink, location.getLocation());
+            riftEntity.teleportTo(entity, location.getYaw(), location.getPitch());
         }
         return true;
     }
 
+    private static void linkRifts(Location from, Location to) {
+        TileEntityRift tileEntityFrom = (TileEntityRift) from.getTileEntity();
+        TileEntityRift tileEntityTo = (TileEntityRift) to.getTileEntity();
+        tileEntityFrom.setDestination(new GlobalDestination(to)); // TODO: local if possible
+        tileEntityTo.getProperties().linksRemaining--;
+        tileEntityTo.updateProperties();
+        tileEntityFrom.markDirty();
+        tileEntityTo.markDirty();
+    }
+
     private double sq(double a) { return a * a; }
 }
diff --git a/src/main/java/org/dimdev/dimdoors/shared/rifts/destinations/EscapeDestination.java b/src/main/java/org/dimdev/dimdoors/shared/rifts/destinations/EscapeDestination.java
index 3671a364..02639dda 100644
--- a/src/main/java/org/dimdev/dimdoors/shared/rifts/destinations/EscapeDestination.java
+++ b/src/main/java/org/dimdev/dimdoors/shared/rifts/destinations/EscapeDestination.java
@@ -1,8 +1,9 @@
 package org.dimdev.dimdoors.shared.rifts.destinations;
 
+import org.dimdev.ddutils.RotatedLocation;
 import org.dimdev.dimdoors.DimDoors;
 import org.dimdev.dimdoors.shared.rifts.RiftDestination;
-import org.dimdev.dimdoors.shared.rifts.RiftRegistry;
+import org.dimdev.dimdoors.shared.rifts.registry.RiftRegistry;
 import org.dimdev.dimdoors.shared.rifts.TileEntityRift;
 import org.dimdev.dimdoors.shared.world.ModDimensions;
 import org.dimdev.dimdoors.shared.world.limbodimension.WorldProviderLimbo;
@@ -15,6 +16,8 @@ import lombok.ToString;
 import net.minecraft.entity.Entity;
 import net.minecraft.nbt.NBTTagCompound;
 
+import java.util.UUID;
+
 @Getter @AllArgsConstructor @Builder(toBuilder = true) @ToString
 public class EscapeDestination extends RiftDestination {
     //public EscapeDestination() {}
@@ -31,14 +34,14 @@ public class EscapeDestination extends RiftDestination {
     }
 
     @Override
-    public boolean teleport(TileEntityRift rift, Entity entity) {
+    public boolean teleport(RotatedLocation loc, Entity entity) {
         if (!ModDimensions.isDimDoorsPocketDimension(entity.world)) {
             DimDoors.chat(entity, "Can't escape from a non-pocket dimension!");
             return false;
         }
-        String uuid = entity.getCachedUniqueIdString();
+        UUID uuid = entity.getUniqueID();
         if (uuid != null) {
-            Location destLoc = RiftRegistry.getOverworldRift(uuid);
+            Location destLoc = RiftRegistry.instance().getOverworldRift(uuid);
             if (destLoc != null && destLoc.getTileEntity() instanceof TileEntityRift) {
                 //TeleportUtils.teleport(entity, new VirtualLocation(destLoc, rift.virtualLocation.getDepth()).projectToWorld()); // TODO
                 // TODO
diff --git a/src/main/java/org/dimdev/dimdoors/shared/rifts/destinations/GlobalDestination.java b/src/main/java/org/dimdev/dimdoors/shared/rifts/destinations/GlobalDestination.java
index 6dfd5e2a..1f78e912 100644
--- a/src/main/java/org/dimdev/dimdoors/shared/rifts/destinations/GlobalDestination.java
+++ b/src/main/java/org/dimdev/dimdoors/shared/rifts/destinations/GlobalDestination.java
@@ -7,6 +7,7 @@ import lombok.Getter;
 import lombok.ToString;
 import net.minecraft.entity.Entity;
 import net.minecraft.nbt.NBTTagCompound;
+import org.dimdev.ddutils.RotatedLocation;
 import org.dimdev.ddutils.nbt.NBTUtils;
 import org.dimdev.annotatednbt.Saved;
 import org.dimdev.annotatednbt.NBTSerializable;
@@ -15,7 +16,7 @@ import org.dimdev.dimdoors.shared.rifts.TileEntityRift;
 
 @Getter @AllArgsConstructor @Builder(toBuilder = true) @ToString
 @NBTSerializable public class GlobalDestination extends RiftDestination { // TODO: location directly in nbt like minecraft?
-    @Saved @Getter protected Location loc;
+    @Saved protected Location loc;
 
     public GlobalDestination() {}
 
@@ -23,13 +24,13 @@ import org.dimdev.dimdoors.shared.rifts.TileEntityRift;
     @Override public NBTTagCompound writeToNBT(NBTTagCompound nbt) { nbt = super.writeToNBT(nbt); return NBTUtils.writeToNBT(this, nbt); }
 
     @Override
-    public boolean teleport(TileEntityRift rift, Entity entity) {
-        ((TileEntityRift) loc.getTileEntity()).teleportTo(entity);
+    public boolean teleport(RotatedLocation loc, Entity entity) {
+        ((TileEntityRift) this.loc.getTileEntity()).teleportTo(entity, loc.getYaw(), loc.getPitch());
         return true;
     }
 
     @Override
-    public Location getReferencedRift(Location rift) {
+    public Location getFixedTarget(Location location) {
         return loc;
     }
 }
diff --git a/src/main/java/org/dimdev/dimdoors/shared/rifts/destinations/LimboDestination.java b/src/main/java/org/dimdev/dimdoors/shared/rifts/destinations/LimboDestination.java
index 59f3c888..63087fa9 100644
--- a/src/main/java/org/dimdev/dimdoors/shared/rifts/destinations/LimboDestination.java
+++ b/src/main/java/org/dimdev/dimdoors/shared/rifts/destinations/LimboDestination.java
@@ -1,7 +1,7 @@
 package org.dimdev.dimdoors.shared.rifts.destinations;
 
+import org.dimdev.ddutils.RotatedLocation;
 import org.dimdev.dimdoors.shared.rifts.RiftDestination;
-import org.dimdev.dimdoors.shared.rifts.TileEntityRift;
 import org.dimdev.dimdoors.shared.world.limbodimension.WorldProviderLimbo;
 import org.dimdev.ddutils.TeleportUtils;
 import lombok.AllArgsConstructor;
@@ -27,7 +27,7 @@ public class LimboDestination extends RiftDestination {
     }
 
     @Override
-    public boolean teleport(TileEntityRift rift, Entity entity) {
+    public boolean teleport(RotatedLocation loc, Entity entity) {
         TeleportUtils.teleport(entity, WorldProviderLimbo.getLimboSkySpawn(entity)); // TODO: do we really want to spam Limbo with items?
         return false;
     }
diff --git a/src/main/java/org/dimdev/dimdoors/shared/rifts/destinations/LinkingDestination.java b/src/main/java/org/dimdev/dimdoors/shared/rifts/destinations/LinkingDestination.java
new file mode 100644
index 00000000..c5bd250d
--- /dev/null
+++ b/src/main/java/org/dimdev/dimdoors/shared/rifts/destinations/LinkingDestination.java
@@ -0,0 +1,60 @@
+package org.dimdev.dimdoors.shared.rifts.destinations;
+
+import net.minecraft.entity.Entity;
+import net.minecraft.nbt.NBTTagCompound;
+import org.dimdev.ddutils.Location;
+import org.dimdev.ddutils.RGBA;
+import org.dimdev.ddutils.RotatedLocation;
+import org.dimdev.dimdoors.shared.rifts.RiftDestination;
+
+public abstract class LinkingDestination extends RiftDestination {
+
+    private RiftDestination wrappedDestination;
+
+    @Override public void readFromNBT(NBTTagCompound nbt) { super.readFromNBT(nbt); }
+    @Override public NBTTagCompound writeToNBT(NBTTagCompound nbt) { nbt = super.writeToNBT(nbt); return nbt; }
+
+    @Override
+    public boolean teleport(RotatedLocation loc, Entity entity) {
+        if (wrappedDestination != null) wrappedDestination.teleport(loc, entity);
+
+        Location linkTarget = makeLinkTarget(loc, entity);
+        if (linkTarget != null) {
+            wrappedDestination = new GlobalDestination();
+            wrappedDestination.register(loc.getLocation());
+
+            wrappedDestination.teleport(loc, entity);
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    @Override
+    public boolean keepAfterTargetGone(Location location) {
+        if (!wrappedDestination.keepAfterTargetGone(location)) {
+            wrappedDestination.unregister(location);
+        }
+        return true;
+    }
+
+    @Override
+    public void unregister(Location location) {
+        if (wrappedDestination != null) wrappedDestination.unregister(location);
+    }
+
+    @Override
+    public RGBA getColor(Location location) {
+        if (wrappedDestination != null) {
+            return wrappedDestination.getColor(location);
+        } else {
+            return getUnlinkedColor(location);
+        }
+    }
+
+    protected RGBA getUnlinkedColor(Location location) {
+        return new RGBA(0, 1, 1, 1);
+    }
+
+    public abstract Location makeLinkTarget(RotatedLocation rift, Entity entity);
+}
diff --git a/src/main/java/org/dimdev/dimdoors/shared/rifts/destinations/LocalDestination.java b/src/main/java/org/dimdev/dimdoors/shared/rifts/destinations/LocalDestination.java
index e4c59a43..9a294c24 100644
--- a/src/main/java/org/dimdev/dimdoors/shared/rifts/destinations/LocalDestination.java
+++ b/src/main/java/org/dimdev/dimdoors/shared/rifts/destinations/LocalDestination.java
@@ -1,6 +1,7 @@
 package org.dimdev.dimdoors.shared.rifts.destinations;
 
 import org.dimdev.ddutils.Location;
+import org.dimdev.ddutils.RotatedLocation;
 import org.dimdev.ddutils.nbt.NBTUtils;
 import lombok.AllArgsConstructor;
 import lombok.Builder;
@@ -24,13 +25,13 @@ import org.dimdev.dimdoors.shared.rifts.TileEntityRift;
     @Override public NBTTagCompound writeToNBT(NBTTagCompound nbt) { nbt = super.writeToNBT(nbt); return NBTUtils.writeToNBT(this, nbt); }
 
     @Override
-    public boolean teleport(TileEntityRift rift, Entity entity) {
-        ((TileEntityRift) rift.getWorld().getTileEntity(pos)).teleportTo(entity);
+    public boolean teleport(RotatedLocation loc, Entity entity) {
+        ((TileEntityRift) loc.getLocation().getWorld().getTileEntity(pos)).teleportTo(entity, loc.getYaw(), loc.getPitch());
         return true;
     }
 
     @Override
-    public Location getReferencedRift(Location rift) {
-        return new Location(rift.getDim(), pos);
+    public Location getFixedTarget(Location location) {
+        return new Location(location.getDim(), pos);
     }
 }
diff --git a/src/main/java/org/dimdev/dimdoors/shared/rifts/destinations/NewPublicDestination.java b/src/main/java/org/dimdev/dimdoors/shared/rifts/destinations/NewPublicDestination.java
deleted file mode 100644
index ea775301..00000000
--- a/src/main/java/org/dimdev/dimdoors/shared/rifts/destinations/NewPublicDestination.java
+++ /dev/null
@@ -1,45 +0,0 @@
-package org.dimdev.dimdoors.shared.rifts.destinations;
-
-import org.dimdev.dimdoors.shared.VirtualLocation;
-import org.dimdev.dimdoors.shared.pockets.Pocket;
-import org.dimdev.dimdoors.shared.pockets.PocketGenerator;
-import lombok.AllArgsConstructor;
-import lombok.Builder;
-import lombok.Getter;
-import lombok.ToString;
-import net.minecraft.entity.Entity;
-import net.minecraft.nbt.NBTTagCompound;
-import org.dimdev.dimdoors.shared.rifts.RiftDestination;
-import org.dimdev.dimdoors.shared.rifts.TileEntityRift;
-
-@Getter @AllArgsConstructor @Builder(toBuilder = true) @ToString
-public class NewPublicDestination extends RiftDestination { // TODO: more config options such as non-default size, etc.
-    //public NewPublicDestination() {}
-
-    @Override
-    public void readFromNBT(NBTTagCompound nbt) {
-        super.readFromNBT(nbt);
-    }
-
-    @Override
-    public NBTTagCompound writeToNBT(NBTTagCompound nbt) {
-        nbt = super.writeToNBT(nbt);
-        return nbt;
-    }
-
-    @Override
-    public boolean teleport(TileEntityRift rift, Entity entity) {
-        VirtualLocation newVirtualLocation = null;
-        if (rift.getVirtualLocation() != null) {
-            int depth = rift.getVirtualLocation().getDepth();
-            if (depth == 0) depth++;
-            newVirtualLocation = rift.getVirtualLocation().toBuilder().depth(depth).build();
-        }
-        Pocket pocket = PocketGenerator.generatePublicPocket(newVirtualLocation);
-        pocket.setup();
-        pocket.linkPocketTo(new GlobalDestination(rift.getLocation()), null, null);
-        rift.makeDestinationPermanent(weightedDestination, pocket.getEntrance());
-        ((TileEntityRift) pocket.getEntrance().getTileEntity()).teleportTo(entity);
-        return true;
-    }
-}
diff --git a/src/main/java/org/dimdev/dimdoors/shared/rifts/destinations/PocketEntranceDestination.java b/src/main/java/org/dimdev/dimdoors/shared/rifts/destinations/PocketEntranceDestination.java
index 0a48188b..a14fcab2 100644
--- a/src/main/java/org/dimdev/dimdoors/shared/rifts/destinations/PocketEntranceDestination.java
+++ b/src/main/java/org/dimdev/dimdoors/shared/rifts/destinations/PocketEntranceDestination.java
@@ -1,5 +1,6 @@
 package org.dimdev.dimdoors.shared.rifts.destinations;
 
+import org.dimdev.ddutils.RotatedLocation;
 import org.dimdev.ddutils.nbt.NBTUtils;
 import org.dimdev.annotatednbt.Saved;
 import org.dimdev.annotatednbt.NBTSerializable;
@@ -12,17 +13,13 @@ import net.minecraft.entity.Entity;
 import net.minecraft.entity.player.EntityPlayer;
 import net.minecraft.nbt.NBTTagCompound;
 import org.dimdev.dimdoors.shared.rifts.RiftDestination;
-import org.dimdev.dimdoors.shared.rifts.TileEntityRift;
-import org.dimdev.dimdoors.shared.rifts.WeightedRiftDestination;
-
-import java.util.LinkedList;
-import java.util.List;
 
 @Getter @AllArgsConstructor @Builder(toBuilder = true) @ToString
-@NBTSerializable public class PocketEntranceDestination extends RiftDestination {
-    @Saved protected float weight;
-    @Saved @SuppressWarnings({"UnusedAssignment", "RedundantSuppression"}) @Builder.Default protected List<WeightedRiftDestination> ifDestinations = new LinkedList<>(); // TODO addIfDestination method in builder
-    @Saved @SuppressWarnings({"UnusedAssignment", "RedundantSuppression"}) @Builder.Default protected List<WeightedRiftDestination> otherwiseDestinations = new LinkedList<>(); // TODO addOtherwiseDestination method in builder
+@NBTSerializable public class PocketEntranceDestination extends RiftDestination { // TODO: not exactly a destination
+    @Builder.Default @Saved protected float weight = 1;
+    @Saved protected RiftDestination ifDestination;
+    @Saved protected RiftDestination otherwiseDestination;
+    @Saved boolean hasBeenChosen;
 
     public PocketEntranceDestination() {}
 
@@ -30,7 +27,7 @@ import java.util.List;
     @Override public NBTTagCompound writeToNBT(NBTTagCompound nbt) { nbt = super.writeToNBT(nbt); return NBTUtils.writeToNBT(this, nbt); }
 
     @Override
-    public boolean teleport(TileEntityRift rift, Entity entity) {
+    public boolean teleport(RotatedLocation loc, Entity entity) {
         if (entity instanceof EntityPlayer) DimDoors.chat(entity, "The entrances of this dungeon has not been linked. Either this is a bug or you are in dungeon-building mode.");
         return false;
     }
diff --git a/src/main/java/org/dimdev/dimdoors/shared/rifts/destinations/PocketExitDestination.java b/src/main/java/org/dimdev/dimdoors/shared/rifts/destinations/PocketExitDestination.java
index 2b562cb2..ed78973e 100644
--- a/src/main/java/org/dimdev/dimdoors/shared/rifts/destinations/PocketExitDestination.java
+++ b/src/main/java/org/dimdev/dimdoors/shared/rifts/destinations/PocketExitDestination.java
@@ -1,5 +1,6 @@
 package org.dimdev.dimdoors.shared.rifts.destinations;
 
+import org.dimdev.ddutils.RotatedLocation;
 import org.dimdev.dimdoors.DimDoors;
 import lombok.AllArgsConstructor;
 import lombok.Builder;
@@ -9,10 +10,9 @@ import net.minecraft.entity.Entity;
 import net.minecraft.entity.player.EntityPlayer;
 import net.minecraft.nbt.NBTTagCompound;
 import org.dimdev.dimdoors.shared.rifts.RiftDestination;
-import org.dimdev.dimdoors.shared.rifts.TileEntityRift;
 
 @Getter @AllArgsConstructor @Builder(toBuilder = true) @ToString
-public class PocketExitDestination extends RiftDestination {
+public class PocketExitDestination extends RiftDestination { // TODO: not exactly a destination
     //public PocketExitDestination() {}
 
     @Override
@@ -27,7 +27,7 @@ public class PocketExitDestination extends RiftDestination {
     }
 
     @Override
-    public boolean teleport(TileEntityRift rift, Entity entity) {
+    public boolean teleport(RotatedLocation loc, Entity entity) {
         if (entity instanceof EntityPlayer) DimDoors.chat(entity, "The exit of this dungeon has not been linked. Either this is a bug or you are in dungeon-building mode.");
         return false;
     }
diff --git a/src/main/java/org/dimdev/dimdoors/shared/rifts/destinations/PrivateDestination.java b/src/main/java/org/dimdev/dimdoors/shared/rifts/destinations/PrivateDestination.java
index 2cb17602..8b9972ef 100644
--- a/src/main/java/org/dimdev/dimdoors/shared/rifts/destinations/PrivateDestination.java
+++ b/src/main/java/org/dimdev/dimdoors/shared/rifts/destinations/PrivateDestination.java
@@ -1,11 +1,14 @@
 package org.dimdev.dimdoors.shared.rifts.destinations;
 
+import org.dimdev.ddutils.RGBA;
+import org.dimdev.ddutils.RotatedLocation;
 import org.dimdev.dimdoors.DimDoors;
+import org.dimdev.dimdoors.shared.VirtualLocation;
 import org.dimdev.dimdoors.shared.pockets.Pocket;
 import org.dimdev.dimdoors.shared.pockets.PocketGenerator;
 import org.dimdev.dimdoors.shared.pockets.PocketRegistry;
 import org.dimdev.dimdoors.shared.rifts.RiftDestination;
-import org.dimdev.dimdoors.shared.rifts.RiftRegistry;
+import org.dimdev.dimdoors.shared.rifts.registry.RiftRegistry;
 import org.dimdev.dimdoors.shared.rifts.TileEntityRift;
 import org.dimdev.dimdoors.shared.world.ModDimensions;
 import org.dimdev.ddutils.EntityUtils;
@@ -17,51 +20,51 @@ import lombok.ToString;
 import net.minecraft.entity.Entity;
 import net.minecraft.nbt.NBTTagCompound;
 
+import java.util.UUID;
+
 @Getter @AllArgsConstructor @Builder(toBuilder = true) @ToString
 public class PrivateDestination extends RiftDestination {
     //public PrivateDestination() {}
 
-    @Override
-    public void readFromNBT(NBTTagCompound nbt) {
-        super.readFromNBT(nbt);
-    }
+    @Override public void readFromNBT(NBTTagCompound nbt) { super.readFromNBT(nbt); }
+    @Override public NBTTagCompound writeToNBT(NBTTagCompound nbt) { nbt = super.writeToNBT(nbt); return nbt; }
 
     @Override
-    public NBTTagCompound writeToNBT(NBTTagCompound nbt) {
-        nbt = super.writeToNBT(nbt);
-        return nbt;
-    }
-
-    @Override
-    public boolean teleport(TileEntityRift rift, Entity entity) {
-        String uuid = EntityUtils.getEntityOwnerUUID(entity);
+    public boolean teleport(RotatedLocation loc, Entity entity) {
+        UUID uuid = EntityUtils.getEntityOwnerUUID(entity);
+        VirtualLocation virtualLocation = VirtualLocation.fromLocation(loc.getLocation());
         if (uuid != null) {
-            PocketRegistry privatePocketRegistry = PocketRegistry.getForDim(ModDimensions.getPrivateDim());
-            RiftRegistry privateRiftRegistry = RiftRegistry.getForDim(ModDimensions.getPrivateDim());
+            PocketRegistry privatePocketRegistry = PocketRegistry.instance(ModDimensions.getPrivateDim());
             Pocket pocket = privatePocketRegistry.getPocket(privatePocketRegistry.getPrivatePocketID(uuid));
             if (pocket == null) { // generate the private pocket and get its entrances
-                pocket = PocketGenerator.generatePrivatePocket(rift.getVirtualLocation() != null ? rift.getVirtualLocation().toBuilder().depth(-1).build() : null); // set to where the pocket was first created
+                // set to where the pocket was first created
+                pocket = PocketGenerator.generatePrivatePocket(virtualLocation != null ? virtualLocation.toBuilder().depth(-1).build() : null);
                 pocket.setup();
                 privatePocketRegistry.setPrivatePocketID(uuid, pocket.getId());
-                ((TileEntityRift) pocket.getEntrance().getTileEntity()).teleportTo(entity);
-                privateRiftRegistry.setPrivatePocketExit(uuid, rift.getLocation());
+                ((TileEntityRift) pocket.getEntrance().getTileEntity()).teleportTo(entity, loc.getYaw(), loc.getPitch());
+                RiftRegistry.instance().setLastPrivatePocketExit(uuid, loc.getLocation());
                 return true;
             } else {
-                Location destLoc = privateRiftRegistry.getPrivatePocketEntrance(uuid); // get the last used entrances
+                Location destLoc = RiftRegistry.instance().getPrivatePocketEntrance(uuid); // get the last used entrances
                 if (destLoc == null) destLoc = pocket.getEntrance(); // if there's none, then set the target to the main entrances
                 if (destLoc == null) { // if the pocket entrances is gone, then create a new private pocket
                     DimDoors.log.info("All entrances are gone, creating a new private pocket!");
-                    pocket = PocketGenerator.generatePrivatePocket(rift.getVirtualLocation() != null ? rift.getVirtualLocation().toBuilder().depth(-1).build() : null);
+                    pocket = PocketGenerator.generatePrivatePocket(virtualLocation != null ? virtualLocation.toBuilder().depth(-1).build() : null);
                     pocket.setup();
                     privatePocketRegistry.setPrivatePocketID(uuid, pocket.getId());
                     destLoc = pocket.getEntrance();
                 }
-                ((TileEntityRift) destLoc.getTileEntity()).teleportTo(entity);
-                privateRiftRegistry.setPrivatePocketExit(uuid, rift.getLocation());
+                ((TileEntityRift) destLoc.getTileEntity()).teleportTo(entity, loc.getYaw(), loc.getPitch());
+                RiftRegistry.instance().setLastPrivatePocketExit(uuid, loc.getLocation());
                 return true;
             }
         } else {
             return false; // TODO: There should be a way to get other entities into your private pocket, though. Add API for other mods.
         }
     }
+
+    @Override
+    public RGBA getColor(Location location) {
+        return new RGBA(0, 1, 0, 1);
+    }
 }
diff --git a/src/main/java/org/dimdev/dimdoors/shared/rifts/destinations/PrivatePocketExitDestination.java b/src/main/java/org/dimdev/dimdoors/shared/rifts/destinations/PrivatePocketExitDestination.java
index 81043d00..7e7f8a15 100644
--- a/src/main/java/org/dimdev/dimdoors/shared/rifts/destinations/PrivatePocketExitDestination.java
+++ b/src/main/java/org/dimdev/dimdoors/shared/rifts/destinations/PrivatePocketExitDestination.java
@@ -1,26 +1,26 @@
 package org.dimdev.dimdoors.shared.rifts.destinations;
 
-import org.dimdev.dimdoors.DimDoors;
-import org.dimdev.dimdoors.shared.pockets.Pocket;
-import org.dimdev.dimdoors.shared.pockets.PocketRegistry;
-import org.dimdev.dimdoors.shared.rifts.RiftDestination;
-import org.dimdev.dimdoors.shared.rifts.RiftRegistry;
-import org.dimdev.dimdoors.shared.rifts.TileEntityRift;
-import org.dimdev.dimdoors.shared.world.ModDimensions;
-import org.dimdev.dimdoors.shared.world.limbodimension.WorldProviderLimbo;
-import org.dimdev.dimdoors.shared.world.pocketdimension.WorldProviderPersonalPocket;
-import org.dimdev.ddutils.EntityUtils;
-import org.dimdev.ddutils.Location;
-import org.dimdev.ddutils.TeleportUtils;
 import lombok.AllArgsConstructor;
 import lombok.Builder;
 import lombok.Getter;
 import lombok.ToString;
 import net.minecraft.entity.Entity;
 import net.minecraft.nbt.NBTTagCompound;
+import org.dimdev.ddutils.*;
+import org.dimdev.dimdoors.DimDoors;
+import org.dimdev.dimdoors.shared.pockets.Pocket;
+import org.dimdev.dimdoors.shared.pockets.PocketRegistry;
+import org.dimdev.dimdoors.shared.rifts.RiftDestination;
+import org.dimdev.dimdoors.shared.rifts.TileEntityRift;
+import org.dimdev.dimdoors.shared.rifts.registry.RiftRegistry;
+import org.dimdev.dimdoors.shared.world.ModDimensions;
+import org.dimdev.dimdoors.shared.world.limbodimension.WorldProviderLimbo;
+import org.dimdev.dimdoors.shared.world.pocketdimension.WorldProviderPersonalPocket;
+
+import java.util.UUID;
 
 @Getter @AllArgsConstructor @Builder(toBuilder = true) @ToString
-public class PrivatePocketExitDestination extends RiftDestination { // TODO: merge into PocketExit or Escape?
+public class PrivatePocketExitDestination extends RiftDestination {
     //public PrivatePocketExitDestination() {}
 
     @Override
@@ -35,15 +35,14 @@ public class PrivatePocketExitDestination extends RiftDestination { // TODO: mer
     }
 
     @Override
-    public boolean teleport(TileEntityRift rift, Entity entity) {
+    public boolean teleport(RotatedLocation loc, Entity entity) {
         Location destLoc;
-        String uuid = EntityUtils.getEntityOwnerUUID(entity);
+        UUID uuid = EntityUtils.getEntityOwnerUUID(entity);
         if (uuid != null) {
-            PocketRegistry privatePocketRegistry = PocketRegistry.getForDim(ModDimensions.getPrivateDim());
-            RiftRegistry privateRiftRegistry = RiftRegistry.getForDim(ModDimensions.getPrivateDim());
-            destLoc = privateRiftRegistry.getPrivatePocketExit(uuid);
-            if (rift.getWorld().provider instanceof WorldProviderPersonalPocket && privatePocketRegistry.getPrivatePocketID(uuid) == privatePocketRegistry.posToID(rift.getPos())) {
-                privateRiftRegistry.setPrivatePocketEntrance(uuid, rift.getLocation()); // Remember which exit was used for next time the pocket is entered
+            PocketRegistry privatePocketRegistry = PocketRegistry.instance(ModDimensions.getPrivateDim());
+            destLoc = RiftRegistry.instance().getPrivatePocketExit(uuid);
+            if (loc.getLocation().getWorld().provider instanceof WorldProviderPersonalPocket && privatePocketRegistry.getPrivatePocketID(uuid) == privatePocketRegistry.posToID(loc.getLocation().getPos())) {
+                RiftRegistry.instance().setLastPrivatePocketEntrance(uuid, loc.getLocation()); // Remember which exit was used for next time the pocket is entered
             }
             if (destLoc == null || !(destLoc.getTileEntity() instanceof TileEntityRift)) {
                 if (destLoc == null) {
@@ -54,7 +53,7 @@ public class PrivatePocketExitDestination extends RiftDestination { // TODO: mer
                 TeleportUtils.teleport(entity, WorldProviderLimbo.getLimboSkySpawn(entity));
                 return false;
             } else {
-                ((TileEntityRift) destLoc.getTileEntity()).teleportTo(entity);
+                ((TileEntityRift) destLoc.getTileEntity()).teleportTo(entity, loc.getYaw(), loc.getPitch());
                 return true;
             }
         } else {
@@ -63,14 +62,15 @@ public class PrivatePocketExitDestination extends RiftDestination { // TODO: mer
     }
 
     @Override
-    public void register(TileEntityRift rift) {
-        PocketRegistry privatePocketRegistry = PocketRegistry.getForDim(rift.getLocation().getDim());
-        Pocket pocket = privatePocketRegistry.getPocketAt(rift.getPos());
-        String uuid = privatePocketRegistry.getPrivatePocketOwner(pocket.getId());
-        if (uuid != null) {
-            RiftRegistry.getForDim(ModDimensions.getPrivateDim()).addPrivatePocketEntrance(uuid, rift.getLocation());
-        }
+    public void register(Location location) {
+        super.register(location);
+        PocketRegistry privatePocketRegistry = PocketRegistry.instance(location.getDim());
+        Pocket pocket = privatePocketRegistry.getPocketAt(location.getPos());
+        RiftRegistry.instance().addPocketEntrance(pocket, location);
     }
 
-    // TODO: unregister
+    @Override
+    public RGBA getColor(Location location) {
+        return new RGBA(0, 1, 0, 1);
+    }
 }
diff --git a/src/main/java/org/dimdev/dimdoors/shared/rifts/destinations/PublicPocketDestination.java b/src/main/java/org/dimdev/dimdoors/shared/rifts/destinations/PublicPocketDestination.java
new file mode 100644
index 00000000..b8c0fc9e
--- /dev/null
+++ b/src/main/java/org/dimdev/dimdoors/shared/rifts/destinations/PublicPocketDestination.java
@@ -0,0 +1,33 @@
+package org.dimdev.dimdoors.shared.rifts.destinations;
+
+import lombok.*;
+import net.minecraft.entity.Entity;
+import net.minecraft.nbt.NBTTagCompound;
+import org.dimdev.ddutils.Location;
+import org.dimdev.ddutils.RotatedLocation;
+import org.dimdev.dimdoors.shared.VirtualLocation;
+import org.dimdev.dimdoors.shared.pockets.Pocket;
+import org.dimdev.dimdoors.shared.pockets.PocketGenerator;
+
+@Getter @AllArgsConstructor @NoArgsConstructor @Builder(toBuilder = true) @ToString
+public class PublicPocketDestination extends LinkingDestination {
+
+    @Override public void readFromNBT(NBTTagCompound nbt) { super.readFromNBT(nbt); }
+    @Override public NBTTagCompound writeToNBT(NBTTagCompound nbt) { nbt = super.writeToNBT(nbt); return nbt; }
+
+    @Override
+    public Location makeLinkTarget(RotatedLocation loc, Entity entity) {
+        VirtualLocation riftVirtualLocation = VirtualLocation.fromLocation(loc.getLocation());
+        VirtualLocation newVirtualLocation = null;
+        if (riftVirtualLocation != null) {
+            int depth = Math.min(riftVirtualLocation.getDepth(), 1);
+            newVirtualLocation = riftVirtualLocation.toBuilder().depth(depth).build();
+        }
+        Pocket pocket = PocketGenerator.generatePublicPocket(newVirtualLocation);
+        pocket.setup();
+
+        pocket.linkPocketTo(new GlobalDestination(loc.getLocation()), null);
+
+        return pocket.getEntrance();
+    }
+}
diff --git a/src/main/java/org/dimdev/dimdoors/shared/rifts/destinations/RelativeDestination.java b/src/main/java/org/dimdev/dimdoors/shared/rifts/destinations/RelativeDestination.java
index 564d5cbb..6976d89b 100644
--- a/src/main/java/org/dimdev/dimdoors/shared/rifts/destinations/RelativeDestination.java
+++ b/src/main/java/org/dimdev/dimdoors/shared/rifts/destinations/RelativeDestination.java
@@ -1,6 +1,7 @@
 package org.dimdev.dimdoors.shared.rifts.destinations;
 
 import org.dimdev.ddutils.Location;
+import org.dimdev.ddutils.RotatedLocation;
 import org.dimdev.ddutils.nbt.NBTUtils;
 import lombok.AllArgsConstructor;
 import lombok.Builder;
@@ -24,13 +25,13 @@ import org.dimdev.dimdoors.shared.rifts.TileEntityRift;
     @Override public NBTTagCompound writeToNBT(NBTTagCompound nbt) { nbt = super.writeToNBT(nbt); return NBTUtils.writeToNBT(this, nbt); }
 
     @Override
-    public boolean teleport(TileEntityRift rift, Entity entity) {
-        rift.getWorld().getTileEntity(rift.getPos().add(offset));
+    public boolean teleport(RotatedLocation loc, Entity entity) {
+        ((TileEntityRift) loc.getLocation().getWorld().getTileEntity(loc.getLocation().getPos().add(offset))).teleportTo(entity, loc.getPitch(), loc.getYaw());
         return true;
     }
 
     @Override
-    public Location getReferencedRift(Location rift) {
-        return new Location(rift.getDim(), rift.getPos().add(offset));
+    public Location getFixedTarget(Location location) {
+        return new Location(location.getDim(), location.getPos().add(offset));
     }
 }
diff --git a/src/main/java/org/dimdev/dimdoors/shared/rifts/AvailableLink.java b/src/main/java/org/dimdev/dimdoors/shared/rifts/registry/LinkProperties.java
similarity index 86%
rename from src/main/java/org/dimdev/dimdoors/shared/rifts/AvailableLink.java
rename to src/main/java/org/dimdev/dimdoors/shared/rifts/registry/LinkProperties.java
index fe90c22f..308f18d4 100644
--- a/src/main/java/org/dimdev/dimdoors/shared/rifts/AvailableLink.java
+++ b/src/main/java/org/dimdev/dimdoors/shared/rifts/registry/LinkProperties.java
@@ -1,4 +1,4 @@
-package org.dimdev.dimdoors.shared.rifts;
+package org.dimdev.dimdoors.shared.rifts.registry;
 
 import lombok.*;
 import lombok.experimental.Wither;
@@ -14,7 +14,7 @@ import java.util.Set;
 import java.util.UUID;
 
 @NBTSerializable @AllArgsConstructor @NoArgsConstructor @EqualsAndHashCode @Builder(toBuilder = true) @ToString
-public class AvailableLink implements INBTStorable {
+public class LinkProperties implements INBTStorable {
     @Wither public Location rift;
 
     @Saved @Builder.Default public UUID id = UUID.randomUUID();
@@ -23,6 +23,7 @@ public class AvailableLink implements INBTStorable {
     @Saved @Builder.Default public Set<Integer> groups = new HashSet<>();
     @Saved public UUID replaceDestination;
     @Saved @Builder.Default public int linksRemaining = 1;
+    @Saved @Builder.Default public boolean oneWay = false;
 
     @Override public void readFromNBT(NBTTagCompound nbt) { NBTUtils.readFromNBT(this, nbt); }
     @Override public NBTTagCompound writeToNBT(NBTTagCompound nbt) { return NBTUtils.writeToNBT(this, nbt); }
diff --git a/src/main/java/org/dimdev/dimdoors/shared/rifts/registry/PlayerRiftPointer.java b/src/main/java/org/dimdev/dimdoors/shared/rifts/registry/PlayerRiftPointer.java
new file mode 100644
index 00000000..5ba480c4
--- /dev/null
+++ b/src/main/java/org/dimdev/dimdoors/shared/rifts/registry/PlayerRiftPointer.java
@@ -0,0 +1,13 @@
+package org.dimdev.dimdoors.shared.rifts.registry;
+
+import lombok.AllArgsConstructor;
+import lombok.NoArgsConstructor;
+import org.dimdev.annotatednbt.NBTSerializable;
+import org.dimdev.annotatednbt.Saved;
+
+import java.util.UUID;
+
+@AllArgsConstructor @NoArgsConstructor
+@NBTSerializable public class PlayerRiftPointer extends RegistryVertex {
+    @Saved public UUID player;
+}
diff --git a/src/main/java/org/dimdev/dimdoors/shared/rifts/registry/PocketEntrancePointer.java b/src/main/java/org/dimdev/dimdoors/shared/rifts/registry/PocketEntrancePointer.java
new file mode 100644
index 00000000..3eac9759
--- /dev/null
+++ b/src/main/java/org/dimdev/dimdoors/shared/rifts/registry/PocketEntrancePointer.java
@@ -0,0 +1,12 @@
+package org.dimdev.dimdoors.shared.rifts.registry;
+
+import lombok.AllArgsConstructor;
+import lombok.NoArgsConstructor;
+import org.dimdev.annotatednbt.NBTSerializable;
+import org.dimdev.annotatednbt.Saved;
+
+@AllArgsConstructor @NoArgsConstructor @NBTSerializable
+public class PocketEntrancePointer extends RegistryVertex { // TODO: PocketRiftPointer superclass?
+    @Saved public int pocketDim;
+    @Saved public int pocketId;
+}
diff --git a/src/main/java/org/dimdev/dimdoors/shared/rifts/registry/RegistryVertex.java b/src/main/java/org/dimdev/dimdoors/shared/rifts/registry/RegistryVertex.java
new file mode 100644
index 00000000..f2f823f5
--- /dev/null
+++ b/src/main/java/org/dimdev/dimdoors/shared/rifts/registry/RegistryVertex.java
@@ -0,0 +1,26 @@
+package org.dimdev.dimdoors.shared.rifts.registry;
+
+import org.dimdev.annotatednbt.Saved;
+
+import java.util.UUID;
+
+public abstract class RegistryVertex {
+    public int dim; // The dimension to store this object in. Links are stored in both registries.
+    @Saved public UUID id = UUID.randomUUID(); // Used to create pointers to registry vertices. Should not be used for anything other than saving.
+
+    public void sourceGone(RegistryVertex source) {
+        RiftRegistry.instance().markSubregistryDirty(dim);
+    }
+
+    public void targetGone(RegistryVertex target) {
+        RiftRegistry.instance().markSubregistryDirty(dim);
+    }
+
+    public void sourceAdded(RegistryVertex to) {
+        RiftRegistry.instance().markSubregistryDirty(dim);
+    }
+
+    public void targetAdded(RegistryVertex to) {
+        RiftRegistry.instance().markSubregistryDirty(dim);
+    }
+}
diff --git a/src/main/java/org/dimdev/dimdoors/shared/rifts/registry/Rift.java b/src/main/java/org/dimdev/dimdoors/shared/rifts/registry/Rift.java
new file mode 100644
index 00000000..a18227b7
--- /dev/null
+++ b/src/main/java/org/dimdev/dimdoors/shared/rifts/registry/Rift.java
@@ -0,0 +1,57 @@
+package org.dimdev.dimdoors.shared.rifts.registry;
+
+import lombok.AllArgsConstructor;
+import lombok.NoArgsConstructor;
+import org.dimdev.annotatednbt.NBTSerializable;
+import org.dimdev.annotatednbt.Saved;
+import org.dimdev.ddutils.Location;
+import org.dimdev.dimdoors.shared.rifts.TileEntityRift;
+
+@NoArgsConstructor @AllArgsConstructor
+@NBTSerializable public class Rift extends RegistryVertex {
+    public @Saved Location location;
+    public @Saved boolean isFloating;
+    public @Saved LinkProperties properties;
+    // TODO: receiveDungeonLink
+
+    public Rift(Location location) {
+        this.location = location;
+    }
+
+    @Override
+    public void sourceGone(RegistryVertex source) {
+        super.sourceGone(source);
+        TileEntityRift riftTileEntity = (TileEntityRift) location.getTileEntity();
+        if (source instanceof Rift) {
+            riftTileEntity.sourceGone(((Rift) source).location);
+        }
+        riftTileEntity.updateColor();
+    }
+
+    @Override
+    public void targetGone(RegistryVertex target) {
+        super.targetGone(target);
+        TileEntityRift riftTileEntity = (TileEntityRift) location.getTileEntity();
+        if (target instanceof Rift) {
+            riftTileEntity.targetGone(((Rift) target).location);
+        }
+        riftTileEntity.updateColor();
+    }
+
+    @Override
+    public void sourceAdded(RegistryVertex source) {
+        super.sourceAdded(source);
+        ((TileEntityRift) location.getTileEntity()).updateColor();
+    }
+
+    @Override
+    public void targetAdded(RegistryVertex target) {
+        super.targetAdded(target);
+        ((TileEntityRift) location.getTileEntity()).updateColor();
+    }
+
+    public void markDirty() {
+        RiftRegistry.instance().markSubregistryDirty(dim);
+        ((TileEntityRift) location.getTileEntity()).updateColor();
+    }
+}
diff --git a/src/main/java/org/dimdev/dimdoors/shared/rifts/registry/RiftPlaceholder.java b/src/main/java/org/dimdev/dimdoors/shared/rifts/registry/RiftPlaceholder.java
new file mode 100644
index 00000000..89c3b307
--- /dev/null
+++ b/src/main/java/org/dimdev/dimdoors/shared/rifts/registry/RiftPlaceholder.java
@@ -0,0 +1,3 @@
+package org.dimdev.dimdoors.shared.rifts.registry;
+
+public class RiftPlaceholder extends Rift {} // TODO: don't extend rift
diff --git a/src/main/java/org/dimdev/dimdoors/shared/rifts/registry/RiftRegistry.java b/src/main/java/org/dimdev/dimdoors/shared/rifts/registry/RiftRegistry.java
new file mode 100644
index 00000000..9e53eb1e
--- /dev/null
+++ b/src/main/java/org/dimdev/dimdoors/shared/rifts/registry/RiftRegistry.java
@@ -0,0 +1,416 @@
+package org.dimdev.dimdoors.shared.rifts.registry;
+
+import net.minecraft.nbt.NBTBase;
+import net.minecraft.nbt.NBTTagCompound;
+import net.minecraft.nbt.NBTTagList;
+import net.minecraft.world.storage.MapStorage;
+import net.minecraft.world.storage.WorldSavedData;
+import net.minecraftforge.common.DimensionManager;
+import org.dimdev.ddutils.Location;
+import org.dimdev.ddutils.WorldUtils;
+import org.dimdev.ddutils.nbt.NBTUtils;
+import org.dimdev.dimdoors.DimDoors;
+import org.dimdev.dimdoors.ddutils.GraphUtils;
+import org.dimdev.dimdoors.shared.pockets.Pocket;
+import org.dimdev.dimdoors.shared.pockets.PocketRegistry;
+import org.dimdev.dimdoors.shared.world.ModDimensions;
+import org.jgrapht.graph.DefaultDirectedGraph;
+import org.jgrapht.graph.DefaultEdge;
+
+import java.util.*;
+import java.util.stream.Collectors;
+
+public class RiftRegistry extends WorldSavedData {
+
+    private static final String DATA_NAME = DimDoors.MODID + "_global_rifts"; // TODO: can we use the same name as subregistries?
+    private static final String SUBREGISTRY_DATA_NAME = DimDoors.MODID + "_rifts";
+
+    protected Map<Integer, RiftSubregistry> subregistries = new HashMap<>();
+    protected DefaultDirectedGraph<RegistryVertex, DefaultEdge> graph = new DefaultDirectedGraph<>(DefaultEdge.class);
+    // TODO: add methods that automatically add vertices/edges and mark appropriate subregistries as dirty
+
+    // Caches to avoid looping through vertices to find specific vertices
+    protected Map<Location, Rift> locationMap = new HashMap<>();
+    protected Map<Pocket, PocketEntrancePointer> pocketEntranceMap = new HashMap<>(); // TODO: We're going to want to move all pocket entrance info to the rift registry later to make PocketLib independent of DimDoors.
+    protected Map<UUID, RegistryVertex> uuidMap = new HashMap<>();
+
+    // These are stored in the main registry
+    protected Map<UUID, PlayerRiftPointer> lastPrivatePocketEntrances = new HashMap<>(); // Player UUID -> last rift used to exit pocket
+    protected Map<UUID, PlayerRiftPointer> lastPrivatePocketExits = new HashMap<>(); // Player UUID -> last rift used to enter pocket
+    protected Map<UUID, PlayerRiftPointer> overworldRifts = new HashMap<>(); // Player UUID -> rift used to exit the overworld
+
+    // <editor-fold defaultstate="collapsed" desc="Code for reading/writing/getting the registry">
+
+    public class RiftSubregistry extends WorldSavedData {
+        private int dim;
+
+        public RiftSubregistry() {
+            super(SUBREGISTRY_DATA_NAME);
+        }
+
+        public RiftSubregistry(String s) {
+            super(s);
+        }
+
+        @Override public void readFromNBT(NBTTagCompound nbt) {
+            // Registry is already loaded
+            if (subregistries.get(dim) != null) return;
+
+            // Read rifts in this dimension
+            NBTTagList riftsNBT = (NBTTagList) nbt.getTag("rifts");
+            for (NBTBase riftNBT : riftsNBT) {
+                Rift rift = NBTUtils.readFromNBT(new Rift(), (NBTTagCompound) riftNBT);
+                rift.dim = dim;
+                graph.addVertex(rift);
+                uuidMap.put(rift.id, rift);
+                locationMap.put(rift.location, rift);
+            }
+
+            NBTTagList pocketsNBT = (NBTTagList) nbt.getTag("pockets");
+            for (NBTBase pocketNBT : pocketsNBT) {
+                PocketEntrancePointer pocket = NBTUtils.readFromNBT(new PocketEntrancePointer(), (NBTTagCompound) pocketNBT);
+                pocket.dim = dim;
+                graph.addVertex(pocket);
+                uuidMap.put(pocket.id, pocket);
+                pocketEntranceMap.put(PocketRegistry.instance(pocket.dim).getPocket(pocket.pocketId), pocket);
+            }
+
+            // Read the connections between links that have a source or destination in this dimension
+            NBTTagList linksNBT = (NBTTagList) nbt.getTag("links");
+            for (NBTBase linkNBT : linksNBT) {
+                RegistryVertex from = uuidMap.get(((NBTTagCompound) linkNBT).getUniqueId("from"));
+                RegistryVertex to = uuidMap.get(((NBTTagCompound) linkNBT).getUniqueId("to"));
+                if (from != null && to != null) {
+                    graph.addEdge(from, to);
+                    // We need a system for detecting links that are incomplete after processing them in the other subregistry too
+                }
+            }
+        }
+
+        // Even though it seems like we could loop only once over the vertices and edges (in the RiftRegistry's writeToNBT
+        // method rather than RiftSubregistry) and save each in the appropriate registry, we can't do this because it is not
+        // always the case that all worlds will be saved at once.
+        @Override public NBTTagCompound writeToNBT(NBTTagCompound nbt) {
+            // Write rifts in this dimension
+            NBTTagList riftsNBT = new NBTTagList();
+            NBTTagList pocketsNBT = new NBTTagList();
+            for (RegistryVertex vertex : graph.vertexSet()) {
+                if (vertex.dim == dim) {
+                    NBTTagCompound vertexNBT = NBTUtils.writeToNBT(vertex, new NBTTagCompound());
+                    if (vertex instanceof Rift) {
+                        riftsNBT.appendTag(vertexNBT);
+                    } else if (vertex instanceof PocketEntrancePointer) {
+                        pocketsNBT.appendTag(vertexNBT);
+                    } else if (!(vertex instanceof PlayerRiftPointer)) {
+                        throw new RuntimeException("Unsupported registry vertex type " + vertex.getClass().getName());
+                    }
+                }
+            }
+            nbt.setTag("rifts", riftsNBT);
+            nbt.setTag("pockets", pocketsNBT);
+
+            // Write the connections between links that have a source or destination in this dimension
+            NBTTagList linksNBT = new NBTTagList();
+            for (DefaultEdge edge : graph.edgeSet()) {
+                RegistryVertex from = graph.getEdgeSource(edge);
+                RegistryVertex to = graph.getEdgeTarget(edge);
+                if (from.dim == dim || to.dim == dim && !(from instanceof PlayerRiftPointer)) {
+                    NBTTagCompound linkNBT = new NBTTagCompound();
+                    linkNBT.setUniqueId("from", from.id);
+                    linkNBT.setUniqueId("to", to.id);
+                    NBTUtils.writeToNBT(edge, linkNBT); // Write in both registries, we might want to notify when there's a missing world later
+                    linksNBT.appendTag(linkNBT);
+                }
+            }
+            nbt.setTag("links", riftsNBT);
+
+            return nbt;
+        }
+    }
+
+    public RiftRegistry() {
+        super(DATA_NAME);
+    }
+
+    public RiftRegistry(String s) {
+        super(s);
+    }
+
+    public static RiftRegistry instance() {
+        MapStorage storage = WorldUtils.getWorld(0).getMapStorage();
+        RiftRegistry instance = (RiftRegistry) storage.getOrLoadData(RiftRegistry.class, DATA_NAME);
+
+        if (instance == null) {
+            instance = new RiftRegistry();
+            storage.setData(DATA_NAME, instance);
+        }
+
+        return instance;
+    }
+
+    @Override
+    public void readFromNBT(NBTTagCompound nbt) {
+        // Trigger the subregistry reading code for all dimensions. It would be better if there was some way of forcing
+        // them to be read from somewhere else, since this is technically more than just reading the NBT. This has to be
+        // done last since links are only in the subregistries.
+        // TODO: If non-dirty but new WorldSavedDatas aren't automatically saved, then create the subregistries here
+        // TODO: rather then in the markSubregistryDirty method.
+        for (int dim : DimensionManager.getStaticDimensionIDs()) {
+            MapStorage storage = WorldUtils.getWorld(dim).getPerWorldStorage();
+            RiftSubregistry instance = (RiftSubregistry) storage.getOrLoadData(RiftSubregistry.class, SUBREGISTRY_DATA_NAME);
+            if (instance != null) {
+                instance.dim = dim;
+                subregistries.put(dim, instance);
+            }
+        }
+
+        // Read player to rift maps (this has to be done after the uuidMap has been filled by the subregistry code)
+        lastPrivatePocketEntrances = readPlayerRiftPointers((NBTTagList) nbt.getTag("lastPrivatePocketEntrances"));
+        lastPrivatePocketExits = readPlayerRiftPointers((NBTTagList) nbt.getTag("lastPrivatePocketExits"));
+        overworldRifts = readPlayerRiftPointers((NBTTagList) nbt.getTag("overworldRifts"));
+    }
+
+    @Override
+    public NBTTagCompound writeToNBT(NBTTagCompound nbt) {
+        // Subregistries are written automatically when the worlds are saved.
+        nbt.setTag("lastPrivatePocketEntrances", writePlayerRiftPointers(lastPrivatePocketEntrances));
+        nbt.setTag("lastPrivatePocketExits", writePlayerRiftPointers(lastPrivatePocketExits));
+        nbt.setTag("overworldRifts", writePlayerRiftPointers(overworldRifts));
+        return nbt;
+    }
+
+    private Map<UUID, PlayerRiftPointer> readPlayerRiftPointers(NBTTagList playerRiftPointersNBT) {
+        Map<UUID, PlayerRiftPointer> pointerMap = new HashMap<>();
+        for (NBTBase entryNBT : playerRiftPointersNBT) {
+            UUID player = ((NBTTagCompound) entryNBT).getUniqueId("player");
+            UUID rift = ((NBTTagCompound) entryNBT).getUniqueId("rift");
+            PlayerRiftPointer pointer = new PlayerRiftPointer(player);
+            pointerMap.put(player, pointer);
+            uuidMap.put(pointer.id, pointer);
+            graph.addVertex(pointer);
+            graph.addEdge(pointer, uuidMap.get(rift));
+        }
+        return pointerMap;
+    }
+
+    private NBTTagList writePlayerRiftPointers(Map<UUID, PlayerRiftPointer> playerRiftPointerMap) {
+        NBTTagList pointers = new NBTTagList();
+        for (Map.Entry<UUID, PlayerRiftPointer> entry : playerRiftPointerMap.entrySet()) {
+            NBTTagCompound entryNBT = new NBTTagCompound();
+            entryNBT.setUniqueId("player", entry.getKey());
+            int count = 0;
+            for (DefaultEdge edge : graph.outgoingEdgesOf(entry.getValue())) {
+                entryNBT.setUniqueId("rift", graph.getEdgeTarget(edge).id);
+                count++;
+            }
+            if (count != 1) throw new RuntimeException("PlayerRiftPointer points to more than one rift");
+            pointers.appendTag(entryNBT);
+        }
+        return pointers;
+    }
+
+    public void markSubregistryDirty(int dim) {
+        RiftSubregistry subregistry = subregistries.get(dim);
+        if (subregistry != null) {
+            subregistry.markDirty();
+        } else {
+            // Create the subregistry
+            MapStorage storage = WorldUtils.getWorld(dim).getPerWorldStorage();
+            RiftSubregistry instance = new RiftSubregistry();
+            instance.dim = dim;
+            instance.markDirty();
+            storage.setData(SUBREGISTRY_DATA_NAME, instance);
+            subregistries.put(dim, instance);
+        }
+    }
+
+    // </editor-fold>
+
+    public boolean isRiftAt(Location location) {
+        return locationMap.get(location) != null;
+    }
+
+    public Rift getRift(Location location) {
+        Rift rift = locationMap.get(location);
+        if (rift == null) throw new IllegalArgumentException("There is no rift registered at " + location);
+        return rift;
+    }
+
+    public void addRift(Location location) {
+        DimDoors.log.info("Adding rift at " + location);
+        RegistryVertex currentRift = getRift(location);
+        Rift rift;
+        if (currentRift instanceof RiftPlaceholder) {
+            rift = new Rift(location);
+            rift.dim = location.getDim();
+            rift.id = currentRift.id;
+            GraphUtils.replaceVertex(graph, currentRift, rift);
+        } else if (currentRift == null) {
+            rift = new Rift(location);
+            rift.dim = location.getDim();
+            graph.addVertex(rift);
+        } else {
+            throw new IllegalArgumentException("There is already a rift registered at " + location);
+        }
+        uuidMap.put(rift.id, rift);
+        locationMap.put(location, rift);
+        rift.markDirty();
+    }
+
+    public void removeRift(Location location) {
+        DimDoors.log.info("Removing rift at " + location);
+
+        Rift rift = getRift(location);
+
+        // Notify the adjacent vertices of the change
+        for (DefaultEdge edge : graph.incomingEdgesOf(rift)) graph.getEdgeSource(edge).targetGone(rift);
+        for (DefaultEdge edge : graph.outgoingEdgesOf(rift)) graph.getEdgeTarget(edge).sourceGone(rift);
+
+        graph.removeVertex(rift);
+        locationMap.remove(location);
+        uuidMap.remove(rift.id);
+        rift.markDirty();
+    }
+
+    private void addEdge(RegistryVertex from, RegistryVertex to) {
+        graph.addEdge(from, to);
+        if (from instanceof PlayerRiftPointer) {
+            markDirty();
+        } else {
+            markSubregistryDirty(from.dim);
+        }
+        markSubregistryDirty(to.dim);
+    }
+
+    public void addLink(Location locationFrom, Location locationTo) {
+        DimDoors.log.info("Adding link " + locationFrom + " -> " + locationTo);
+        Rift from = getRift(locationFrom);
+
+        Rift to = getRift(locationTo);
+
+        addEdge(from, to);
+
+        // Notify the linked vertices of the change
+        from.targetAdded(to);
+        to.sourceAdded(from);
+    }
+
+    public void removeLink(Location locationFrom, Location locationTo) {
+        DimDoors.log.info("Removing link " + locationFrom + " -> " + locationTo);
+
+        Rift from = getRift(locationFrom);
+        Rift to = getRift(locationTo);
+
+        addEdge(from, to);
+
+        // Notify the linked vertices of the change
+        from.targetGone(to);
+        to.sourceGone(from);
+    }
+
+    public void setProperties(Location location, LinkProperties properties) {
+        DimDoors.log.info("Setting DungeonLinkProperties for rift at " + location + " to " + properties);
+        Rift rift = getRift(location);
+        rift.properties = properties;
+        rift.markDirty();
+    }
+
+    public Set<Location> getPocketEntrances(Pocket pocket) {
+        PocketEntrancePointer pointer = pocketEntranceMap.get(pocket);
+        if (pointer == null) {
+            return Collections.emptySet();
+        } else {
+            return graph.outgoingEdgesOf(pointer).stream()
+                    .map(graph::getEdgeTarget)
+                    .map(Rift.class::cast)
+                    .map(rift -> rift.location)
+                    .collect(Collectors.toSet());
+        }
+    }
+
+    public void addPocketEntrance(Pocket pocket, Location location) {
+        DimDoors.log.info("Adding pocket entrance for pocket " + pocket.getId() + " in dimension " + pocket.getDim() + " at " + location);
+        PocketEntrancePointer pointer = pocketEntranceMap.get(pocket);
+        if (pointer == null) {
+            pointer = new PocketEntrancePointer(pocket.getDim(), pocket.getId());
+            pointer.dim = pocket.getDim();
+            graph.addVertex(pointer);
+            pocketEntranceMap.put(pocket, pointer);
+            uuidMap.put(pointer.id, pointer);
+        }
+        Rift rift = getRift(location);
+        addEdge(pointer, rift);
+    }
+
+    public Location getPrivatePocketEntrance(UUID playerUUID) {
+        // Try to get the last used entrance
+        PlayerRiftPointer entrancePointer = lastPrivatePocketEntrances.get(playerUUID);
+        Rift entrance = (Rift) GraphUtils.followPointer(graph, entrancePointer);
+        if (entrance != null) return entrance.location;
+
+        // If there was no last used private entrance, get one of the player's private pocket entrances
+        PocketRegistry privatePocketRegistry = PocketRegistry.instance(ModDimensions.getPrivateDim());
+        Pocket pocket = privatePocketRegistry.getPocket(privatePocketRegistry.getPrivatePocketID(playerUUID));
+        return getPocketEntrances(pocket).stream().findFirst().orElse(null);
+    }
+
+    private void setPlayerRiftPointer(UUID playerUUID, Location rift, Map<UUID, PlayerRiftPointer> map) {
+        PlayerRiftPointer pointer = map.get(playerUUID);
+        if (pointer == null) {
+            pointer = new PlayerRiftPointer(playerUUID);
+            graph.addVertex(pointer);
+            map.put(playerUUID, pointer);
+            uuidMap.put(pointer.id, pointer);
+        } else {
+            graph.removeAllEdges(graph.outgoingEdgesOf(pointer));
+        }
+        addEdge(pointer, getRift(rift));
+    }
+
+    public void setLastPrivatePocketEntrance(UUID playerUUID, Location rift) {
+        DimDoors.log.info("Setting last used private pocket entrance for " + playerUUID + " at " + rift);
+        setPlayerRiftPointer(playerUUID, rift, lastPrivatePocketEntrances);
+    }
+
+    public Location getPrivatePocketExit(UUID playerUUID) {
+        PlayerRiftPointer entrancePointer = lastPrivatePocketExits.get(playerUUID);
+        Rift entrance = (Rift) GraphUtils.followPointer(graph, entrancePointer);
+        return entrance.location;
+    }
+
+    public void setLastPrivatePocketExit(UUID playerUUID, Location rift) {
+        DimDoors.log.info("Setting last used private pocket entrance for " + playerUUID + " at " + rift);
+        setPlayerRiftPointer(playerUUID, rift, lastPrivatePocketExits);
+    }
+
+    public Location getOverworldRift(UUID playerUUID) {
+        PlayerRiftPointer entrancePointer = overworldRifts.get(playerUUID);
+        Rift entrance = (Rift) GraphUtils.followPointer(graph, entrancePointer);
+        return entrance.location;
+    }
+
+    public void setOverworldRift(UUID playerUUID, Location rift) {
+        DimDoors.log.info("Setting last used private pocket entrance for " + playerUUID + " at " + rift);
+        setPlayerRiftPointer(playerUUID, rift, overworldRifts);
+    }
+
+    public Collection<Rift> getRifts() {
+        return locationMap.values();
+    }
+
+    public Set<Location> getTargets(Location location) {
+        return graph.outgoingEdgesOf(locationMap.get(location)).stream()
+                .map(graph::getEdgeTarget)
+                .map(Rift.class::cast)
+                .map(rift -> rift.location)
+                .collect(Collectors.toSet());
+    }
+
+    public Set<Location> getSources(Location location) {
+        return graph.incomingEdgesOf(locationMap.get(location)).stream()
+                .map(graph::getEdgeTarget)
+                .map(Rift.class::cast)
+                .map(rift -> rift.location)
+                .collect(Collectors.toSet());
+    }
+}
diff --git a/src/main/java/org/dimdev/dimdoors/shared/tools/PocketSchematicGenerator.java b/src/main/java/org/dimdev/dimdoors/shared/tools/PocketSchematicGenerator.java
index d7a82330..322ab506 100644
--- a/src/main/java/org/dimdev/dimdoors/shared/tools/PocketSchematicGenerator.java
+++ b/src/main/java/org/dimdev/dimdoors/shared/tools/PocketSchematicGenerator.java
@@ -10,6 +10,7 @@ import org.dimdev.dimdoors.shared.rifts.*;
 import org.dimdev.dimdoors.shared.rifts.destinations.PocketEntranceDestination;
 import org.dimdev.dimdoors.shared.rifts.destinations.PocketExitDestination;
 import org.dimdev.dimdoors.shared.rifts.destinations.PrivatePocketExitDestination;
+import org.dimdev.dimdoors.shared.rifts.registry.LinkProperties;
 import org.dimdev.dimdoors.shared.tileentities.TileEntityEntranceRift;
 import org.dimdev.ddutils.schem.Schematic;
 import net.minecraft.block.BlockDoor;
@@ -30,10 +31,7 @@ import java.io.DataOutputStream;
 import java.io.File;
 import java.io.FileOutputStream;
 import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.List;
+import java.util.*;
 
 /**
  * @author Robijnvogel
@@ -158,10 +156,15 @@ public final class PocketSchematicGenerator {
         // Generate the rift TileEntities
         schematic.tileEntities = new ArrayList<>();
         TileEntityEntranceRift rift = (TileEntityEntranceRift) doorBlock.createTileEntity(null, doorBlock.getDefaultState());
-        rift.setSingleDestination(PocketEntranceDestination.builder()
-                .ifDestinations(Collections.singletonList(new WeightedRiftDestination(exitDest, 1, 0)))
+        rift.setDestination(PocketEntranceDestination.builder()
+                .ifDestination(exitDest)
+                .build());
+        rift.setProperties(LinkProperties.builder()
+                .groups(Collections.singleton(1))
+                .linksRemaining(1)
+                .entranceWeight(chaosWeight)
+                .floatingWeight(chaosWeight)
                 .build());
-        rift.setChaosWeight(chaosWeight);
 
         rift.setPlaceRiftOnBreak(true);
         NBTTagCompound tileNBT = rift.serializeNBT();
diff --git a/src/main/java/org/dimdev/dimdoors/shared/world/limbodimension/BiomeLimbo.java b/src/main/java/org/dimdev/dimdoors/shared/world/limbodimension/BiomeLimbo.java
index 57bedd2c..118ee0fa 100644
--- a/src/main/java/org/dimdev/dimdoors/shared/world/limbodimension/BiomeLimbo.java
+++ b/src/main/java/org/dimdev/dimdoors/shared/world/limbodimension/BiomeLimbo.java
@@ -4,7 +4,6 @@ import org.dimdev.dimdoors.shared.entities.EntityMonolith;
 import net.minecraft.util.math.BlockPos;
 import net.minecraft.world.World;
 import net.minecraft.world.biome.Biome;
-import net.minecraft.world.biome.BiomeDecorator;
 import net.minecraft.world.chunk.ChunkPrimer;
 import net.minecraftforge.fml.relauncher.Side;
 import net.minecraftforge.fml.relauncher.SideOnly;
diff --git a/src/main/java/org/dimdev/dimdoors/shared/world/pocketdimension/BiomeBlank.java b/src/main/java/org/dimdev/dimdoors/shared/world/pocketdimension/BiomeBlank.java
index f812661e..c4b16714 100644
--- a/src/main/java/org/dimdev/dimdoors/shared/world/pocketdimension/BiomeBlank.java
+++ b/src/main/java/org/dimdev/dimdoors/shared/world/pocketdimension/BiomeBlank.java
@@ -1,6 +1,5 @@
 package org.dimdev.dimdoors.shared.world.pocketdimension;
 
-import org.dimdev.dimdoors.shared.entities.EntityMonolith;
 import net.minecraft.init.Blocks;
 import net.minecraft.util.math.BlockPos;
 import net.minecraft.world.World;