1
0
Fork 0

Portals actually work!

main
Marissa Staib 5 years ago
parent 4f304895ad
commit 2ae528fc98
  1. 5
      TODO
  2. 51
      src/main/kotlin/net/deliciousreya/minecraftportal/MinecraftPortalPlugin.kt
  3. 1
      src/main/kotlin/net/deliciousreya/minecraftportal/extensions/Vector.kt
  4. 4
      src/main/kotlin/net/deliciousreya/minecraftportal/model/Portal.kt
  5. 112
      src/main/kotlin/net/deliciousreya/minecraftportal/model/PortalDataStore.kt
  6. 62
      src/main/kotlin/net/deliciousreya/minecraftportal/model/PortalFrame.kt
  7. 48
      src/main/kotlin/net/deliciousreya/minecraftportal/model/PortalType.kt
  8. 2
      src/main/proto/portal-save-data.proto

@ -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

@ -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()
}
}
}
}

@ -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)

@ -11,7 +11,7 @@ import java.lang.IllegalArgumentException
fun PortalSaveDataProtos.Portal.toPortal(server: Server, portalTypes: Map<Material, PortalType>): 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()
}

@ -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<Material, PortalType>
init {
val portalTypesBuilder = ImmutableMap.builder<Material, PortalType>()
@ -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) {

@ -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<Material, Material> = ImmutableMap.Builder<Material, Material>()
.put(COAL_BLOCK, BLACK_STAINED_GLASS)
@ -32,7 +32,7 @@ val DOOR_TYPES: ImmutableSet<Material> = 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<Material>, val doorBlocks: ImmutableSet<Material>, val mineralBlocks: ImmutableSet<Material>) {
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<Material> = ImmutableSet.Builder<Material>().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<Vector> = 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) {

@ -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
}
}

@ -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;
}

Loading…
Cancel
Save