diff --git a/src/main/java/io/luna/game/model/map/DynamicMap.java b/src/main/java/io/luna/game/model/map/DynamicMap.java
new file mode 100644
index 00000000..0226e359
--- /dev/null
+++ b/src/main/java/io/luna/game/model/map/DynamicMap.java
@@ -0,0 +1,266 @@
+package io.luna.game.model.map;
+
+import io.luna.LunaContext;
+import io.luna.game.model.Entity;
+import io.luna.game.model.EntityType;
+import io.luna.game.model.Position;
+import io.luna.game.model.World;
+import io.luna.game.model.chunk.Chunk;
+import io.luna.game.model.chunk.ChunkRepository;
+import io.luna.game.model.item.GroundItem;
+import io.luna.game.model.map.builder.DynamicMapBuilder;
+import io.luna.game.model.map.builder.DynamicMapChunk;
+import io.luna.game.model.map.builder.DynamicMapPalette;
+import io.luna.game.model.mob.Npc;
+import io.luna.game.model.mob.Player;
+import io.luna.game.model.mob.controller.ControllerKey;
+import io.luna.game.model.object.GameObject;
+import io.luna.net.msg.out.DynamicRegionMessageWriter;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * A dynamically constructed map; also known as an instance, somewhere in the rs2 world. These can be used for things
+ * like minigames, private areas, random events, and player cutscenes.
+ *
+ * Dynamic maps can be constructed simply by using specialized functions within {@link DynamicMapBuilder}, while
+ * {@link DynamicMapPalette} allows for finer control over chunk placement in the palette.
+ *
+ * All instances are assigned empty space somewhere in the world, which is reclaimed when the instance is no longer
+ * needed. This ensures that instances are always isolated and that empty space is always available.
+ *
+ * @author lare96
+ */
+public final class DynamicMap {
+
+ /**
+ * The world instance.
+ */
+ private final World world;
+
+ /**
+ * The base real chunk that will be used to translate coordinates from the real map to this instanced map.
+ */
+ private final Chunk baseChunk;
+
+ /**
+ * The palette of this map.
+ */
+ private final DynamicMapPalette palette;
+
+ /**
+ * The key of the controller for this instance.
+ */
+ private final ControllerKey> controllerKey;
+
+ /**
+ * The region update message that will be sent.
+ */
+ private final DynamicRegionMessageWriter regionUpdateMsg;
+
+ /**
+ * The players within this instance.
+ */
+ private final Set players = new LinkedHashSet<>();
+
+ /**
+ * The empty space that this instance is assigned to.
+ */
+ private DynamicMapSpace assignedSpace;
+
+ /**
+ * Creates a new {@link DynamicMap}.
+ */
+ public DynamicMap(LunaContext context, Chunk baseChunk,
+ DynamicMapPalette palette, ControllerKey extends DynamicMapController> controllerKey,
+ DynamicRegionMessageWriter regionUpdateMsg) {
+ this.baseChunk = baseChunk;
+ this.palette = palette;
+ this.controllerKey = controllerKey;
+ this.regionUpdateMsg = regionUpdateMsg;
+ world = context.getWorld();
+ }
+
+ /**
+ * Creates this instance by requesting empty space, and transferring all map data over to the empty space.
+ */
+ public void create() {
+ // Request empty space from the pool.
+ assignedSpace = world.getDynamicMapSpacePool().request();
+
+ // Get the base chunks from the palette.
+ Set realChunks = new HashSet<>();
+ palette.forEach((palette, x, y, z) -> {
+ DynamicMapChunk mapChunk = palette.getChunk(x, y, z);
+ if(mapChunk != null) {
+ realChunks.add(mapChunk.getChunk());
+ }
+ });
+
+ // Now add the static objects in the real map to our instanced map.
+ for (Chunk chunk : realChunks) {
+ ChunkRepository repository = world.getChunks().load(chunk);
+ repository.getAll(EntityType.OBJECT).stream().map(it -> (GameObject) it).
+ filter(it -> !it.isDynamic()).forEach(it -> {
+ GameObject instanceObject = GameObject.createStatic(world.getContext(), it.getId(),
+ getInstancePosition(it.getPosition()), it.getObjectType(), it.getDirection());
+ world.getObjects().register(instanceObject);
+ });
+ }
+ }
+
+ /**
+ * Attempts to add {@code plr} to this instance.
+ *
+ * @param plr The player to add.
+ */
+ public boolean join(Player plr) {
+ if (players.add(plr)) {
+ plr.setDynamicMap(this);
+ plr.getControllers().register(controllerKey);
+ plr.queue(regionUpdateMsg);
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Attempts to remove {@code plr} from this instance.
+ *
+ * @param plr The player to remove.
+ */
+ public boolean leave(Player plr) {
+ if (players.remove(plr)) {
+ Position oldPosition = plr.getPosition();
+ plr.setLastRegion(null);
+ plr.getControllers().unregister(controllerKey);
+ plr.setDynamicMap(null);
+ plr.sendRegionUpdate(oldPosition);
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Deletes this instance by forcing all players to leave, and clearing the area of entities.
+ */
+ public void delete() {
+ // Force all players to leave.
+ Set removePlayers = new HashSet<>(players);
+ for (Player player : removePlayers) {
+ leave(player);
+ }
+
+ // Clear all entities.
+ int mainRegionId = assignedSpace.getMain().getId();
+ int paddingRegionId = assignedSpace.getPadding().getId();
+
+ Set clearChunks = new HashSet<>();
+ clearChunks.addAll(DynamicMapPalette.getAllChunksInRegion(mainRegionId));
+ clearChunks.addAll(DynamicMapPalette.getAllChunksInRegion(paddingRegionId));
+
+ List removalActions = new ArrayList<>();
+ for (Chunk chunk : clearChunks) {
+ ChunkRepository repository = world.getChunks().load(chunk);
+ for (var next : repository.getAll().entrySet()) {
+ EntityType type = next.getKey();
+ Set entities = next.getValue();
+ for (Entity entity : entities) {
+ switch (type) {
+ case ITEM:
+ removalActions.add(() -> world.getItems().unregister((GroundItem) entity));
+ break;
+ case NPC:
+ removalActions.add(() -> world.getNpcs().remove((Npc) entity));
+ break;
+ case OBJECT:
+ removalActions.add(() -> world.getObjects().unregister((GameObject) entity));
+ break;
+ }
+ }
+ }
+ removalActions.add(repository::clear);
+ }
+
+ // Finalize removals.
+ for (Runnable action : removalActions) {
+ action.run();
+ }
+
+ // Return empty space back to pool.
+ world.getDynamicMapSpacePool().release(this);
+ }
+
+ /**
+ * Retrieves the {@link Position} in this instance that mirrors the actual coordinate.
+ *
+ * @param actualPosition The real position to get the instance position of.
+ * @return The instance position.
+ */
+ public Position getInstancePosition(Position actualPosition) {
+ // Determine the deltas between a real arbitrary base position and the base display chunk. We can use
+ // these deltas later to translate from our instance position the exact same way.
+ Position basePosition = baseChunk.getAbsPosition();
+ int deltaX = actualPosition.getX() - basePosition.getX();
+ int deltaY = actualPosition.getY() - basePosition.getY();
+
+ // The base position in our instance will always be the same spot as base display chunk. Therefore we can do a
+ // 1:1 translation using the previous deltas.
+ return assignedSpace.getMain().getBasePosition().translate(deltaX, deltaY);
+ }
+
+ /**
+ * Forwards to {@link DynamicMap#getInstancePosition(Position)} with a {@code z} value of 0.
+ *
+ * @param x The real {@code x}.
+ * @param y The real {@code y}.
+ * @return The instance position.
+ */
+ public Position getInstancePosition(int x, int y) {
+ return getInstancePosition(x, y, 0);
+ }
+
+ /**
+ * Forwards to {@link DynamicMap#getInstancePosition(Position)}.
+ *
+ * @param x The real {@code x}.
+ * @param y The real {@code y}.
+ * @param z The real {@code z}.
+ * @return The instance position.
+ */
+ public Position getInstancePosition(int x, int y, int z) {
+ return getInstancePosition(new Position(x, y, z));
+ }
+
+ /**
+ * @return The empty space that this instance is assigned to.
+ */
+ public DynamicMapSpace getAssignedSpace() {
+ return assignedSpace;
+ }
+
+ /**
+ * @return The base real chunk that will be used to translate coordinates from the real map to this instanced map.
+ */
+ public Chunk getBaseChunk() {
+ return baseChunk;
+ }
+
+ /**
+ * @return The palette of this map.
+ */
+ public DynamicMapPalette getPalette() {
+ return palette;
+ }
+
+ /**
+ * @return The key of the controller for this instance.
+ */
+ public ControllerKey> getControllerKey() {
+ return controllerKey;
+ }
+}
diff --git a/src/main/java/io/luna/game/model/map/DynamicMapController.java b/src/main/java/io/luna/game/model/map/DynamicMapController.java
new file mode 100644
index 00000000..3b3dcf6b
--- /dev/null
+++ b/src/main/java/io/luna/game/model/map/DynamicMapController.java
@@ -0,0 +1,23 @@
+package io.luna.game.model.map;
+
+import io.luna.game.model.Position;
+import io.luna.game.model.mob.Player;
+import io.luna.game.model.mob.controller.PlayerController;
+
+public abstract class DynamicMapController extends PlayerController {
+
+ // TODO temporary? need to think more about what could be added here
+ @Override
+ public final void onRegister(Player player) {
+ player.teleport(enter(player));
+ }
+
+ @Override
+ public final void onUnregister(Player player) {
+ player.teleport(exit(player));
+ }
+
+ public abstract Position enter(Player player);
+
+ public abstract Position exit(Player player);
+}
diff --git a/src/main/java/io/luna/game/model/map/DynamicMapSpace.java b/src/main/java/io/luna/game/model/map/DynamicMapSpace.java
new file mode 100644
index 00000000..65692466
--- /dev/null
+++ b/src/main/java/io/luna/game/model/map/DynamicMapSpace.java
@@ -0,0 +1,51 @@
+package io.luna.game.model.map;
+
+import com.google.common.base.Objects;
+import io.luna.game.model.Location;
+import io.luna.game.model.Position;
+import io.luna.game.model.Region;
+
+public class DynamicMapSpace implements Location {
+
+ private final Region main;
+ private final Region padding;
+
+ public DynamicMapSpace(Region main) {
+ this.main = main;
+ padding = new Region(main.getX() + 1, main.getY());
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof DynamicMapSpace)) return false;
+ DynamicMapSpace space = (DynamicMapSpace) o;
+ return Objects.equal(main, space.main) && Objects.equal(padding, space.padding);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hashCode(main, padding);
+ }
+
+ @Override
+ public boolean contains(Position position) {
+ return main.contains(position) || padding.contains(position);
+ }
+
+ public boolean isVisibleTo(DynamicMapSpace other) {
+ return main.isWithinDistance(other.main, 1) ||
+ main.isWithinDistance(other.padding, 1) ||
+ padding.isWithinDistance(other.main, 1) ||
+ padding.isWithinDistance(other.padding, 1);
+ }
+
+ public Region getMain() {
+ return main;
+ }
+
+ public Region getPadding() {
+ return padding;
+ }
+
+}
diff --git a/src/main/java/io/luna/game/model/map/DynamicMapSpacePool.java b/src/main/java/io/luna/game/model/map/DynamicMapSpacePool.java
new file mode 100644
index 00000000..35161e79
--- /dev/null
+++ b/src/main/java/io/luna/game/model/map/DynamicMapSpacePool.java
@@ -0,0 +1,107 @@
+package io.luna.game.model.map;
+
+import io.luna.LunaContext;
+import io.luna.game.model.Position;
+import io.luna.game.model.Region;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+
+import static com.google.common.base.Preconditions.checkState;
+import static org.apache.logging.log4j.util.Unbox.box;
+
+/**
+ * The {@link DynamicMapSpacePool} assigns empty space to dynamic maps so that they can be isolated from the main game
+ * world and other map instances. It also reclaims empty space when an instance is no longer using it. This rotating pool of instances ensures
+ * that
+ * - every instance and all available space is tracked
+ * - an instance is never visible to another
+ * - an instance is always available when needed
+ * - empty space is reclaimed when an instance is done
+ */
+public class DynamicMapSpacePool {
+ private static final Logger logger = LogManager.getLogger();
+ private final Set emptySpacePool = new HashSet<>();
+ private final List instances = new ArrayList<>();
+
+ private final LunaContext context;
+
+ public DynamicMapSpacePool(LunaContext context) {
+ this.context = context;
+ }
+
+ public void buildEmptySpacePool() {
+ if (emptySpacePool.isEmpty()) {
+ // Loop through all empty space.
+ Set emptyRegionIds = new LinkedHashSet<>(10_000);
+ for (int regionId = 25_000; regionId < 34_097; regionId++) {
+ Region region = new Region(regionId);
+ Position base = region.getBasePosition();
+ if (base.getX() >= 6400 && base.getY() <= 5248) {
+ emptyRegionIds.add(regionId);
+ }
+ }
+
+ // Build dynamic map spaces from empty regions.
+ Set usedRegionIds = new HashSet<>();
+ for (int regionId : emptyRegionIds) {
+ Region main = new Region(regionId);
+ DynamicMapSpace emptySpace = new DynamicMapSpace(main);
+ int paddingRegionId = emptySpace.getPadding().getId();
+ if (!emptyRegionIds.contains(paddingRegionId) ||
+ usedRegionIds.contains(main.getId()) ||
+ usedRegionIds.contains(paddingRegionId)) {
+ continue;
+ }
+ emptySpacePool.add(emptySpace);
+ usedRegionIds.add(main.getId());
+ usedRegionIds.add(paddingRegionId);
+ }
+ logger.info("Created {} pooled dynamic map spaces.", box(emptySpacePool.size()));
+ }
+ }
+
+ DynamicMapSpace request() {
+ DynamicMapSpace foundSpace = null;
+ Iterator it = emptySpacePool.iterator();
+ while (it.hasNext()) {
+ DynamicMapSpace nextSpace = it.next();
+ boolean visible = false;
+ for (DynamicMap usedMap : instances) {
+ if (usedMap.getAssignedSpace().isVisibleTo(nextSpace)) {
+ // An instance in use is visible to the proposed empty space. Skip it.
+ visible = true;
+ break;
+ }
+ }
+ if (visible) {
+ // Try again with another piece of empty space.
+ continue;
+ }
+ foundSpace = nextSpace;
+ it.remove();
+ }
+ checkState(foundSpace != null,
+ "No empty space left in the pool! Ensure all instances are deleted when no longer in use.");
+ return foundSpace;
+ }
+
+ void release(DynamicMap map) {
+ if (instances.remove(map)) {
+ // Return the empty space back to the pool.
+ DynamicMapSpace assignedSpace = map.getAssignedSpace();
+ emptySpacePool.add(assignedSpace);
+ }
+ }
+
+ public List getInstances() {
+ return Collections.unmodifiableList(instances);
+ }
+}
diff --git a/src/main/java/io/luna/game/model/map/builder/DynamicMapBuilder.java b/src/main/java/io/luna/game/model/map/builder/DynamicMapBuilder.java
new file mode 100644
index 00000000..1ce087bf
--- /dev/null
+++ b/src/main/java/io/luna/game/model/map/builder/DynamicMapBuilder.java
@@ -0,0 +1,131 @@
+package io.luna.game.model.map.builder;
+
+import io.luna.LunaContext;
+import io.luna.game.model.Region;
+import io.luna.game.model.chunk.Chunk;
+import io.luna.game.model.map.DynamicMap;
+import io.luna.game.model.map.DynamicMapController;
+import io.luna.game.model.mob.Player;
+import io.luna.game.model.mob.controller.ControllerKey;
+import io.luna.net.msg.out.DynamicRegionMessageWriter;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+/**
+ * A builder to create {@link DynamicMap} types.
+ *
+ * @author lare96
+ */
+public final class DynamicMapBuilder {
+
+ /**
+ * A builder for the controller portion.
+ */
+ public final class DynamicMapControllerBuilder {
+ private final DynamicMapPalette palette;
+ private final Chunk baseChunk;
+
+ private DynamicMapControllerBuilder(DynamicMapPalette palette, Chunk baseChunk) {
+ this.palette = palette;
+ this.baseChunk = baseChunk;
+ }
+
+ /**
+ * Sets the controller that will be registered to {@link Player} tupes when a {@link DynamicMap} instance is
+ * joined.
+ *
+ * @param controllerKey The key of the controller.
+ * @return The next builder.
+ */
+ public CreateDynamicMapBuilder setController(ControllerKey extends DynamicMapController> controllerKey) {
+ return new CreateDynamicMapBuilder(palette, baseChunk, controllerKey);
+ }
+ }
+
+ /**
+ * A builder to construct the instance.
+ */
+ public final class CreateDynamicMapBuilder {
+ private final DynamicMapPalette palette;
+ private final Chunk baseChunk;
+ private final ControllerKey extends DynamicMapController> controllerKey;
+
+ private CreateDynamicMapBuilder(DynamicMapPalette palette, Chunk baseChunk, ControllerKey extends DynamicMapController> controllerKey) {
+ this.palette = palette;
+ this.baseChunk = baseChunk;
+ this.controllerKey = controllerKey;
+ }
+
+ /**
+ * Constructs the {@link DynamicMap} instance based on the supplied settings.
+ */
+ public DynamicMap build() {
+ DynamicRegionMessageWriter msg = new DynamicRegionMessageWriter(palette);
+ return new DynamicMap(context, baseChunk, palette, controllerKey, msg);
+ }
+ }
+
+ /**
+ * The context.
+ */
+ private final LunaContext context;
+
+ /**
+ * Creates a new {@link DynamicMapBuilder}.
+ *
+ * @param context The context.
+ */
+ public DynamicMapBuilder(LunaContext context) {
+ this.context = context;
+ }
+
+ /**
+ * Fills this builder's palette with {@code regionId} at height level {@code 0}.
+ *
+ * @param regionId The region to fill the palette with.
+ * @return The next builder.
+ */
+ public DynamicMapControllerBuilder fillWithRegion(int regionId) {
+ return fillWithRegion(new Region(regionId));
+
+ }
+
+ /**
+ * Fills this builder's palette with {@code region} at height level {@code 0}.
+ *
+ * @param region The region to fill the palette with.
+ * @return The next builder.
+ */
+ public DynamicMapControllerBuilder fillWithRegion(Region region) {
+ DynamicMapPalette palette = new DynamicMapPalette();
+ palette.setRegion(4, 0, region);
+ DynamicMapChunk mapChunk = palette.getChunk(6, 6, 0);
+ return new DynamicMapControllerBuilder(palette, mapChunk.getChunk());
+ }
+
+ /**
+ * Fills this builder's palette with chunks surrounding {@code baseChunk} with {@code radius} at height level
+ * {@code 0}.
+ *
+ * @param baseChunk The base chunk.
+ * @param radius The radius.
+ * @return The next builder.
+ */
+ public DynamicMapControllerBuilder fillWithChunks(Chunk baseChunk, int radius) {
+ checkArgument(radius >= 0 && radius < 6, "radius must be >= 0 && < 6");
+ DynamicMapPalette palette = new DynamicMapPalette();
+ palette.setChunkRadius(6 - radius, 6 - radius, 0, baseChunk, radius, radius);
+ return new DynamicMapControllerBuilder(palette, baseChunk);
+ }
+
+ /**
+ * Fills this builder's palette with {@code palette}.
+ *
+ * @param baseChunk The base chunk that will be used to translate coordinates between the real and instanced map.
+ * @param palette The palette to fill this builder with.
+ * @return The next builder.
+ */
+ public DynamicMapControllerBuilder setPalette(Chunk baseChunk, DynamicMapPalette palette) {
+ return new DynamicMapControllerBuilder(palette, baseChunk);
+ }
+}
diff --git a/src/main/java/io/luna/game/model/map/builder/DynamicMapChunk.java b/src/main/java/io/luna/game/model/map/builder/DynamicMapChunk.java
new file mode 100644
index 00000000..8558a734
--- /dev/null
+++ b/src/main/java/io/luna/game/model/map/builder/DynamicMapChunk.java
@@ -0,0 +1,102 @@
+package io.luna.game.model.map.builder;
+
+import io.luna.game.model.chunk.Chunk;
+import io.luna.game.model.map.DynamicMap;
+
+/**
+ * Represents a real world map chunk that can be copied into a {@link DynamicMapPalette}. Used to build
+ * {@link DynamicMap} types.
+ *
+ * @author lare96
+ */
+public final class DynamicMapChunk {
+
+ /**
+ * All possible rotation values for chunks within a palette.
+ */
+ public enum Rotation {
+ NORMAL(0),
+ CW_90_DEGREES(1),
+ CW_180_DEGREES(2),
+ CW_270_DEGREES(3);
+
+ /**
+ * The rotation value.
+ */
+ private final int value;
+
+ /**
+ * Creates a new {@link Rotation}.
+ *
+ * @param value The rotation value.
+ */
+ Rotation(int value) {
+ this.value = value;
+ }
+
+ /**
+ * @return The rotation value.
+ */
+ public int getValue() {
+ return value;
+ }
+ }
+
+ /**
+ * The real world base chunk.
+ */
+ private final Chunk chunk;
+
+ /**
+ * The rotation of the base chunk in the palette.
+ */
+ private final Rotation rotation;
+
+ /**
+ * Creates a new {@link DynamicMapChunk}.
+ *
+ * @param chunk The real world base chunk.
+ * @param rotation The rotation of the base chunk in the palette.
+ */
+ public DynamicMapChunk(Chunk chunk, Rotation rotation) {
+ this.chunk = chunk;
+ this.rotation = rotation;
+ }
+
+ /**
+ * Creates a new {@link DynamicMapChunk} with a normal rotation value.
+ *
+ * @param chunk The real world base chunk.
+ */
+ public DynamicMapChunk(Chunk chunk) {
+ this(chunk, Rotation.NORMAL);
+ }
+
+ /**
+ * @return The real world base chunk.
+ */
+ public Chunk getChunk() {
+ return chunk;
+ }
+
+ /**
+ * @return The formatted chunk {@code x} coordinate.
+ */
+ public int getX() {
+ return chunk.getAbsX() / 8;
+ }
+
+ /**
+ * @return The formatted chunk {@code y} coordinate.
+ */
+ public int getY() {
+ return chunk.getAbsY() / 8;
+ }
+
+ /**
+ * @return The rotation value.
+ */
+ public int getRotation() {
+ return rotation.getValue();
+ }
+}
diff --git a/src/main/java/io/luna/game/model/map/builder/DynamicMapPalette.java b/src/main/java/io/luna/game/model/map/builder/DynamicMapPalette.java
new file mode 100644
index 00000000..60595ba3
--- /dev/null
+++ b/src/main/java/io/luna/game/model/map/builder/DynamicMapPalette.java
@@ -0,0 +1,184 @@
+package io.luna.game.model.map.builder;
+
+import io.luna.game.model.Region;
+import io.luna.game.model.chunk.Chunk;
+import io.luna.game.model.map.DynamicMap;
+
+import java.util.ArrayDeque;
+import java.util.LinkedHashSet;
+import java.util.Queue;
+import java.util.Set;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkState;
+
+/**
+ * Represents a 13x13x4 space made up of {@link DynamicMapChunk} types. Chunks from the real Runescape world can
+ * be copied into this palette in order to make {@link DynamicMap} types.
+ *
+ * @author lare96
+ */
+public final class DynamicMapPalette {
+
+ /**
+ * Retrieves all {@link Chunk} locations within {@code regionId}.
+ *
+ * @param regionId The ID of the region.
+ * @return All chunks within the region
+ */
+ public static Set getAllChunksInRegion(int regionId) {
+ Set chunks = new LinkedHashSet<>(64);
+ Region region = new Region(regionId);
+ Chunk baseChunk = region.getBasePosition().getChunk();
+ for (int x = baseChunk.getX() - 8; x < baseChunk.getX() + 8; x++) {
+ for (int y = baseChunk.getY() - 8; y < baseChunk.getY() + 8; y++) {
+ Chunk chunk = new Chunk(x, y);
+ if (chunk.getAbsPosition().getRegion().getId() == regionId) {
+ chunks.add(chunk);
+ }
+ }
+ }
+ return chunks;
+ }
+
+ /**
+ * Retrieves all {@link Chunk} locations within {@code radiusX} and {@code radiusY} surrounding {@code baseChunk}.
+ *
+ * @param baseChunk The base chunk.
+ * @param radiusX The {@code x} radius.
+ * @param radiusY The {@code y} radius.
+ * @return All chunks within the radius.
+ */
+ public static Set getSurroundingChunks(Chunk baseChunk, int radiusX, int radiusY) {
+ Set chunks = new LinkedHashSet<>();
+ for (int x = -radiusX; x <= radiusX; x++) {
+ for (int y = -radiusY; y <= radiusY; y++) {
+ Chunk nextChunk = baseChunk.translate(x, y);
+ chunks.add(nextChunk);
+ }
+ }
+ return chunks;
+ }
+
+ /**
+ * A consumer used within {@link #forEach(PaletteConsumer)}.
+ */
+ public interface PaletteConsumer {
+
+ /**
+ * The application function of this consumer.
+ *
+ * @param palette The underlying palette.
+ * @param x The {@code x} slot in the palette.
+ * @param y The {@code y} slot in the palette.
+ * @param z The {@code z} slot in the palette.
+ */
+ void apply(DynamicMapPalette palette, int x, int y, int z);
+ }
+
+ /**
+ * The dynamic map palette.
+ */
+ private final DynamicMapChunk[][][] palette = new DynamicMapChunk[13][13][4];
+
+ /**
+ * Traverses through this palette applying {@code processor} to every index.
+ *
+ * @param processor The consumer to apply.
+ */
+ public void forEach(PaletteConsumer processor) {
+ for (int z = 0; z < 4; z++) {
+ for (int x = 0; x < 13; x++) {
+ for (int y = 0; y < 13; y++) {
+ processor.apply(this, x, y, z);
+ }
+ }
+ }
+ }
+
+ /**
+ * Fills this palette at height level {@code z} with all chunks located within {@code region}.
+ *
+ * @param startXY Where on the palette to place the region (min 0, max 5).
+ * @param z The height level on the palette to place the region.
+ * @param region The region to place.
+ * @return This palette.
+ */
+ public DynamicMapPalette setRegion(int startXY, int z, Region region) {
+ // TODO test different xy values and see if that changes instance coordinates?
+ checkArgument(startXY >= 0 && startXY <= 5, "startXY must be above or equal to 0 and below or equal to 5.");
+
+ int regionId = region.getId();
+ Queue regionChunks = new ArrayDeque<>(getAllChunksInRegion(regionId));
+ for (int x = startXY; x < startXY + 8; x++) { // A region is 8x8 chunks, so we give it 8x8 slots on our palette.
+ for (int y = startXY; y < startXY + 8; y++) {
+ Chunk chunk = regionChunks.poll();
+ checkState(chunk != null, "Size mismatch, expected 64 chunks in regionChunks.");
+ palette[x][y][z] = new DynamicMapChunk(chunk);
+ }
+ }
+ return this;
+ }
+
+ /**
+ * Fills this palette at height level {@code z} with all chunks surrounding {@code baseChunk} with {@code radius}.
+ *
+ * @param startX The {@code x} placement coordinate on the palette.
+ * @param startY The {@code y} placement coordinate on the palette.
+ * @param z The height level on the palette to place the chunks.
+ * @param baseChunk The base chunk.
+ * @param radiusX The {@code x} radius of the base chunk.
+ * @param radiusY The {@code y} radius of the base chunk.
+ * @return This palette.
+ */
+ public DynamicMapPalette setChunkRadius(int startX, int startY, int z, Chunk baseChunk, int radiusX, int radiusY) {
+ int lowerBoundX = startX - radiusX;
+ int upperBoundX = startX + radiusX;
+ int lowerBoundY = startY - radiusY;
+ int upperBoundY = startY + radiusY;
+ checkArgument(lowerBoundX > 0 && lowerBoundY > 0, "[startX - radiusX && startY - radiusY] cannot be below 0");
+ checkArgument(upperBoundX < palette.length && upperBoundY < palette.length, "[startX + radiusX && startY + radiusY] cannot exceed palette size");
+
+ Queue regionChunks = new ArrayDeque<>(getSurroundingChunks(baseChunk, radiusX, radiusY));
+ for (int x = startX - radiusX; x <= startX + radiusX; x++) {
+ for (int y = startY - radiusY; y <= startY + radiusY; y++) {
+ Chunk chunk = regionChunks.poll();
+ checkState(chunk != null, "Size mismatch in regionChunks.");
+ palette[x][y][z] = new DynamicMapChunk(chunk);
+ }
+ }
+ return this;
+ }
+
+ /**
+ * Sets the {@link DynamicMapChunk} on the specified coordinates in the palette.
+ *
+ * @param x The {@code x} coordinate.
+ * @param y The {@code y} coordinate.
+ * @param z The {@code z} coordinate.
+ * @return This palette.
+ */
+ public DynamicMapPalette setChunk(int x, int y, int z, DynamicMapChunk chunk) {
+ palette[x][y][z] = chunk;
+ return this;
+ }
+
+ /**
+ * Retrieves the {@link DynamicMapChunk} on the specified coordinates in the palette.
+ *
+ * @param x The {@code x} coordinate.
+ * @param y The {@code y} coordinate.
+ * @param z The {@code z} coordinate.
+ * @return The dynamic map chunk.
+ */
+ public DynamicMapChunk getChunk(int x, int y, int z) {
+ return palette[x][y][z];
+ }
+
+ /**
+ * @return The dynamic map palette.
+ */
+ public DynamicMapChunk[][][] getArray() {
+ return palette;
+ }
+}