diff --git a/api/src/main/java/me/tofaa/entitylib/utils/Check.java b/api/src/main/java/me/tofaa/entitylib/utils/Check.java
index 478ed49..8dcf013 100644
--- a/api/src/main/java/me/tofaa/entitylib/utils/Check.java
+++ b/api/src/main/java/me/tofaa/entitylib/utils/Check.java
@@ -1,5 +1,6 @@
 package me.tofaa.entitylib.utils;
 
+import com.github.retrooper.packetevents.protocol.world.Location;
 import org.jetbrains.annotations.Contract;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
@@ -15,6 +16,12 @@ public final class Check {
 
     private Check() {}
 
+    public static boolean inChunk(Location location, int cx, int cz) {
+        // Assumes each chunk is 16x
+        int lx = ((int) Math.floor(location.getX())) >> 4;
+        int lz = ((int) Math.floor(location.getZ())) >> 4;
+        return cx == lx && cz == lz;
+    }
 
     public static <T> void arrayLength(List<T> lines, int index, T e) {
         if (index >= lines.size()) {
diff --git a/api/src/main/java/me/tofaa/entitylib/ve/ViewerEngine.java b/api/src/main/java/me/tofaa/entitylib/ve/ViewerEngine.java
new file mode 100644
index 0000000..fb2a059
--- /dev/null
+++ b/api/src/main/java/me/tofaa/entitylib/ve/ViewerEngine.java
@@ -0,0 +1,86 @@
+package me.tofaa.entitylib.ve;
+
+
+import me.tofaa.entitylib.EntityLib;
+import me.tofaa.entitylib.wrapper.WrapperEntity;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.jetbrains.annotations.UnmodifiableView;
+
+import java.util.*;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+
+public class ViewerEngine {
+
+    private final List<ViewerRule> globalRules;
+    private final Set<WrapperEntity> tracked;
+    private final ViewerEngineListener listener;
+    private Executor executor;
+
+    public ViewerEngine() {
+        this.globalRules = new CopyOnWriteArrayList<>();
+        this.tracked = Collections.newSetFromMap(new WeakHashMap<>());
+        this.executor = Executors.newSingleThreadExecutor();
+        this.listener = new ViewerEngineListener(this);
+    }
+
+    public void enable() {
+        EntityLib.getApi().getPacketEvents().getEventManager().registerListener(listener);
+    }
+    public void disable() {
+        EntityLib.getApi().getPacketEvents().getEventManager().unregisterListener(listener);
+    }
+
+    public Executor getExecutor() {
+        return executor;
+    }
+
+    public void setExecutor(Executor executor) {
+        this.executor = executor;
+    }
+
+    public void track(@NotNull WrapperEntity entity) {
+        tracked.add(entity);
+    }
+
+    public void clearTracked() {
+        tracked.clear();
+    }
+
+    public @UnmodifiableView Collection<WrapperEntity> getTracked() {
+        return Collections.unmodifiableCollection(tracked);
+    }
+
+    Set<WrapperEntity> getTracked0() {
+        return tracked;
+    }
+
+    public void addViewerRule(@NotNull ViewerRule rule) {
+        this.globalRules.add(rule);
+    }
+
+    public void removeViewerRule(@NotNull ViewerRule rule) {
+        this.globalRules.remove(rule);
+    }
+
+    public void removeViewerRule(int index) {
+        this.globalRules.remove(index);
+    }
+
+    public void clearViewerRules() {
+        this.globalRules.clear();
+    }
+
+    public @UnmodifiableView Collection<ViewerRule> getViewerRules() {
+        return Collections.unmodifiableCollection(globalRules);
+    }
+
+    public @Nullable ViewerRule getViewerRule(int index) {
+        if (this.globalRules.size() >= index - 1) return null;
+        if (index < 0) return null;
+        return globalRules.get(index);
+    }
+
+}
diff --git a/api/src/main/java/me/tofaa/entitylib/ve/ViewerEngineListener.java b/api/src/main/java/me/tofaa/entitylib/ve/ViewerEngineListener.java
new file mode 100644
index 0000000..42de5ac
--- /dev/null
+++ b/api/src/main/java/me/tofaa/entitylib/ve/ViewerEngineListener.java
@@ -0,0 +1,60 @@
+package me.tofaa.entitylib.ve;
+
+import com.github.retrooper.packetevents.event.PacketListenerAbstract;
+import com.github.retrooper.packetevents.event.PacketSendEvent;
+import com.github.retrooper.packetevents.protocol.packettype.PacketType;
+import com.github.retrooper.packetevents.protocol.packettype.PacketTypeCommon;
+import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerUnloadChunk;
+import me.tofaa.entitylib.utils.Check;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+
+final class ViewerEngineListener extends PacketListenerAbstract {
+
+    private final ViewerEngine engine;
+
+    ViewerEngineListener(ViewerEngine engine) {
+        this.engine = engine;
+    }
+
+    @Override
+    public void onPacketSend(PacketSendEvent event) {
+        PacketTypeCommon type = event.getPacketType();
+        if (type == PacketType.Play.Server.UNLOAD_CHUNK) {
+            PacketSendEvent copy = event.clone();
+            engine.getExecutor().execute(() -> {
+                WrapperPlayServerUnloadChunk packet = new WrapperPlayServerUnloadChunk(event);
+                int chunkX = packet.getChunkX();
+                int chunkZ = packet.getChunkZ();
+                engine.getTracked0().forEach(entity -> {
+                    if (!Check.inChunk(entity.getLocation(), chunkX, chunkZ)) return;
+                    entity.removeViewer(event.getUser());
+                });
+                copy.cleanUp();
+            });
+        }
+        if (type == PacketType.Play.Server.CHUNK_DATA) {
+            PacketSendEvent copy = event.clone();
+            engine.getExecutor().execute(() -> {
+                WrapperPlayServerUnloadChunk packet = new WrapperPlayServerUnloadChunk(event);
+                int chunkX = packet.getChunkX();
+                int chunkZ = packet.getChunkZ();
+                engine.getTracked0().forEach(entity -> {
+                    if (!Check.inChunk(entity.getLocation(), chunkX, chunkZ)) return;
+                    if (entity.hasViewer(event.getUser())) return;
+                    AtomicBoolean pass = new AtomicBoolean(false);
+                    entity.getViewerRules().forEach(rule -> {
+                        pass.set(rule.shouldSee(event.getUser()));
+                    });
+                    engine.getViewerRules().forEach(rule -> {
+                        pass.set(rule.shouldSee(event.getUser()));
+                    });
+                    if (!pass.get()) return;
+                    entity.addViewer(event.getUser());
+                });
+                copy.cleanUp();
+            });
+        }
+    }
+
+}
diff --git a/api/src/main/java/me/tofaa/entitylib/ve/ViewerRule.java b/api/src/main/java/me/tofaa/entitylib/ve/ViewerRule.java
new file mode 100644
index 0000000..61431e6
--- /dev/null
+++ b/api/src/main/java/me/tofaa/entitylib/ve/ViewerRule.java
@@ -0,0 +1,11 @@
+package me.tofaa.entitylib.ve;
+
+
+import com.github.retrooper.packetevents.protocol.player.User;
+
+@FunctionalInterface
+public interface ViewerRule {
+
+    boolean shouldSee(User user);
+
+}
diff --git a/api/src/main/java/me/tofaa/entitylib/wrapper/WrapperEntity.java b/api/src/main/java/me/tofaa/entitylib/wrapper/WrapperEntity.java
index e40db7b..206f594 100644
--- a/api/src/main/java/me/tofaa/entitylib/wrapper/WrapperEntity.java
+++ b/api/src/main/java/me/tofaa/entitylib/wrapper/WrapperEntity.java
@@ -12,12 +12,15 @@ import me.tofaa.entitylib.container.EntityContainer;
 import me.tofaa.entitylib.meta.EntityMeta;
 import me.tofaa.entitylib.meta.types.ObjectData;
 import me.tofaa.entitylib.tick.Tickable;
+import me.tofaa.entitylib.ve.ViewerRule;
 import net.kyori.adventure.text.Component;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
+import org.jetbrains.annotations.UnmodifiableView;
 
 import java.util.*;
 import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CopyOnWriteArrayList;
 import java.util.function.Consumer;
 
 public class WrapperEntity implements Tickable, TrackedEntity {
@@ -36,6 +39,7 @@ public class WrapperEntity implements Tickable, TrackedEntity {
     private int riding = -1;
     private final Set<Integer> passengers;
     private EntityContainer parent;
+    private final List<ViewerRule> viewerRules;
 
     public WrapperEntity(int entityId, UUID uuid, EntityType entityType, EntityMeta entityMeta) {
         this.entityId = entityId;
@@ -46,6 +50,7 @@ public class WrapperEntity implements Tickable, TrackedEntity {
         this.viewers = ConcurrentHashMap.newKeySet();
         this.passengers = ConcurrentHashMap.newKeySet();
         this.location = new Location(0, 0, 0, 0, 0);
+        this.viewerRules = new CopyOnWriteArrayList<>();
     }
 
     public WrapperEntity(int entityId, EntityType entityType) {
@@ -353,6 +358,31 @@ public class WrapperEntity implements Tickable, TrackedEntity {
         return new WrapperPlayServerSetPassengers(entityId, passengers.stream().mapToInt(i -> i).toArray());
     }
 
+    public @UnmodifiableView Collection<ViewerRule> getViewerRules() {
+        return Collections.unmodifiableCollection(viewerRules);
+    }
+
+    public void addViewerRule(@NotNull ViewerRule rule) {
+        this.viewerRules.add(rule);
+    }
+
+    public void removeViewerRule(@NotNull ViewerRule rule) {
+        this.viewerRules.remove(rule);
+    }
+
+    public void removeViewerRule(int index) {
+        this.viewerRules.remove(index);
+    }
+
+    public void clearViewerRules() {
+        this.viewerRules.clear();
+    }
+
+    public @Nullable ViewerRule getViewerRule(int index) {
+        if (this.viewerRules.size() >= index - 1) return null;
+        if (index < 0) return null;
+        return viewerRules.get(index);
+    }
 
     private WrapperPlayServerEntityVelocity getVelocityPacket() {
         Vector3d velocity = this.velocity.multiply(8000.0f / 20.0f);
@@ -578,6 +608,14 @@ public class WrapperEntity implements Tickable, TrackedEntity {
         return Collections.unmodifiableSet(viewers);
     }
 
+    public boolean hasViewer(UUID uuid) {
+        return viewers.contains(uuid);
+    }
+
+    public boolean hasViewer(User user) {
+        return hasViewer(user.getUUID());
+    }
+
     public Location getLocation() {
         return location;
     }