diff --git a/TODO b/TODO index c0516a2..86ab0cc 100644 --- a/TODO +++ b/TODO @@ -1,10 +1,7 @@ -* automatically open the door of a portal that is freshly activated -* save portal pairs to disk and reload them on restarting the server * teleport from one to the other a set time after entering a teleportation chamber and closing the door * automatically open the door after effects wear off -* close the door on the other side when someone shuts the door * teleport everyone inside the portal, not just the person who closed the door -* interrupt teleportation if portal is destroyed mid-teleportation +* interrupt teleportation if portal is destroyed mid-teleportation, or if a new portal is constructed during teleportation, changing the destination * cancel teleportation if someone moves out of the teleporter during teleportation (maybe prevent them from starting?) * save any effects that would be overwritten by teleportation (including to disk!), and put them back when the player opens the door or arrives * add portal particles and ambient sounds to the teleporter when portals are paired \ No newline at end of file diff --git a/src/main/kotlin/net/deliciousreya/minecraftportal/MinecraftPortalPlugin.kt b/src/main/kotlin/net/deliciousreya/minecraftportal/MinecraftPortalPlugin.kt index 0a40b41..3078a35 100644 --- a/src/main/kotlin/net/deliciousreya/minecraftportal/MinecraftPortalPlugin.kt +++ b/src/main/kotlin/net/deliciousreya/minecraftportal/MinecraftPortalPlugin.kt @@ -1,6 +1,8 @@ package net.deliciousreya.minecraftportal import com.destroystokyo.paper.event.block.BlockDestroyEvent +import net.deliciousreya.minecraftportal.extensions.MID_BLOCK +import net.deliciousreya.minecraftportal.extensions.plus import net.deliciousreya.minecraftportal.model.DOOR_TYPES import net.deliciousreya.minecraftportal.model.PortalDataStore import net.deliciousreya.minecraftportal.model.PortalFrame @@ -25,9 +27,15 @@ class MinecraftPortalPlugin() : JavaPlugin(), Listener override fun onEnable() { super.onEnable() + portals.loadFromAndAutoSaveTo(server, dataFolder.resolve("portal_data.binproto"), dataFolder.resolve("portal_data.binproto.bak")) server.pluginManager.registerEvents(this, this) } + override fun onDisable() { + super.onDisable() + portals.unload() + } + @EventHandler fun onBlockPlaced(e: BlockPlaceEvent) { if (e.block.type in PortalFrame.State.INACTIVE.allValidBlocks) { @@ -37,8 +45,16 @@ class MinecraftPortalPlugin() : JavaPlugin(), Listener ) if (newPortal != null) { val replacedPortal = portals.activateAndReplacePortal(newPortal) + val otherPortal = portals.getOtherPortal(newPortal) newPortal.activate() replacedPortal?.deactivate() + if (otherPortal != null) { + newPortal.open() + otherPortal.open() + } else { + newPortal.ejectEntities() + newPortal.close() + } } } } @@ -88,27 +104,34 @@ class MinecraftPortalPlugin() : JavaPlugin(), Listener if(block.type !in DOOR_TYPES) { return } - val portalScanResults = findPortalFrameConnectedTo( - block, - PortalFrame.State.ACTIVE - ) ?: return val door = block.blockData as Door - if (!portalScanResults.isStandingInPortal(e.player.location)) { + val location = e.player.location + // i.e., is the door part of a portal and are we standing in that portal? + val portal = portals.isLocationInPortalChamber(block.location + MID_BLOCK) ?: return + if (location.toVector() !in portal.portalInsideBoundingBox) { e.isCancelled = true return } if (!door.isOpen) { // door is about to be opened, so undrug the player e.player.stopSound(Sound.BLOCK_PORTAL_TRIGGER) - e.player.removePotionEffect(PotionEffectType.BLINDNESS) + val otherPortal = portals.getOtherPortal(frame = portal) ?: return + otherPortal.open() + /* e.player.removePotionEffect(PotionEffectType.BLINDNESS) e.player.removePotionEffect(PotionEffectType.CONFUSION) - e.player.removePotionEffect(PotionEffectType.INVISIBILITY) + e.player.removePotionEffect(PotionEffectType.INVISIBILITY) */ } else { // door is about to be closed, so drug the player - e.player.playSound(e.player.location, Sound.BLOCK_PORTAL_TRIGGER, 20f, 1f) - e.player.addPotionEffect(PotionEffect(PotionEffectType.CONFUSION, 200, 1, false, false, false)) + val otherPortal = portals.getOtherPortal(frame = portal) ?: return + otherPortal.close() + val relativeLocation = portal.getRelativeLocationFromAbsoluteLocation(location) + val destination = otherPortal.getAbsoluteLocationFromRelativeLocation(relativeLocation) + relativeLocation.world?.playSound(e.player.location, Sound.BLOCK_PORTAL_TRIGGER, 20f, 1f) + destination.world?.playSound(destination, Sound.BLOCK_PORTAL_TRAVEL, 20f, 1f) + e.player.teleport(destination) + /* e.player.addPotionEffect(PotionEffect(PotionEffectType.CONFUSION, 200, 1, false, false, false)) e.player.addPotionEffect(PotionEffect(PotionEffectType.BLINDNESS, 200, 1, false, false, false)) - e.player.addPotionEffect(PotionEffect(PotionEffectType.INVISIBILITY, 200, 1, false, false, false)) + e.player.addPotionEffect(PotionEffect(PotionEffectType.INVISIBILITY, 200, 1, false, false, false)) */ } } @@ -120,8 +143,14 @@ class MinecraftPortalPlugin() : JavaPlugin(), Listener block, PortalFrame.State.ACTIVE ) - if (oldPortal != null && portals.deactivatePortal(oldPortal)) { + if (oldPortal != null) { + val otherPortal = portals.getOtherPortal(oldPortal) + portals.deactivatePortal(oldPortal) oldPortal.deactivate() + if (otherPortal != null) { + otherPortal.ejectEntities() + otherPortal.close() + } } } } \ No newline at end of file diff --git a/src/main/kotlin/net/deliciousreya/minecraftportal/extensions/Vector.kt b/src/main/kotlin/net/deliciousreya/minecraftportal/extensions/Vector.kt index 2a47e13..74eb00a 100644 --- a/src/main/kotlin/net/deliciousreya/minecraftportal/extensions/Vector.kt +++ b/src/main/kotlin/net/deliciousreya/minecraftportal/extensions/Vector.kt @@ -5,6 +5,7 @@ import org.bukkit.util.Vector val ZERO = Vector(0, 0, 0) val UP = Vector(0, 1, 0) val MID_BLOCK = Vector(0.5, 0.5, 0.5) +val MID_BLOCK_BOTTOM = Vector(0.5, 0.0, 0.5) operator fun Vector.plus(v:Vector):Vector { return this.clone().add(v) diff --git a/src/main/kotlin/net/deliciousreya/minecraftportal/model/Portal.kt b/src/main/kotlin/net/deliciousreya/minecraftportal/model/Portal.kt index d2cd53c..62a0795 100644 --- a/src/main/kotlin/net/deliciousreya/minecraftportal/model/Portal.kt +++ b/src/main/kotlin/net/deliciousreya/minecraftportal/model/Portal.kt @@ -11,7 +11,7 @@ import java.lang.IllegalArgumentException fun PortalSaveDataProtos.Portal.toPortal(server: Server, portalTypes: Map): Portal { val portalType = portalTypes[mineral.toMaterial()] ?: throw IllegalArgumentException("No portal type for $mineral") - val result = Portal(PortalFrame(lowerLeftFrontCorner.toLocation(server), entranceDirection.toDirection()), portalType) + val result = Portal(PortalFrame(lowerLeftFrontCorner.toLocation(server), exitDirection.toDirection()), portalType) // TODO: assert that the saved portal and mineral match the actual state of the world return result } @@ -20,7 +20,7 @@ class Portal(val frame: PortalFrame, val type: PortalType) { fun toProto(): PortalSaveDataProtos.Portal { return PortalSaveDataProtos.Portal.newBuilder() .setLowerLeftFrontCorner(frame.lowerLeftFrontCorner.toProto()) - .setEntranceDirection(frame.direction.toProto()) + .setExitDirection(frame.direction.toProto()) .setMineral(frame.mineral.toMineralProto()) .build() } diff --git a/src/main/kotlin/net/deliciousreya/minecraftportal/model/PortalDataStore.kt b/src/main/kotlin/net/deliciousreya/minecraftportal/model/PortalDataStore.kt index 0bf60f2..147208b 100644 --- a/src/main/kotlin/net/deliciousreya/minecraftportal/model/PortalDataStore.kt +++ b/src/main/kotlin/net/deliciousreya/minecraftportal/model/PortalDataStore.kt @@ -1,14 +1,19 @@ package net.deliciousreya.minecraftportal.model import com.google.common.collect.ImmutableMap +import com.google.protobuf.InvalidProtocolBufferException import net.deliciousreya.minecraftportal.extensions.MINERAL_MAPPING import net.deliciousreya.minecraftportal.proto.PortalSaveDataProtos +import org.bukkit.Location import org.bukkit.Material import org.bukkit.Server import java.io.File +import java.io.IOException import java.lang.IllegalArgumentException -class PortalDataStore() { +class PortalDataStore { + var saveDataTo: File? = null + var useBackup: File? = null val portalTypes: ImmutableMap init { val portalTypesBuilder = ImmutableMap.builder() @@ -18,27 +23,118 @@ class PortalDataStore() { portalTypes = portalTypesBuilder.build() } + fun isLocationInPortalOrChamber(location: Location): PortalFrame? { + for (type in this.portalTypes.values) { + if (type.isEmpty) { + continue + } + val result = type.isLocationInPortalOrChamber(location) + if (result != null) { + return result.frame + } + } + return null + } + + fun isLocationInPortalChamber(location: Location): PortalFrame? { + for (type in this.portalTypes.values) { + if (type.isEmpty) { + continue + } + val result = type.isLocationInPortalChamber(location) + if (result != null) { + return result.frame + } + } + return null + } + + fun loadFromAndAutoSaveTo(server: Server, mainFile: File, backupFile: File? = null) { + var firstException:Throwable? = null + val loadingMainFile = mainFile.exists() + if (loadingMainFile) { + try { + val protoData = mainFile.readBytes() + val proto = PortalSaveDataProtos.PortalSaveData.parseFrom(protoData) + this.loadFromProto(server, proto) + } catch (e: IOException) { + if (backupFile == null || !backupFile.exists()) { + throw DeserializationException(cause = e) + } else { + firstException = e + } + } catch (e: InvalidProtocolBufferException) { + if (backupFile == null || !backupFile.exists()) { + throw DeserializationException(cause = e) + } else { + firstException = e + } + } + } + if ((!loadingMainFile || firstException != null) && backupFile != null && backupFile.exists()) { + try { + val protoData = backupFile.readBytes() + backupFile.renameTo(mainFile) + val proto = PortalSaveDataProtos.PortalSaveData.parseFrom(protoData) + this.loadFromProto(server, proto) + } catch (e: IOException) { + throw DeserializationException(cause = firstException ?: e) + } catch (e: InvalidProtocolBufferException) { + throw DeserializationException(cause = firstException ?: e) + } + } + this.saveDataTo = mainFile + this.useBackup = backupFile + } + + fun unload() { + this.clear() + this.saveDataTo = null + this.useBackup = null + } + fun onAfterChanged() { - // Does nothing for now. - // TODO: Save the data. + val destination = saveDataTo ?: return + val tempDestination = File.createTempFile("PortalSaveData", ".tmp.binproto") + val backupDestination = useBackup + if (backupDestination != null && destination.exists()) { + backupDestination.delete() + destination.copyTo(backupDestination) + } + val proto = toProto() + val bytes = proto.toByteArray() + tempDestination.writeBytes(bytes) + destination.delete() + tempDestination.renameTo(destination) + backupDestination?.delete() } /** Marks the new portal as active, and removes the one that needs to be deactivated, if any. */ fun activateAndReplacePortal(frame: PortalFrame): PortalFrame? { - val type = portalTypes[frame.mineral] ?: throw IllegalArgumentException("Can't create a portal of type ${frame.mineral}") + val type = getPortalType(frame) val result = type.addOrReplacePortal(Portal(frame, type))?.frame onAfterChanged() return result } - /** Deactivates the given portal. */ - fun deactivatePortal(frame: PortalFrame): Boolean { - val type = portalTypes[frame.mineral] ?: throw IllegalArgumentException("There are no portals of type ${frame.mineral}") - val result = type.removePortalWithFrame(frame) + /** Deactivates the given portal. Returns the other portal if it needs to be closed. */ + fun deactivatePortal(frame: PortalFrame): PortalFrame? { + val type = getPortalType(frame) + val result = type.removePortalWithFrame(frame)?.frame onAfterChanged() return result } + fun getPortalType(frame: PortalFrame): PortalType { + return portalTypes[frame.mineral] ?: throw IllegalArgumentException("There are no portals of type ${frame.mineral}") + } + + /** Gets the other portal, if the given frame is one of the portals in a pair. */ + fun getOtherPortal(frame: PortalFrame): PortalFrame? { + val type = getPortalType(frame) + return type.getOtherPortal(frame)?.frame + } + /** Forgets all the data without actually affecting the world. */ fun clear() { for (portalType in this.portalTypes.values) { diff --git a/src/main/kotlin/net/deliciousreya/minecraftportal/model/PortalFrame.kt b/src/main/kotlin/net/deliciousreya/minecraftportal/model/PortalFrame.kt index 2b2750e..90d913d 100644 --- a/src/main/kotlin/net/deliciousreya/minecraftportal/model/PortalFrame.kt +++ b/src/main/kotlin/net/deliciousreya/minecraftportal/model/PortalFrame.kt @@ -15,7 +15,7 @@ import org.bukkit.Sound import org.bukkit.block.BlockFace import org.bukkit.block.data.Directional import org.bukkit.block.data.Openable -import org.bukkit.block.data.type.Door +import org.bukkit.util.BoundingBox val MINERAL_TYPES: ImmutableMap = ImmutableMap.Builder() .put(COAL_BLOCK, BLACK_STAINED_GLASS) @@ -32,7 +32,7 @@ val DOOR_TYPES: ImmutableSet = ImmutableSet.of(ACACIA_DOOR, BIRCH_DOOR fun findPortalFrameConnectedTo(block:Block, state: PortalFrame.State): PortalFrame? { when { - block.type in state.glassBlocks -> for (direction in PortalFrame.EntranceDirection.values()) { + block.type in state.glassBlocks -> for (direction in PortalFrame.ExitDirection.values()) { for (vector in direction.glassOffsets) { val scanResult = checkPortalFrameAt( block.location.subtract(vector), @@ -44,7 +44,7 @@ fun findPortalFrameConnectedTo(block:Block, state: PortalFrame.State): PortalFra } } } - block.type in state.doorBlocks -> for (direction in PortalFrame.EntranceDirection.values()) { + block.type in state.doorBlocks -> for (direction in PortalFrame.ExitDirection.values()) { for (vector in direction.doorOffsets) { val scanResult = checkPortalFrameAt( block.location.subtract(vector), @@ -56,7 +56,7 @@ fun findPortalFrameConnectedTo(block:Block, state: PortalFrame.State): PortalFra } } } - block.type in state.mineralBlocks -> for (direction in PortalFrame.EntranceDirection.values()) { + block.type in state.mineralBlocks -> for (direction in PortalFrame.ExitDirection.values()) { val scanResult = checkPortalFrameAt( block.location.subtract(direction.mineralOffset), direction, @@ -71,7 +71,7 @@ fun findPortalFrameConnectedTo(block:Block, state: PortalFrame.State): PortalFra } /** Detects whether there is a portal frame in the given state at the given location, extending in the given direction. */ -fun checkPortalFrameAt(location:Location, direction: PortalFrame.EntranceDirection, state: PortalFrame.State): PortalFrame? { +fun checkPortalFrameAt(location:Location, direction: PortalFrame.ExitDirection, state: PortalFrame.State): PortalFrame? { val mineral = (location + direction.mineralOffset).block if (mineral.type !in state.mineralBlocks) { return null @@ -92,8 +92,8 @@ fun checkPortalFrameAt(location:Location, direction: PortalFrame.EntranceDirecti return PortalFrame(location, direction) } -fun PortalSaveDataProtos.Portal.Direction.toDirection(): PortalFrame.EntranceDirection { - for (direction in PortalFrame.EntranceDirection.values()) { +fun PortalSaveDataProtos.Portal.Direction.toDirection(): PortalFrame.ExitDirection { + for (direction in PortalFrame.ExitDirection.values()) { if (direction.protoEnum == this) { return direction } @@ -102,7 +102,7 @@ fun PortalSaveDataProtos.Portal.Direction.toDirection(): PortalFrame.EntranceDir } /** Information about a portal frame. */ -data class PortalFrame(val lowerLeftFrontCorner: Location, val direction: EntranceDirection) { +data class PortalFrame(val lowerLeftFrontCorner: Location, val direction: ExitDirection) { enum class State(val glassBlocks: ImmutableSet, val doorBlocks: ImmutableSet, val mineralBlocks: ImmutableSet) { ACTIVE(ImmutableSet.copyOf(MINERAL_TYPES.values), DOOR_TYPES, ImmutableSet.copyOf(MINERAL_TYPES.keys)), @@ -111,12 +111,12 @@ data class PortalFrame(val lowerLeftFrontCorner: Location, val direction: Entran val allValidBlocks:ImmutableSet = ImmutableSet.Builder().addAll(glassBlocks).addAll(doorBlocks).addAll(mineralBlocks).build() } - /** The direction along which a portal frame extends. */ - enum class EntranceDirection(toRight: Vector, toBack: Vector, val doorDirection: BlockFace, val protoEnum: PortalSaveDataProtos.Portal.Direction) { + /** The direction a player faces when exiting the portal. */ + enum class ExitDirection(val toRight: Vector, val toBack: Vector, val doorDirection: BlockFace, val protoEnum: PortalSaveDataProtos.Portal.Direction) { NORTH(Vector(1, 0, 0), Vector(0, 0, 1), BlockFace.SOUTH, PortalSaveDataProtos.Portal.Direction.NORTH), - SOUTH(Vector(1, 0, 0), Vector(0, 0, -1), BlockFace.NORTH, PortalSaveDataProtos.Portal.Direction.SOUTH), + SOUTH(Vector(-1, 0, 0), Vector(0, 0, -1), BlockFace.NORTH, PortalSaveDataProtos.Portal.Direction.SOUTH), EAST(Vector(0, 0, 1), Vector(-1, 0, 0), BlockFace.WEST, PortalSaveDataProtos.Portal.Direction.EAST), - WEST(Vector(0, 0, 1), Vector(1, 0, 0), BlockFace.EAST, PortalSaveDataProtos.Portal.Direction.WEST); + WEST(Vector(0, 0, -1), Vector(1, 0, 0), BlockFace.EAST, PortalSaveDataProtos.Portal.Direction.WEST); val glassOffsets: ImmutableList = ImmutableList.of( // check corners first: @@ -174,9 +174,35 @@ data class PortalFrame(val lowerLeftFrontCorner: Location, val direction: Entran } val portalCenter = lowerLeftFrontCorner + direction.doorOffsets[0] + val fullPortalBoundingBox = BoundingBox.of(lowerLeftFrontCorner.block, (lowerLeftFrontCorner + UP * 3 + direction.toRight * 2 + direction.toBack).block) + val portalInsideBoundingBox = BoundingBox.of(portalCenter.block, (portalCenter + UP).block) - fun isStandingInPortal(location: Location): Boolean { - return direction.doorOffsets.any { offset -> location.block.location - offset == lowerLeftFrontCorner } + /** Gets the worldless location (direction and position vectors) relative to the center of the chamber, rotating so that the exit is north. */ + fun getRelativeLocationFromAbsoluteLocation(location: Location): Location { + if (portalCenter.world != location.world) { + throw IllegalArgumentException("can't get relative position from ${portalCenter.world} to ${location.world}") + } + if (portalCenter.world == null) { + throw IllegalArgumentException("How did we end up with a relative portal center??") + } + val oldDirection = location.direction + val translatedPosition = location.toVector() - (portalCenter + MID_BLOCK_BOTTOM).toVector() + val newLocation = Location(null, translatedPosition.dot(direction.toRight), translatedPosition.dot(UP), translatedPosition.dot(direction.toBack), 0f, 0f) + newLocation.direction = Vector(oldDirection.dot(direction.toRight), oldDirection.dot(UP), oldDirection.dot(direction.toBack)) + return newLocation + } + + fun getAbsoluteLocationFromRelativeLocation(location: Location): Location { + if (location.world != null) { + throw IllegalArgumentException("can't get absolute position from already absolute position") + } + val world = portalCenter.world ?: throw IllegalArgumentException("How did we end up with a relative portal center??") + val oldDirection = location.direction + val oldPosition = location.toVector() + val rotatedPosition = Vector(oldPosition.dot(direction.toRight), oldPosition.dot(UP), oldPosition.dot(direction.toBack)) + val newLocation = (rotatedPosition + portalCenter.toVector() + MID_BLOCK_BOTTOM).toLocation(world) + newLocation.direction = Vector(oldDirection.dot(direction.toRight), oldDirection.dot(UP), oldDirection.dot(direction.toBack)) + return newLocation } val mineral = (lowerLeftFrontCorner + direction.mineralOffset).block.type @@ -199,6 +225,14 @@ data class PortalFrame(val lowerLeftFrontCorner: Location, val direction: Entran } } + fun ejectEntities() { + val world = portalCenter.world ?: return + for (entity in world.getNearbyEntities(portalInsideBoundingBox)) { + // kick 'em out the front! + entity.teleport(entity.location - direction.toBack) + } + } + fun activate() { val mineral = (lowerLeftFrontCorner + direction.mineralOffset).block for (offset in direction.glassOffsets) { diff --git a/src/main/kotlin/net/deliciousreya/minecraftportal/model/PortalType.kt b/src/main/kotlin/net/deliciousreya/minecraftportal/model/PortalType.kt index f9f3da9..717f594 100644 --- a/src/main/kotlin/net/deliciousreya/minecraftportal/model/PortalType.kt +++ b/src/main/kotlin/net/deliciousreya/minecraftportal/model/PortalType.kt @@ -1,6 +1,7 @@ package net.deliciousreya.minecraftportal.model import net.deliciousreya.minecraftportal.proto.PortalSaveDataProtos +import org.bukkit.Location import org.bukkit.Material import java.lang.IllegalArgumentException import java.lang.IllegalStateException @@ -77,13 +78,54 @@ class PortalType(val mineral: Material) { return "PortalType{mineral=$mineral, oldestActivePortal=$oldestActivePortal, newestActivePortal=$newestActivePortal}" } - fun removePortalWithFrame(frame: PortalFrame):Boolean { + /** + * Removes the given portal. If that brought this down to one portal and the other portal should become closed, + * returns it. Otherwise (if that frame wasn't for either portal or if there are none left) returns null. + */ + fun removePortalWithFrame(frame: PortalFrame): Portal? { if (oldestActivePortal?.frame == frame) { oldestActivePortal = newestActivePortal } else if (newestActivePortal?.frame != frame) { - return false + return null } newestActivePortal = null - return true + return oldestActivePortal + } + + /** Gets the matching portal - null if there is none, or if that isn't one of the portals of this type */ + fun getOtherPortal(frame: PortalFrame): Portal? { + return when (frame) { + oldestActivePortal?.frame -> newestActivePortal + newestActivePortal?.frame -> oldestActivePortal + else -> null + } + } + + fun isLocationInPortalChamber(location: Location): Portal? { + if (oldestActivePortal?.frame?.lowerLeftFrontCorner?.world == location.world + && oldestActivePortal?.frame?.portalInsideBoundingBox?.contains(location.toVector()) == true + ) { + return oldestActivePortal + } + if (newestActivePortal?.frame?.lowerLeftFrontCorner?.world == location.world + && newestActivePortal?.frame?.portalInsideBoundingBox?.contains(location.toVector()) == true + ) { + return newestActivePortal + } + return null + } + + fun isLocationInPortalOrChamber(location: Location): Portal? { + if (oldestActivePortal?.frame?.lowerLeftFrontCorner?.world == location.world + && oldestActivePortal?.frame?.fullPortalBoundingBox?.contains(location.toVector()) == true + ) { + return oldestActivePortal + } + if (newestActivePortal?.frame?.lowerLeftFrontCorner?.world == location.world + && newestActivePortal?.frame?.fullPortalBoundingBox?.contains(location.toVector()) == true + ) { + return newestActivePortal + } + return null } } \ No newline at end of file diff --git a/src/main/proto/portal-save-data.proto b/src/main/proto/portal-save-data.proto index 50512b6..e7f1c01 100644 --- a/src/main/proto/portal-save-data.proto +++ b/src/main/proto/portal-save-data.proto @@ -32,7 +32,7 @@ message Portal { QUARTZ = 8; } Location lower_left_front_corner = 1; - Direction entrance_direction = 2; + Direction exit_direction = 2; Mineral mineral = 3; }