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 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 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 controllerKey; + + private CreateDynamicMapBuilder(DynamicMapPalette palette, Chunk baseChunk, ControllerKey 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; + } +}