1
0
Fork 0

Data model for paired portals

main
Marissa Staib 6 years ago
parent 289db444d0
commit cc17c0da71
  1. 2
      TODO
  2. 2
      build.gradle
  3. 42
      src/main/kotlin/net/deliciousreya/minecraftportal/MinecraftPortalPlugin.kt
  4. 19
      src/main/kotlin/net/deliciousreya/minecraftportal/extensions/Location.kt
  5. 26
      src/main/kotlin/net/deliciousreya/minecraftportal/extensions/Material.kt
  6. 5
      src/main/kotlin/net/deliciousreya/minecraftportal/model/DeserializationException.kt
  7. 27
      src/main/kotlin/net/deliciousreya/minecraftportal/model/Portal.kt
  8. 82
      src/main/kotlin/net/deliciousreya/minecraftportal/model/PortalDataStore.kt
  9. 68
      src/main/kotlin/net/deliciousreya/minecraftportal/model/PortalFrame.kt
  10. 89
      src/main/kotlin/net/deliciousreya/minecraftportal/model/PortalType.kt
  11. 5
      src/main/kotlin/net/deliciousreya/minecraftportal/model/SerializationException.kt
  12. 47
      src/main/proto/portal-save-data.proto

@ -7,4 +7,4 @@
* teleport everyone inside the portal, not just the person who closed the door
* interrupt teleportation if portal is destroyed mid-teleportation
* 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
* add portal particles and ambient sounds to the teleporter when portals are paired

@ -2,6 +2,7 @@ plugins {
id 'java'
id 'org.jetbrains.kotlin.jvm' version '1.3.21'
id "ru.endlesscode.bukkitgradle" version "0.8.1"
id 'com.google.protobuf' version '0.8.8'
}
group 'net.deliciousreya'
@ -26,6 +27,7 @@ dependencies {
compileOnly bukkit()
compileOnly group: 'com.destroystokyo.paper', name: 'paper-api', version: '1.13.2-R0.1-SNAPSHOT'
shade "com.google.guava:guava:27.1-jre"
shade "com.google.protobuf:protobuf-java:3.7.1"
shade "org.jetbrains.kotlin:kotlin-stdlib-jdk8"
testCompile group: 'junit', name: 'junit', version: '4.12'
}

@ -1,8 +1,10 @@
package net.deliciousreya.minecraftportal
import com.destroystokyo.paper.event.block.BlockDestroyEvent
import org.bukkit.Effect
import org.bukkit.Material
import net.deliciousreya.minecraftportal.model.DOOR_TYPES
import net.deliciousreya.minecraftportal.model.PortalDataStore
import net.deliciousreya.minecraftportal.model.PortalFrame
import net.deliciousreya.minecraftportal.model.findPortalFrameConnectedTo
import org.bukkit.Particle
import org.bukkit.Sound
import org.bukkit.block.Block
@ -10,10 +12,8 @@ import org.bukkit.block.data.type.Door
import org.bukkit.event.EventHandler
import org.bukkit.event.Listener
import org.bukkit.event.block.*
import org.bukkit.event.entity.CreatureSpawnEvent
import org.bukkit.event.entity.EntityChangeBlockEvent
import org.bukkit.event.entity.EntityExplodeEvent
import org.bukkit.event.player.PlayerBucketFillEvent
import org.bukkit.event.player.PlayerInteractEvent
import org.bukkit.plugin.java.JavaPlugin
import org.bukkit.potion.PotionEffect
@ -21,6 +21,8 @@ import org.bukkit.potion.PotionEffectType
class MinecraftPortalPlugin() : JavaPlugin(), Listener
{
val portals: PortalDataStore = PortalDataStore()
override fun onEnable() {
super.onEnable()
logger.info("Loaded the portal plugin!")
@ -30,12 +32,15 @@ class MinecraftPortalPlugin() : JavaPlugin(), Listener
@EventHandler
fun onBlockPlaced(e: BlockPlaceEvent) {
if (e.block.type in PortalFrame.State.INACTIVE.allValidBlocks) {
val portalScanResults = findPortalFrameConnectedTo(e.block, PortalFrame.State.INACTIVE)
if (portalScanResults != null) {
val newPortal = findPortalFrameConnectedTo(
e.block,
PortalFrame.State.INACTIVE
)
if (newPortal != null) {
logger.info("found portal frame, creating portal")
portalScanResults.color()
e.block.world.playSound(portalScanResults.portalCenter, Sound.BLOCK_BEACON_ACTIVATE, 20f, 1f)
e.block.world.spawnParticle(Particle.SPELL, portalScanResults.portalCenter, 40)
val replacedPortal = portals.activateAndReplacePortal(newPortal)
newPortal.activate()
replacedPortal?.deactivate()
} else {
logger.info("no portal frame found matching placed block, ignoring")
}
@ -87,7 +92,10 @@ class MinecraftPortalPlugin() : JavaPlugin(), Listener
if(block.type !in DOOR_TYPES) {
return
}
val portalScanResults = findPortalFrameConnectedTo(block, PortalFrame.State.ACTIVE) ?: return
val portalScanResults = findPortalFrameConnectedTo(
block,
PortalFrame.State.ACTIVE
) ?: return
val door = block.blockData as Door
if (!portalScanResults.isStandingInPortal(e.player.location)) {
e.isCancelled = true
@ -112,14 +120,12 @@ class MinecraftPortalPlugin() : JavaPlugin(), Listener
if (block.type !in PortalFrame.State.ACTIVE.allValidBlocks) {
return
}
val portalScanResults = findPortalFrameConnectedTo(block, PortalFrame.State.ACTIVE)
if (portalScanResults != null) {
logger.info("found portal frame matching destroyed block, deactivating")
portalScanResults.uncolor()
block.world.playSound(portalScanResults.portalCenter, Sound.BLOCK_BEACON_DEACTIVATE, 20f, 1f)
block.world.spawnParticle(Particle.SMOKE_NORMAL, portalScanResults.portalCenter, 40)
} else {
logger.info("no portal frame found matching destroyed block, ignoring")
val oldPortal = findPortalFrameConnectedTo(
block,
PortalFrame.State.ACTIVE
)
if (oldPortal != null && portals.deactivatePortal(oldPortal)) {
oldPortal.deactivate()
}
}
}

@ -1,7 +1,15 @@
package net.deliciousreya.minecraftportal.extensions
import net.deliciousreya.minecraftportal.model.DeserializationException
import net.deliciousreya.minecraftportal.model.SerializationException
import net.deliciousreya.minecraftportal.proto.PortalSaveDataProtos
import org.bukkit.Location
import org.bukkit.Server
import org.bukkit.util.BlockVector
import org.bukkit.util.Vector
import java.lang.IllegalArgumentException
import java.lang.IllegalStateException
import java.util.*
operator fun Location.plus(v: Vector):Location {
return this.clone().add(v)
@ -30,3 +38,14 @@ fun Location.minus(x: Double, y: Double, z: Double):Location {
operator fun Location.times(n:Double):Location {
return this.clone().multiply(n)
}
fun PortalSaveDataProtos.Location.toLocation(server: Server): Location {
val uuid = UUID.fromString(this.worldUuid)
val world = (server.getWorld(uuid) ?: throw DeserializationException("Couldn't find world with UUID $worldUuid"))
return BlockVector(this.x, this.y, this.z).toLocation(world)
}
fun Location.toProto(): PortalSaveDataProtos.Location {
val world = this.world ?: throw SerializationException("Can't serialize location without world")
return PortalSaveDataProtos.Location.newBuilder().setWorldUuid(world.uid.toString()).setX(this.blockX).setY(this.blockY).setZ(this.blockZ).build()
}

@ -0,0 +1,26 @@
package net.deliciousreya.minecraftportal.extensions
import com.google.common.collect.ImmutableBiMap
import net.deliciousreya.minecraftportal.model.DeserializationException
import net.deliciousreya.minecraftportal.proto.PortalSaveDataProtos
import org.bukkit.Material
import java.lang.IllegalStateException
val MINERAL_MAPPING: ImmutableBiMap<Material, PortalSaveDataProtos.Portal.Mineral> = ImmutableBiMap.builder<Material, PortalSaveDataProtos.Portal.Mineral>()
.put(Material.COAL_BLOCK, PortalSaveDataProtos.Portal.Mineral.COAL)
.put(Material.REDSTONE_BLOCK, PortalSaveDataProtos.Portal.Mineral.REDSTONE)
.put(Material.LAPIS_BLOCK, PortalSaveDataProtos.Portal.Mineral.LAPIS)
.put(Material.GOLD_BLOCK, PortalSaveDataProtos.Portal.Mineral.GOLD)
.put(Material.DIAMOND_BLOCK, PortalSaveDataProtos.Portal.Mineral.DIAMOND)
.put(Material.EMERALD_BLOCK, PortalSaveDataProtos.Portal.Mineral.EMERALD)
.put(Material.IRON_BLOCK, PortalSaveDataProtos.Portal.Mineral.IRON)
.put(Material.QUARTZ_BLOCK, PortalSaveDataProtos.Portal.Mineral.QUARTZ)
.build()
fun Material.toMineralProto(): PortalSaveDataProtos.Portal.Mineral {
return MINERAL_MAPPING[this] ?: throw DeserializationException("Not a mineral: ${this}")
}
fun PortalSaveDataProtos.Portal.Mineral.toMaterial(): Material {
return MINERAL_MAPPING.inverse()[this] ?: throw DeserializationException("Not a material: ${this}")
}

@ -0,0 +1,5 @@
package net.deliciousreya.minecraftportal.model
import java.lang.Exception
class DeserializationException (message: String? = null, cause: Throwable? = null) : Exception(message, cause)

@ -0,0 +1,27 @@
package net.deliciousreya.minecraftportal.model
import net.deliciousreya.minecraftportal.extensions.toLocation
import net.deliciousreya.minecraftportal.extensions.toMaterial
import net.deliciousreya.minecraftportal.extensions.toMineralProto
import net.deliciousreya.minecraftportal.extensions.toProto
import net.deliciousreya.minecraftportal.proto.PortalSaveDataProtos
import org.bukkit.Material
import org.bukkit.Server
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)
// TODO: assert that the saved portal and mineral match the actual state of the world
return result
}
class Portal(val frame: PortalFrame, val type: PortalType) {
fun toProto(): PortalSaveDataProtos.Portal {
return PortalSaveDataProtos.Portal.newBuilder()
.setLowerLeftFrontCorner(frame.lowerLeftFrontCorner.toProto())
.setEntranceDirection(frame.direction.toProto())
.setMineral(frame.mineral.toMineralProto())
.build()
}
}

@ -0,0 +1,82 @@
package net.deliciousreya.minecraftportal.model
import com.google.common.collect.ImmutableMap
import net.deliciousreya.minecraftportal.extensions.MINERAL_MAPPING
import net.deliciousreya.minecraftportal.proto.PortalSaveDataProtos
import org.bukkit.Material
import org.bukkit.Server
import java.io.File
import java.lang.IllegalArgumentException
class PortalDataStore() {
val portalTypes: ImmutableMap<Material, PortalType>
init {
val portalTypesBuilder = ImmutableMap.builder<Material, PortalType>()
for (mineralType in MINERAL_MAPPING.keys) {
portalTypesBuilder.put(mineralType, PortalType(mineralType))
}
portalTypes = portalTypesBuilder.build()
}
fun onAfterChanged() {
// Does nothing for now.
// TODO: Save the data.
}
/** 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 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)
onAfterChanged()
return result
}
/** Forgets all the data without actually affecting the world. */
fun clear() {
for (portalType in this.portalTypes.values) {
portalType.clear()
}
}
fun loadFromProto(server: Server, proto: PortalSaveDataProtos.PortalSaveData) {
clear()
for (portalProto in proto.unpairedPortalsList) {
val portal = portalProto.toPortal(server, portalTypes)
portal.type.loadUnpairedPortal(portal)
}
for (portalPair in proto.pairedPortalsList) {
val olderPortal = portalPair.older.toPortal(server, portalTypes)
val newerPortal = portalPair.newer.toPortal(server, portalTypes)
if (olderPortal.type != newerPortal.type) {
throw DeserializationException("Pair had non-matching types: ${olderPortal.type} and ${newerPortal.type}")
}
olderPortal.type.loadPairedPortals(olderPortal, newerPortal)
}
}
fun toProto(): PortalSaveDataProtos.PortalSaveData {
val builder = PortalSaveDataProtos.PortalSaveData.newBuilder()
for (portalType in this.portalTypes.values) {
when {
portalType.isEmpty -> {
// Nothing here to save. Just skip it.
}
portalType.isPaired -> {
builder.addPairedPortals(portalType.toProto())
}
else -> {
builder.addUnpairedPortals(portalType.getOnlyPortal.toProto())
}
}
}
return builder.build()
}
}

@ -1,4 +1,4 @@
package net.deliciousreya.minecraftportal
package net.deliciousreya.minecraftportal.model
import com.google.common.collect.ImmutableList
import com.google.common.collect.ImmutableMap
@ -7,8 +7,11 @@ import org.bukkit.Location
import org.bukkit.block.Block
import org.bukkit.util.Vector
import net.deliciousreya.minecraftportal.extensions.*
import net.deliciousreya.minecraftportal.proto.PortalSaveDataProtos
import org.bukkit.Material
import org.bukkit.Material.*
import org.bukkit.Particle
import org.bukkit.Sound
import org.bukkit.block.BlockFace
import org.bukkit.material.Directional
@ -25,11 +28,15 @@ val MINERAL_TYPES: ImmutableMap<Material, Material> = ImmutableMap.Builder<Mater
val DOOR_TYPES: ImmutableSet<Material> = ImmutableSet.of(ACACIA_DOOR, BIRCH_DOOR, DARK_OAK_DOOR, IRON_DOOR, JUNGLE_DOOR, OAK_DOOR, SPRUCE_DOOR)
fun findPortalFrameConnectedTo(block:Block, state:PortalFrame.State): PortalFrame? {
fun findPortalFrameConnectedTo(block:Block, state: PortalFrame.State): PortalFrame? {
when {
block.type in state.glassBlocks -> for (direction in PortalFrame.EntranceDirection.values()) {
for (vector in direction.glassOffsets) {
val scanResult = checkPortalFrameAt(block.location.subtract(vector), direction, state)
val scanResult = checkPortalFrameAt(
block.location.subtract(vector),
direction,
state
)
if (scanResult != null) {
return scanResult
}
@ -37,14 +44,22 @@ fun findPortalFrameConnectedTo(block:Block, state:PortalFrame.State): PortalFram
}
block.type in state.doorBlocks -> for (direction in PortalFrame.EntranceDirection.values()) {
for (vector in direction.doorOffsets) {
val scanResult = checkPortalFrameAt(block.location.subtract(vector), direction, state)
val scanResult = checkPortalFrameAt(
block.location.subtract(vector),
direction,
state
)
if (scanResult != null) {
return scanResult
}
}
}
block.type in state.mineralBlocks -> for (direction in PortalFrame.EntranceDirection.values()) {
val scanResult = checkPortalFrameAt(block.location.subtract(direction.mineralOffset), direction, state)
val scanResult = checkPortalFrameAt(
block.location.subtract(direction.mineralOffset),
direction,
state
)
if (scanResult != null) {
return scanResult
}
@ -54,7 +69,7 @@ fun findPortalFrameConnectedTo(block:Block, state:PortalFrame.State): PortalFram
}
/** 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.EntranceDirection, state: PortalFrame.State): PortalFrame? {
val mineral = (location + direction.mineralOffset).block
if (mineral.type !in state.mineralBlocks) {
return null
@ -74,20 +89,31 @@ fun checkPortalFrameAt(location:Location, direction:PortalFrame.EntranceDirectio
return PortalFrame(location, direction)
}
fun PortalSaveDataProtos.Portal.Direction.toDirection(): PortalFrame.EntranceDirection {
for (direction in PortalFrame.EntranceDirection.values()) {
if (direction.protoEnum == this) {
return direction
}
}
throw IllegalStateException("Bad enum value: $this")
}
/** Information about a portal frame. */
data class PortalFrame(val lowerLeftFrontCorner: Location, val direction: EntranceDirection) {
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)),
INACTIVE(ImmutableSet.of(GLASS), DOOR_TYPES, ImmutableSet.copyOf(MINERAL_TYPES.keys));
ACTIVE(ImmutableSet.copyOf(MINERAL_TYPES.values),
DOOR_TYPES, ImmutableSet.copyOf(MINERAL_TYPES.keys)),
INACTIVE(ImmutableSet.of(GLASS),
DOOR_TYPES, ImmutableSet.copyOf(MINERAL_TYPES.keys));
val allValidBlocks = ImmutableSet.Builder<Material>().addAll(glassBlocks).addAll(doorBlocks).addAll(mineralBlocks).build()
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) {
NORTH(Vector(1, 0, 0), Vector(0, 0, 1), BlockFace.NORTH),
SOUTH(Vector(1, 0, 0), Vector(0, 0, -1), BlockFace.SOUTH),
EAST(Vector(0, 0, 1), Vector(-1, 0, 0), BlockFace.EAST),
WEST(Vector(0, 0, 1), Vector(1, 0, 0), BlockFace.WEST);
enum class EntranceDirection(toRight: Vector, toBack: Vector, val doorDirection: BlockFace, val protoEnum: PortalSaveDataProtos.Portal.Direction) {
NORTH(Vector(1, 0, 0), Vector(0, 0, 1), BlockFace.NORTH, PortalSaveDataProtos.Portal.Direction.NORTH),
SOUTH(Vector(1, 0, 0), Vector(0, 0, -1), BlockFace.SOUTH, PortalSaveDataProtos.Portal.Direction.SOUTH),
EAST(Vector(0, 0, 1), Vector(-1, 0, 0), BlockFace.EAST, PortalSaveDataProtos.Portal.Direction.EAST),
WEST(Vector(0, 0, 1), Vector(1, 0, 0), BlockFace.WEST, PortalSaveDataProtos.Portal.Direction.WEST);
val glassOffsets: ImmutableList<Vector> = ImmutableList.of(
// check corners first:
@ -138,6 +164,10 @@ data class PortalFrame(val lowerLeftFrontCorner: Location, val direction: Entran
Vector(0, 1, 0) + toRight,
Vector(0, 2, 0) + toRight
)
fun toProto():PortalSaveDataProtos.Portal.Direction {
return this.protoEnum
}
}
val portalCenter = lowerLeftFrontCorner + direction.doorOffsets[0]
@ -146,16 +176,22 @@ data class PortalFrame(val lowerLeftFrontCorner: Location, val direction: Entran
return direction.doorOffsets.any { offset -> location.block.location - offset == lowerLeftFrontCorner }
}
fun color() {
val mineral = (lowerLeftFrontCorner + direction.mineralOffset).block.type
fun activate() {
val mineral = (lowerLeftFrontCorner + direction.mineralOffset).block
for (offset in direction.glassOffsets) {
(lowerLeftFrontCorner + offset).block.type = MINERAL_TYPES.getOrDefault(mineral.type, BROWN_STAINED_GLASS)
}
portalCenter.world?.playSound(portalCenter, Sound.BLOCK_BEACON_ACTIVATE, 20f, 1f)
portalCenter.world?.spawnParticle(Particle.SPELL, portalCenter, 40)
}
fun uncolor() {
fun deactivate() {
for (offset in direction.glassOffsets) {
(lowerLeftFrontCorner + offset).block.type = GLASS
}
portalCenter.world?.playSound(portalCenter, Sound.BLOCK_BEACON_DEACTIVATE, 20f, 1f)
portalCenter.world?.spawnParticle(Particle.SMOKE_NORMAL, portalCenter, 40)
}
}

@ -0,0 +1,89 @@
package net.deliciousreya.minecraftportal.model
import net.deliciousreya.minecraftportal.proto.PortalSaveDataProtos
import org.bukkit.Material
import java.lang.IllegalArgumentException
import java.lang.IllegalStateException
class PortalType(val mineral: Material) {
var oldestActivePortal: Portal? = null
var newestActivePortal: Portal? = null
val isEmpty: Boolean get() = oldestActivePortal == null && newestActivePortal == null
val isPaired: Boolean get() = oldestActivePortal != null && newestActivePortal != null
val getOnlyPortal: Portal get() = if (!isEmpty && !isPaired) oldestActivePortal!! else throw IllegalStateException("Can't getOnlyPortal for empty or paired portal type $this")
fun toProto(): PortalSaveDataProtos.PortalPair {
val builder = PortalSaveDataProtos.PortalPair.newBuilder()
val oldestPortal = oldestActivePortal
val newestPortal = newestActivePortal
if (oldestPortal != null) {
builder.older = oldestPortal.toProto()
}
if (newestPortal != null) {
builder.newer = newestPortal.toProto()
}
return builder.build()
}
fun addOrReplacePortal(portal: Portal): Portal? {
if (portal.type != this) {
throw DeserializationException("Tried to add $portal which didn't belong to ${this}")
}
return when {
this.isEmpty -> {
this.oldestActivePortal = portal
null
}
else -> {
val replacedPortal = newestActivePortal
this.newestActivePortal = portal
replacedPortal
}
}
}
fun loadUnpairedPortal(portal: Portal) {
if (!this.isEmpty) {
throw DeserializationException("Already have a value for ${this} when adding $portal")
}
if (portal.type != this) {
throw DeserializationException("Tried to add $portal which didn't belong to ${this}")
}
this.oldestActivePortal = portal
}
fun loadPairedPortals(olderPortal: Portal, newerPortal: Portal) {
if (!this.isEmpty) {
throw DeserializationException("Already have a value for ${this} when adding $olderPortal and $newerPortal")
}
if (olderPortal.type != this) {
throw DeserializationException("Tried to add $olderPortal which didn't belong to ${this}")
}
if (newerPortal.type != this) {
throw DeserializationException("Tried to add $newerPortal which didn't belong to ${this}")
}
this.oldestActivePortal = olderPortal
this.newestActivePortal = newerPortal
}
fun clear() {
oldestActivePortal = null
newestActivePortal = null
}
override fun toString():String {
return "PortalType{mineral=$mineral, oldestActivePortal=$oldestActivePortal, newestActivePortal=$newestActivePortal}"
}
fun removePortalWithFrame(frame: PortalFrame):Boolean {
if (oldestActivePortal?.frame == frame) {
oldestActivePortal = newestActivePortal
} else if (newestActivePortal?.frame != frame) {
return false
}
newestActivePortal = null
return true
}
}

@ -0,0 +1,5 @@
package net.deliciousreya.minecraftportal.model
import java.lang.Exception
class SerializationException (message: String? = null, cause: Throwable? = null) : Exception(message, cause)

@ -0,0 +1,47 @@
syntax = "proto3";
package net.deliciousreya.minecraftportal;
option java_package = "net.deliciousreya.minecraftportal.proto";
option java_outer_classname = "PortalSaveDataProtos";
message Location {
string world_uuid = 1;
int32 x = 2;
int32 y = 3;
int32 z = 4;
}
message Portal {
enum Direction {
UNKNOWN_DIRECTION = 0;
NORTH = 1;
EAST = 2;
WEST = 3;
SOUTH = 4;
}
enum Mineral {
UNKNOWN_MINERAL = 0;
COAL = 1;
REDSTONE = 2;
LAPIS = 3;
GOLD = 4;
DIAMOND = 5;
EMERALD = 6;
IRON = 7;
QUARTZ = 8;
}
Location lower_left_front_corner = 1;
Direction entrance_direction = 2;
Mineral mineral = 3;
}
message PortalPair {
Portal older = 1;
Portal newer = 2;
}
message PortalSaveData {
repeated PortalPair paired_portals = 1;
repeated Portal unpaired_portals = 2;
}
Loading…
Cancel
Save