diff --git a/nms/src/main/kotlin/io/github/pylonmc/rebar/i18n/packet/PlayerPacketHandler.kt b/nms/src/main/kotlin/io/github/pylonmc/rebar/i18n/packet/PlayerPacketHandler.kt index 1f9e6fee9..b918cf4b6 100644 --- a/nms/src/main/kotlin/io/github/pylonmc/rebar/i18n/packet/PlayerPacketHandler.kt +++ b/nms/src/main/kotlin/io/github/pylonmc/rebar/i18n/packet/PlayerPacketHandler.kt @@ -4,7 +4,6 @@ package io.github.pylonmc.rebar.i18n.packet import io.github.pylonmc.rebar.Rebar import io.github.pylonmc.rebar.i18n.PlayerTranslationHandler -import io.github.pylonmc.rebar.item.RebarItem import io.github.pylonmc.rebar.item.RebarItemSchema import io.github.pylonmc.rebar.util.editData import io.netty.channel.ChannelDuplexHandler @@ -247,7 +246,7 @@ class PlayerPacketHandler(private val player: ServerPlayer, val handler: PlayerT if (stack.isEmpty) return stack val bukkitStack = CraftItemStack.asCraftMirror(stack) val schema = RebarItemSchema.fromStack(bukkitStack) ?: return stack - val prototype = schema.getItemStack() + val prototype = schema.createNewItemStack() prototype.copyDataFrom(bukkitStack) { it != DataComponentTypes.ITEM_NAME && it != DataComponentTypes.LORE } prototype.editPersistentDataContainer { it.remove(PlayerTranslationHandler.FOOTER_APPENDED) } prototype.amount = bukkitStack.amount diff --git a/rebar/build.gradle.kts b/rebar/build.gradle.kts index e50a1b9bf..c5a3e50ae 100644 --- a/rebar/build.gradle.kts +++ b/rebar/build.gradle.kts @@ -41,8 +41,8 @@ dependencies { paperLibraryApi("xyz.xenondevs.invui:invui:2.0.0-beta.5") paperLibraryApi("xyz.xenondevs.invui:invui-kotlin:2.0.0-beta.5") - implementation("com.github.Tofaa2.EntityLib:spigot:2.4.11") - implementation("com.github.retrooper:packetevents-spigot:2.11.2") + paperLibraryApi("com.github.Tofaa2.EntityLib:spigot:2.4.11") + paperLibraryApi("com.github.retrooper:packetevents-spigot:2.11.2") implementation("info.debatty:java-string-similarity:2.0.0") implementation("org.bstats:bstats-bukkit:2.2.1") paperLibrary("com.github.ben-manes.caffeine:caffeine:3.2.2") @@ -144,8 +144,6 @@ tasks.shadowJar { exclude("org/intellij/lang/annotations/**") exclude("org/jetbrains/annotations/**") - relocate("com.github.retrooper.packetevents", "io.github.pylonmc.rebar.packetevents") - relocate("me.tofaa.entitylib", "io.github.pylonmc.rebar.entitylib") relocate("org.bstats", "io.github.pylonmc.rebar.bstats") archiveBaseName = "rebar" diff --git a/rebar/src/main/kotlin/io/github/pylonmc/rebar/Rebar.kt b/rebar/src/main/kotlin/io/github/pylonmc/rebar/Rebar.kt index 4cb2dd53e..532d6c3e7 100644 --- a/rebar/src/main/kotlin/io/github/pylonmc/rebar/Rebar.kt +++ b/rebar/src/main/kotlin/io/github/pylonmc/rebar/Rebar.kt @@ -21,6 +21,7 @@ import io.github.pylonmc.rebar.content.debug.DebugWaxedWeatheredCutCopperStairs import io.github.pylonmc.rebar.content.fluid.* import io.github.pylonmc.rebar.content.guide.RebarGuide import io.github.pylonmc.rebar.culling.BlockCullingEngine +import io.github.pylonmc.rebar.electricity.WireConnectionService import io.github.pylonmc.rebar.entity.ConfettiCreeperListener import io.github.pylonmc.rebar.entity.EntityListener import io.github.pylonmc.rebar.entity.EntityStorage @@ -44,6 +45,7 @@ import io.github.pylonmc.rebar.recipe.RecipeCompletion import io.github.pylonmc.rebar.recipe.RecipeType import io.github.pylonmc.rebar.registry.RebarRegistry import io.github.pylonmc.rebar.resourcepack.armor.ArmorTextureEngine +import io.github.pylonmc.rebar.util.PlayerInput import io.github.pylonmc.rebar.util.delayTicks import io.github.pylonmc.rebar.util.mergeGlobalConfig import io.github.pylonmc.rebar.waila.Waila @@ -63,7 +65,6 @@ import org.bukkit.Material import org.bukkit.NamespacedKey import org.bukkit.configuration.file.YamlConfiguration import org.bukkit.entity.BlockDisplay -import org.bukkit.entity.Display import org.bukkit.entity.FallingBlock import org.bukkit.entity.ItemDisplay import org.bukkit.permissions.Permission @@ -71,7 +72,7 @@ import org.bukkit.permissions.PermissionDefault import org.bukkit.plugin.java.JavaPlugin import xyz.xenondevs.invui.InvUI import xyz.xenondevs.invui.i18n.Languages -import java.util.Locale +import java.util.* import kotlin.io.path.* /** @@ -133,6 +134,7 @@ object Rebar : JavaPlugin(), RebarAddon { pm.registerEvents(RebarTranslator, this) pm.registerEvents(RebarAddon, this) + @Suppress("UnusedExpression") RebarMetrics // initialize metrics by referencing it // Anything that listens for addon registration must be above this line @@ -164,95 +166,100 @@ object Rebar : JavaPlugin(), RebarAddon { pm.registerEvents(RebarTickingEntity, this) pm.registerEvents(ChunkScope, this) pm.registerEvents(PlayerScope, this) + pm.registerEvents(RebarElectricBlock, this) + pm.registerEvents(RebarElectricConsumerBlock, this) + pm.registerEvents(RebarElectricProducerBlock, this) + pm.registerEvents(WireConnectionService, this) + pm.registerEvents(PlayerInput, this) pm.registerEvents(RebarJoinHandler, this) - ConfettiCreeperListener.register(this, pm) + ConfettiCreeperListener.register(this) // Rebar Blocks - BlockListener.register(this, pm) - RebarBeacon.register(this, pm) - RebarBell.register(this, pm) - RebarTNT.register(this, pm) - RebarNoteBlock.register(this, pm) - RebarCrafter.register(this, pm) - RebarSponge.register(this, pm) - RebarFurnace.register(this, pm) - RebarCampfire.register(this, pm) - RebarBrewingStand.register(this, pm) - RebarDispenser.register(this, pm) - RebarGrowable.register(this, pm) - RebarCauldron.register(this, pm) - RebarSign.register(this, pm) - RebarVault.register(this, pm) - RebarLeaf.register(this, pm) - RebarTargetBlock.register(this, pm) - RebarComposter.register(this, pm) - RebarShearable.register(this, pm) - RebarLectern.register(this, pm) - RebarPiston.register(this, pm) - RebarEnchantingTable.register(this, pm) - RebarRedstoneBlock.register(this, pm) - RebarInteractBlock.register(this, pm) - RebarSneakBlock.register(this, pm) - RebarJobBlock.register(this, pm) - RebarJumpBlock.register(this, pm) - RebarUnloadBlock.register(this, pm) - RebarFlowerPot.register(this, pm) - RebarVanillaContainerBlock.register(this, pm) - RebarHopper.register(this, pm) - RebarCargoBlock.register(this, pm) - RebarCopperBlock.register(this, pm) - RebarEntityChangedBlock.register(this, pm) + BlockListener.register(this) + RebarBeacon.register(this) + RebarBell.register(this) + RebarTNT.register(this) + RebarNoteBlock.register(this) + RebarCrafter.register(this) + RebarSponge.register(this) + RebarFurnace.register(this) + RebarCampfire.register(this) + RebarBrewingStand.register(this) + RebarDispenser.register(this) + RebarGrowable.register(this) + RebarCauldron.register(this) + RebarSign.register(this) + RebarVault.register(this) + RebarLeaf.register(this) + RebarTargetBlock.register(this) + RebarComposter.register(this) + RebarShearable.register(this) + RebarLectern.register(this) + RebarPiston.register(this) + RebarEnchantingTable.register(this) + RebarRedstoneBlock.register(this) + RebarInteractBlock.register(this) + RebarSneakBlock.register(this) + RebarJobBlock.register(this) + RebarJumpBlock.register(this) + RebarUnloadBlock.register(this) + RebarFlowerPot.register(this) + RebarVanillaContainerBlock.register(this) + RebarHopper.register(this) + RebarCargoBlock.register(this) + RebarCopperBlock.register(this) + RebarEntityChangedBlock.register(this) // Rebar Items - RebarArrow.register(this, pm) - RebarBlockInteractor.register(this, pm) - RebarBow.register(this, pm) - RebarBrewingStandFuel.register(this, pm) - RebarBucket.register(this, pm) - RebarConsumable.register(this, pm) - RebarDispensable.register(this, pm) - RebarInteractor.register(this, pm) - RebarItemDamageable.register(this, pm) - RebarItemEntityInteractor.register(this, pm) - RebarLingeringPotion.register(this, pm) - RebarSplashPotion.register(this, pm) - RebarTool.register(this, pm) - RebarWeapon.register(this, pm) - VanillaCookingFuel.register(this, pm) + RebarArrow.register(this) + RebarBlockInteractor.register(this) + RebarBow.register(this) + RebarBrewingStandFuel.register(this) + RebarBucket.register(this) + RebarConsumable.register(this) + RebarDispensable.register(this) + RebarInteractor.register(this) + RebarItemDamageable.register(this) + RebarItemEntityInteractor.register(this) + RebarLingeringPotion.register(this) + RebarSplashPotion.register(this) + RebarTool.register(this) + RebarWeapon.register(this) + VanillaCookingFuel.register(this) // Rebar Entities - EntityListener.register(this, pm) - RebarBat.register(this, pm) - RebarBreedable.register(this, pm) - RebarCombustibleEntity.register(this, pm) - RebarCop.register(this, pm) - RebarCreeper.register(this, pm) - RebarDamageableEntity.register(this, pm) - RebarDeathEntity.register(this, pm) - RebarDragonFireball.register(this, pm) - RebarDyeable.register(this, pm) - RebarEnderDragon.register(this, pm) - RebarEnderman.register(this, pm) - RebarExperienceOrb.register(this, pm) - RebarExplosiveEntity.register(this, pm) - RebarFirework.register(this, pm) - RebarInteractEntity.register(this, pm) - RebarItemEntity.register(this, pm) - RebarLeashable.register(this, pm) - RebarMountableEntity.register(this, pm) - RebarMountingEntity.register(this, pm) - RebarMovingEntity.register(this, pm) - RebarPathingEntity.register(this, pm) - RebarPiglin.register(this, pm) - RebarProjectile.register(this, pm) - RebarResurrectable.register(this, pm) - RebarSlime.register(this, pm) - RebarSpellcaster.register(this, pm) - RebarTameable.register(this, pm) - RebarTurtle.register(this, pm) - RebarVillager.register(this, pm) - RebarWitch.register(this, pm) - RebarZombiePigman.register(this, pm) + EntityListener.register(this) + RebarBat.register(this) + RebarBreedable.register(this) + RebarCombustibleEntity.register(this) + RebarCop.register(this) + RebarCreeper.register(this) + RebarDamageableEntity.register(this) + RebarDeathEntity.register(this) + RebarDragonFireball.register(this) + RebarDyeable.register(this) + RebarEnderDragon.register(this) + RebarEnderman.register(this) + RebarExperienceOrb.register(this) + RebarExplosiveEntity.register(this) + RebarFirework.register(this) + RebarInteractEntity.register(this) + RebarItemEntity.register(this) + RebarLeashable.register(this) + RebarMountableEntity.register(this) + RebarMountingEntity.register(this) + RebarMovingEntity.register(this) + RebarPathingEntity.register(this) + RebarPiglin.register(this) + RebarProjectile.register(this) + RebarResurrectable.register(this) + RebarSlime.register(this) + RebarSpellcaster.register(this) + RebarTameable.register(this) + RebarTurtle.register(this) + RebarVillager.register(this) + RebarWitch.register(this) + RebarZombiePigman.register(this) Bukkit.getScheduler().runTaskTimer(this, RebarInventoryTicker(), 0, RebarConfig.INVENTORY_TICKER_BASE_RATE) diff --git a/rebar/src/main/kotlin/io/github/pylonmc/rebar/block/BlockStorage.kt b/rebar/src/main/kotlin/io/github/pylonmc/rebar/block/BlockStorage.kt index 9be319784..9c6fa60cc 100644 --- a/rebar/src/main/kotlin/io/github/pylonmc/rebar/block/BlockStorage.kt +++ b/rebar/src/main/kotlin/io/github/pylonmc/rebar/block/BlockStorage.kt @@ -31,7 +31,7 @@ import org.bukkit.event.world.ChunkUnloadEvent import org.bukkit.inventory.ItemStack import org.bukkit.persistence.PersistentDataContainer import org.bukkit.persistence.PersistentDataType -import java.util.Collections +import java.util.* import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.locks.ReentrantReadWriteLock import kotlin.random.Random @@ -181,6 +181,63 @@ object BlockStorage : Listener { */ inline fun getAs(location: Location): T? = getAs(T::class.java, location) + /** + * Returns the Rebar block (of type [T]) at the given [blockPosition]. + * + * @throws IllegalArgumentException if the chunk containing the block is not loaded or the block is not a Rebar block + * @throws ClassCastException if the block is not of the expected class + */ + @JvmStatic + fun getAsOrThrow(clazz: Class, blockPosition: BlockPosition): T { + val block = get(blockPosition) ?: throw IllegalArgumentException("No Rebar block found at $blockPosition") + if (!clazz.isInstance(block)) { + throw ClassCastException("Block at $blockPosition is not of type ${clazz.simpleName}") + } + return clazz.cast(block) + } + + /** + * Returns the Rebar block (of type [T]) at the given [block]. + * + * @throws IllegalArgumentException if the chunk containing the block is not loaded or the block is not a Rebar block + * @throws ClassCastException if the block is not of the expected class + */ + @JvmStatic + fun getAsOrThrow(clazz: Class, block: Block): T = getAsOrThrow(clazz, block.position) + + /** + * Returns the Rebar block (of type [T]) at the given [location]. + * + * @throws IllegalArgumentException if the chunk containing the block is not loaded or the block is not a Rebar block + * @throws ClassCastException if the block is not of the expected class + */ + @JvmStatic + fun getAsOrThrow(clazz: Class, location: Location): T = getAsOrThrow(clazz, BlockPosition(location)) + + /** + * Returns the Rebar block (of type [T]) at the given [blockPosition]. + * + * @throws IllegalArgumentException if the chunk containing the block is not loaded or the block is not a Rebar block + * @throws ClassCastException if the block is not of the expected class + */ + inline fun getAsOrThrow(blockPosition: BlockPosition): T = getAsOrThrow(T::class.java, blockPosition) + + /** + * Returns the Rebar block (of type [T]) at the given [block]. + * + * @throws IllegalArgumentException if the chunk containing the block is not loaded or the block is not a Rebar block + * @throws ClassCastException if the block is not of the expected class + */ + inline fun getAsOrThrow(block: Block): T = getAsOrThrow(T::class.java, block) + + /** + * Returns the Rebar block (of type [T]) at the given [location]. + * + * @throws IllegalArgumentException if the chunk containing the block is not loaded or the block is not a Rebar block + * @throws ClassCastException if the block is not of the expected class + */ + inline fun getAsOrThrow(location: Location): T = getAsOrThrow(T::class.java, location) + /** * Returns all the Plyon blocks in the chunk at [chunkPosition]. * @@ -193,7 +250,7 @@ object BlockStorage : Listener { } /** - * Returns all the Plyon blocks with type [key]. + * Returns all the Pylon blocks with type [key]. */ @JvmStatic fun getByKey(key: NamespacedKey): Collection = @@ -205,6 +262,18 @@ object BlockStorage : Listener { emptySet() } + /** + * Returns all the Pylon blocks of type [T] + */ + @JvmStatic + fun getByType(clazz: Class) = lockBlockRead { blocks.values.filter { clazz.isInstance(it) }.map { clazz.cast(it)!! } } + + /** + * Returns all the Pylon blocks of type [T] + */ + @JvmStatic + inline fun getByType() = getByType(T::class.java) + /** * Returns whether the block at [blockPosition] is a Rebar block. * Returns false if the chunk at [blockPosition] is not loaded. @@ -313,6 +382,7 @@ object BlockStorage : Listener { RebarBlockPlaceEvent(blockPosition.block, block, context).callEvent() block.postInitialise() + RebarBlockInitializeEvent(blockPosition.block, block).callEvent() BlockCullingEngine.insert(block) return block diff --git a/rebar/src/main/kotlin/io/github/pylonmc/rebar/block/RebarBlock.kt b/rebar/src/main/kotlin/io/github/pylonmc/rebar/block/RebarBlock.kt index c2af4a710..29ed77be3 100644 --- a/rebar/src/main/kotlin/io/github/pylonmc/rebar/block/RebarBlock.kt +++ b/rebar/src/main/kotlin/io/github/pylonmc/rebar/block/RebarBlock.kt @@ -17,6 +17,7 @@ import io.github.pylonmc.rebar.content.debug.DebugWaxedWeatheredCutCopperStairs import io.github.pylonmc.rebar.datatypes.RebarSerializers import io.github.pylonmc.rebar.entity.packet.BlockTextureEntity import io.github.pylonmc.rebar.event.RebarBlockDeserializeEvent +import io.github.pylonmc.rebar.event.RebarBlockInitializeEvent import io.github.pylonmc.rebar.event.RebarBlockSerializeEvent import io.github.pylonmc.rebar.item.builder.ItemStackBuilder import io.github.pylonmc.rebar.nms.NmsAccessor @@ -37,6 +38,7 @@ import org.bukkit.entity.Player import org.bukkit.inventory.ItemStack import org.bukkit.persistence.PersistentDataAdapterContext import org.bukkit.persistence.PersistentDataContainer +import org.jetbrains.annotations.MustBeInvokedByOverriders /** * Represents a Rebar block in the world. @@ -127,8 +129,10 @@ open class RebarBlock private constructor(val block: Block) : Keyed { * If you need to use data from these interfaces (such as the amount of fluid stored in * a [io.github.pylonmc.rebar.block.base.RebarFluidBufferBlock], you must use this * instead of using the data in the load constructor. + * + * @param pdc the persistent data container used to load the block, in case you need to read any additional data from it */ - protected open fun postLoad() {} + protected open fun postLoad(pdc: PersistentDataContainer) {} /** * Called after both the create constructor and the load constructor. @@ -215,6 +219,7 @@ open class RebarBlock private constructor(val block: Block) : Keyed { * instead of returning a new map entirely, to ensure that any properties provided by superclasses * are preserved. (e.g. [RebarDirectionalBlock]) */ + @MustBeInvokedByOverriders open fun getBlockTextureProperties(): MutableMap> { val properties = mutableMapOf>() if (this is RebarDirectionalBlock) { @@ -238,7 +243,7 @@ open class RebarBlock private constructor(val block: Block) : Keyed { * * @return the item that should be used to display the block's texture */ - open fun getBlockTextureItem() = defaultItem?.getItemStack()?.let { ItemStackBuilder(it) }?.apply { + open fun getBlockTextureItem() = defaultItem?.createNewItemStack()?.let { ItemStackBuilder(it) }?.apply { editPdc { it.set(rebarBlockTextureEntityKey, RebarSerializers.BOOLEAN, true) } val properties = NmsAccessor.instance.getStateProperties(block, getBlockTextureProperties()) for ((property, value) in properties) { @@ -267,7 +272,7 @@ open class RebarBlock private constructor(val block: Block) : Keyed { */ open fun getDropItem(context: BlockBreakContext): ItemStack? { return if (context.normallyDrops) { - defaultItem?.getItemStack() + defaultItem?.createNewItemStack() } else { null } @@ -281,7 +286,7 @@ open class RebarBlock private constructor(val block: Block) : Keyed { * * @return the item the block should give when middle clicked, or null if none */ - open fun getPickItem() = defaultItem?.getItemStack() + open fun getPickItem() = defaultItem?.createNewItemStack() /** * Called when debug info is requested for the block by someone @@ -409,7 +414,8 @@ open class RebarBlock private constructor(val block: Block) : Keyed { RebarBlockDeserializeEvent(block.block, block, pdc).callEvent() block.postInitialise() - block.postLoad() + RebarBlockInitializeEvent(block.block, block).callEvent() + block.postLoad(pdc) return block } catch (t: Throwable) { Rebar.logger.severe("Error while loading block $key at $position") diff --git a/rebar/src/main/kotlin/io/github/pylonmc/rebar/block/base/RebarElectricBlock.kt b/rebar/src/main/kotlin/io/github/pylonmc/rebar/block/base/RebarElectricBlock.kt new file mode 100644 index 000000000..1cbc04a69 --- /dev/null +++ b/rebar/src/main/kotlin/io/github/pylonmc/rebar/block/base/RebarElectricBlock.kt @@ -0,0 +1,155 @@ +package io.github.pylonmc.rebar.block.base + +import io.github.pylonmc.rebar.datatypes.RebarSerializers +import io.github.pylonmc.rebar.electricity.ElectricNode +import io.github.pylonmc.rebar.electricity.ElectricityManager +import io.github.pylonmc.rebar.electricity.WireConnectionService +import io.github.pylonmc.rebar.entity.display.InteractionBuilder +import io.github.pylonmc.rebar.entity.display.ItemDisplayBuilder +import io.github.pylonmc.rebar.entity.display.transform.TransformBuilder +import io.github.pylonmc.rebar.event.RebarBlockBreakEvent +import io.github.pylonmc.rebar.event.RebarBlockDeserializeEvent +import io.github.pylonmc.rebar.event.RebarBlockSerializeEvent +import io.github.pylonmc.rebar.event.RebarBlockUnloadEvent +import io.github.pylonmc.rebar.item.builder.ItemStackBuilder +import io.github.pylonmc.rebar.util.rebarKey +import org.bukkit.Bukkit +import org.bukkit.Material +import org.bukkit.block.BlockFace +import org.bukkit.entity.Interaction +import org.bukkit.event.EventHandler +import org.bukkit.event.EventPriority +import org.bukkit.event.Listener +import org.jetbrains.annotations.ApiStatus +import java.util.* + +interface RebarElectricBlock : RebarEntityHolderBlock { + + @ApiStatus.NonExtendable + fun addElectricNode(node: T): T { + electricBlocks.getOrPut(this, ::mutableListOf).add(node) + ElectricityManager.addNode(node) + return node + } + + @get:ApiStatus.NonExtendable + val electricNodes: List + get() = electricBlocks[this].orEmpty() + + @ApiStatus.NonExtendable + fun getElectricNode(name: String) = electricNodes.find { it.name == name } + + @ApiStatus.NonExtendable + fun getElectricNodeOrThrow(name: String) = getElectricNode(name) ?: throw NoSuchElementException("No electric node with name '$name' found in block at ${block.location}") + + /** + * Adds an electric node to this block that has a physical presence in the form of several display entities. + * [RebarDirectionalBlock.facing] must be set before calling this method. + * + * @return [node] + */ + @ApiStatus.NonExtendable + fun addElectricPort(face: BlockFace, node: T, radius: Double, overrideMaterial: Material?): T { + val material = overrideMaterial ?: when (node) { + is ElectricNode.Connector -> Material.GRAY_CONCRETE + is ElectricNode.Consumer -> Material.LIME_CONCRETE + is ElectricNode.Acceptor -> Material.LIME_CONCRETE + is ElectricNode.Producer -> Material.RED_CONCRETE + } + val expandedRadius = radius - PORT_OUTER_SCALE / 2 + 0.001 + addEntity( + "outer_${node.id}", ItemDisplayBuilder() + .itemStack(ItemStackBuilder.of(material).addCustomModelDataString("electric_port_outer")) + .transformation(TransformBuilder().scale(PORT_OUTER_SCALE)) + .build(block.location.toCenterLocation().add(face.direction.multiply(expandedRadius))) + ) + addEntity( + "inner_${node.id}", ItemDisplayBuilder() + .itemStack(ItemStackBuilder.of(Material.BLACK_CONCRETE).addCustomModelDataString("electric_port_inner")) + .transformation(TransformBuilder().scale(PORT_INNER_SCALE)) + .build(block.location.toCenterLocation().add(face.direction.multiply(expandedRadius + 0.001 + PORT_OUTER_SCALE / 2 - PORT_INNER_SCALE / 2))) + ) + val interaction = addEntity( + "interaction_${node.id}", InteractionBuilder() + .width(PORT_OUTER_SCALE) + .height(PORT_OUTER_SCALE) + .build(block.location.toCenterLocation().add(face.direction.multiply(radius - 0.001))) + ) + interaction.persistentDataContainer.set(NODE_KEY, RebarSerializers.UUID, node.id) + WireConnectionService.addInteraction(interaction, node) + return addElectricNode(node) + } + + /** + * Adds an electric node to this block that has a physical presence in the form of several display entities. + * [RebarDirectionalBlock.facing] must be set before calling this method. Radius defaults to 0.5. + * + * @return [node] + */ + @ApiStatus.NonExtendable + fun addElectricPort(face: BlockFace, node: T): T = addElectricPort(face, node, 0.5, null) + + @ApiStatus.Internal + companion object : Listener { + + private const val PORT_OUTER_SCALE = 0.19f + private const val PORT_INNER_SCALE = PORT_OUTER_SCALE / 2 + + private val NODE_KEY = rebarKey("node") + + private val NODES_KEY = rebarKey("nodes") + private val NODES_TYPE = RebarSerializers.LIST.listTypeFrom(ElectricNode.PDC_TYPE) + + private val electricBlocks = IdentityHashMap>() + + @EventHandler(priority = EventPriority.MONITOR) + private fun onDeserialize(event: RebarBlockDeserializeEvent) { + val block = event.rebarBlock as? RebarElectricBlock ?: return + val nodes = event.pdc.get(NODES_KEY, NODES_TYPE)!!.toMutableList() + electricBlocks[block] = nodes + + for (node in nodes) { + ElectricityManager.addNode(node) + } + + for ((_, id) in block.heldEntities) { + val interaction = Bukkit.getEntity(id) as? Interaction ?: continue + val nodeId = interaction.persistentDataContainer.get(NODE_KEY, RebarSerializers.UUID) ?: continue + val node = ElectricityManager.getNodeById(nodeId) ?: continue + WireConnectionService.addInteraction(interaction, node) + } + } + + @EventHandler + private fun onSerialize(event: RebarBlockSerializeEvent) { + val block = event.rebarBlock as? RebarElectricBlock ?: return + event.pdc.set(NODES_KEY, NODES_TYPE, electricBlocks[block].orEmpty()) + } + + @EventHandler + private fun onUnload(event: RebarBlockUnloadEvent) { + if (event.rebarBlock !is RebarElectricBlock) return + for (node in electricBlocks.remove(event.rebarBlock).orEmpty()) { + ElectricityManager.removeNode(node) + } + } + + @EventHandler + private fun onBreak(event: RebarBlockBreakEvent) { + val block = event.rebarBlock + if (block !is RebarElectricBlock) return + for (node in electricBlocks.remove(block).orEmpty()) { + when (node) { + is ElectricNode.Connector -> { + for (connection in node.connections) { + ElectricityManager.getNodeById(connection)?.let { node.disconnectFrom(it) } + } + } + + is ElectricNode.Leaf -> node.disconnect() + } + ElectricityManager.removeNode(node) + } + } + } +} \ No newline at end of file diff --git a/rebar/src/main/kotlin/io/github/pylonmc/rebar/block/base/RebarElectricConsumerBlock.kt b/rebar/src/main/kotlin/io/github/pylonmc/rebar/block/base/RebarElectricConsumerBlock.kt new file mode 100644 index 000000000..b1d724c23 --- /dev/null +++ b/rebar/src/main/kotlin/io/github/pylonmc/rebar/block/base/RebarElectricConsumerBlock.kt @@ -0,0 +1,48 @@ +package io.github.pylonmc.rebar.block.base + +import io.github.pylonmc.rebar.electricity.ElectricNode +import io.github.pylonmc.rebar.event.RebarBlockPlaceEvent +import io.github.pylonmc.rebar.util.position.position +import org.bukkit.event.EventHandler +import org.bukkit.event.Listener +import org.jetbrains.annotations.ApiStatus + +/** + * Convenience interface for electric blocks that only have a single consumer node + */ +interface RebarElectricConsumerBlock : RebarElectricBlock, RebarDirectionalBlock { + + @get:ApiStatus.NonExtendable + val node: ElectricNode.Consumer + get() = electricNodes.single() as ElectricNode.Consumer + + @get:ApiStatus.NonExtendable + val isPowered: Boolean + get() = node.isPowered + + @get:ApiStatus.NonExtendable + @set:ApiStatus.NonExtendable + var requiredPower: Double + get() = node.requiredPower + set(value) { + node.requiredPower = value + } + + @ApiStatus.Internal + companion object : Listener { + + @EventHandler + private fun onPlace(event: RebarBlockPlaceEvent) { + val block = event.rebarBlock as? RebarElectricConsumerBlock ?: return + val blockPos = event.block.position + block.addElectricPort( + block.facing, + ElectricNode.Consumer( + name = "main", + block = blockPos, + requiredPower = 0.0 + ) + ) + } + } +} \ No newline at end of file diff --git a/rebar/src/main/kotlin/io/github/pylonmc/rebar/block/base/RebarElectricProducerBlock.kt b/rebar/src/main/kotlin/io/github/pylonmc/rebar/block/base/RebarElectricProducerBlock.kt new file mode 100644 index 000000000..4459855cf --- /dev/null +++ b/rebar/src/main/kotlin/io/github/pylonmc/rebar/block/base/RebarElectricProducerBlock.kt @@ -0,0 +1,44 @@ +package io.github.pylonmc.rebar.block.base + +import io.github.pylonmc.rebar.electricity.ElectricNode +import io.github.pylonmc.rebar.event.RebarBlockPlaceEvent +import io.github.pylonmc.rebar.util.position.position +import org.bukkit.event.EventHandler +import org.bukkit.event.Listener +import org.jetbrains.annotations.ApiStatus + +/** + * Convenience interface for electric blocks that only have a single producer node + */ +interface RebarElectricProducerBlock : RebarElectricBlock, RebarDirectionalBlock { + + @get:ApiStatus.NonExtendable + val node: ElectricNode.Producer + get() = electricNodes.single() as ElectricNode.Producer + + @get:ApiStatus.NonExtendable + @set:ApiStatus.NonExtendable + var power: Double + get() = node.power + set(value) { + node.power = value + } + + @ApiStatus.Internal + companion object : Listener { + + @EventHandler + private fun onPlace(event: RebarBlockPlaceEvent) { + val block = event.rebarBlock as? RebarElectricProducerBlock ?: return + val blockPos = event.block.position + block.addElectricPort( + block.facing, + ElectricNode.Producer( + name = "main", + block = blockPos, + power = 0.0 + ) + ) + } + } +} \ No newline at end of file diff --git a/rebar/src/main/kotlin/io/github/pylonmc/rebar/block/base/RebarEntityHolderBlock.kt b/rebar/src/main/kotlin/io/github/pylonmc/rebar/block/base/RebarEntityHolderBlock.kt index ed6724ff1..d53896b83 100644 --- a/rebar/src/main/kotlin/io/github/pylonmc/rebar/block/base/RebarEntityHolderBlock.kt +++ b/rebar/src/main/kotlin/io/github/pylonmc/rebar/block/base/RebarEntityHolderBlock.kt @@ -19,8 +19,7 @@ import org.bukkit.event.Listener import org.bukkit.event.entity.EntityRemoveEvent import org.bukkit.persistence.PersistentDataContainer import org.jetbrains.annotations.ApiStatus -import java.util.IdentityHashMap -import java.util.UUID +import java.util.* import java.util.function.Consumer /** @@ -37,16 +36,19 @@ interface RebarEntityHolderBlock { val heldEntities: MutableMap get() = holders.getOrPut(this) { mutableMapOf() } - fun addEntity(name: String, entity: Entity) { + fun addEntity(name: String, entity: T): T { heldEntities[name] = entity.uniqueId entity.persistentDataContainer.set(blockKey, RebarSerializers.BLOCK_POSITION, block.position) + return entity } - fun addEntity(name: String, entity: RebarEntity<*>) - = addEntity(name, entity.entity) + fun > addEntity(name: String, entity: T): T { + addEntity(name, entity.entity) + return entity + } fun tryRemoveEntity(name: String) { - val uuid = heldEntities[name] ?: return + val uuid = heldEntities.remove(name) ?: return Bukkit.getEntity(uuid)?.remove() } diff --git a/rebar/src/main/kotlin/io/github/pylonmc/rebar/block/base/RebarFallingBlock.kt b/rebar/src/main/kotlin/io/github/pylonmc/rebar/block/base/RebarFallingBlock.kt index 94363c396..3cc9854fd 100644 --- a/rebar/src/main/kotlin/io/github/pylonmc/rebar/block/base/RebarFallingBlock.kt +++ b/rebar/src/main/kotlin/io/github/pylonmc/rebar/block/base/RebarFallingBlock.kt @@ -48,7 +48,7 @@ interface RebarFallingBlock { /** * When called the block doesn't exist in the world and in [BlockStorage] */ - fun onItemDrop(event: EntityDropItemEvent, entity: RebarFallingBlockEntity) = RebarRegistry.ITEMS[(this as RebarBlock).key]?.getItemStack() + fun onItemDrop(event: EntityDropItemEvent, entity: RebarFallingBlockEntity) = RebarRegistry.ITEMS[(this as RebarBlock).key]?.createNewItemStack() class RebarFallingBlockEntity : RebarEntity { val fallStartPosition: BlockPosition @@ -77,7 +77,7 @@ interface RebarFallingBlock { } fun fallbackItem() : ItemStack? { - return this.entity.persistentDataContainer.get(FALLBACK_ITEM, RebarSerializers.ITEM_STACK) ?: RebarRegistry.ITEMS[blockSchema.key]?.getItemStack() + return this.entity.persistentDataContainer.get(FALLBACK_ITEM, RebarSerializers.ITEM_STACK) ?: RebarRegistry.ITEMS[blockSchema.key]?.createNewItemStack() } } diff --git a/rebar/src/main/kotlin/io/github/pylonmc/rebar/block/base/RebarFluidBufferBlock.kt b/rebar/src/main/kotlin/io/github/pylonmc/rebar/block/base/RebarFluidBufferBlock.kt index 32996790e..f5577fcbc 100644 --- a/rebar/src/main/kotlin/io/github/pylonmc/rebar/block/base/RebarFluidBufferBlock.kt +++ b/rebar/src/main/kotlin/io/github/pylonmc/rebar/block/base/RebarFluidBufferBlock.kt @@ -10,7 +10,7 @@ import io.github.pylonmc.rebar.util.rebarKey import org.bukkit.event.EventHandler import org.bukkit.event.Listener import org.jetbrains.annotations.ApiStatus -import java.util.IdentityHashMap +import java.util.* import kotlin.math.max /** @@ -124,7 +124,7 @@ interface RebarFluidBufferBlock : RebarFluidBlock { * Removes from a fluid buffer only if the new amount of fluid is greater * than zero and fits in the buffer. * - * @return true only if the buffer was added to successfully + * @return true only if the buffer was removed from successfully */ fun removeFluid(fluid: RebarFluid, amount: Double): Boolean { return setFluid(fluid, fluidData(fluid).amount - amount) @@ -163,8 +163,7 @@ interface RebarFluidBufferBlock : RebarFluidBlock { private fun onDeserialize(event: RebarBlockDeserializeEvent) { val block = event.rebarBlock if (block is RebarFluidBufferBlock) { - bufferFluidBlocks[block] = event.pdc.get(fluidBuffersKey, fluidBuffersType)?.toMutableMap() - ?: error("Fluid buffers not found for ${block.key}") + bufferFluidBlocks[block] = event.pdc.get(fluidBuffersKey, fluidBuffersType).orEmpty().toMutableMap() } } @@ -172,7 +171,7 @@ interface RebarFluidBufferBlock : RebarFluidBlock { private fun onSerialize(event: RebarBlockSerializeEvent) { val block = event.rebarBlock if (block is RebarFluidBufferBlock) { - event.pdc.set(fluidBuffersKey, fluidBuffersType, bufferFluidBlocks[block]!!) + event.pdc.set(fluidBuffersKey, fluidBuffersType, bufferFluidBlocks[block].orEmpty()) } } diff --git a/rebar/src/main/kotlin/io/github/pylonmc/rebar/block/base/RebarGhostBlockHolder.kt b/rebar/src/main/kotlin/io/github/pylonmc/rebar/block/base/RebarGhostBlockHolder.kt index e452e96d8..ae1f18c1f 100644 --- a/rebar/src/main/kotlin/io/github/pylonmc/rebar/block/base/RebarGhostBlockHolder.kt +++ b/rebar/src/main/kotlin/io/github/pylonmc/rebar/block/base/RebarGhostBlockHolder.kt @@ -11,7 +11,6 @@ import io.github.pylonmc.rebar.entity.display.transform.TransformBuilder import io.github.pylonmc.rebar.event.RebarBlockLoadEvent import io.github.pylonmc.rebar.registry.RebarRegistry import io.github.pylonmc.rebar.util.rebarKey -import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.time.delay import org.bukkit.Color @@ -29,7 +28,6 @@ import org.bukkit.util.Vector import org.jetbrains.annotations.MustBeInvokedByOverriders import org.joml.Vector3i import java.time.Duration -import kotlin.time.Duration.Companion.seconds /** * A wrapper over [RebarEntityHolderBlock] which allows for 'ghost blocks' (like those used @@ -120,7 +118,7 @@ interface RebarGhostBlockHolder : RebarEntityHolderBlock { constructor(ghostBlockHolder: Block, position: Vector3i, items: MutableList) : super( KEY, ItemDisplayBuilder() - .itemStack(RebarRegistry.ITEMS.getOrThrow(items.first()).getItemStack()) + .itemStack(RebarRegistry.ITEMS.getOrThrow(items.first()).createNewItemStack()) .glow(Color.WHITE) .transformation(TransformBuilder().scale(0.501)) .build(ghostBlockHolder.location.toCenterLocation().add(Vector.fromJOML(position))), @@ -139,7 +137,7 @@ interface RebarGhostBlockHolder : RebarEntityHolderBlock { } fun setIndex(i: Int) { - entity.setItemStack(RebarRegistry.ITEMS.getOrThrow(rebarBlocks[i]).getItemStack()) + entity.setItemStack(RebarRegistry.ITEMS.getOrThrow(rebarBlocks[i]).createNewItemStack()) } companion object { diff --git a/rebar/src/main/kotlin/io/github/pylonmc/rebar/block/base/RebarGuiBlock.kt b/rebar/src/main/kotlin/io/github/pylonmc/rebar/block/base/RebarGuiBlock.kt index c712c5d06..ecbea2470 100644 --- a/rebar/src/main/kotlin/io/github/pylonmc/rebar/block/base/RebarGuiBlock.kt +++ b/rebar/src/main/kotlin/io/github/pylonmc/rebar/block/base/RebarGuiBlock.kt @@ -7,13 +7,13 @@ import io.github.pylonmc.rebar.event.RebarBlockLoadEvent import io.github.pylonmc.rebar.event.RebarBlockPlaceEvent import io.github.pylonmc.rebar.event.RebarBlockUnloadEvent import net.kyori.adventure.text.Component +import org.bukkit.entity.Player import org.bukkit.event.Event import org.bukkit.event.EventHandler import org.bukkit.event.EventPriority import org.bukkit.event.Listener import org.bukkit.event.player.PlayerInteractEvent import org.bukkit.inventory.EquipmentSlot -import org.jetbrains.annotations.MustBeInvokedByOverriders import xyz.xenondevs.invui.gui.Gui import xyz.xenondevs.invui.inventory.VirtualInventory import xyz.xenondevs.invui.window.Window @@ -43,6 +43,13 @@ interface RebarGuiBlock : RebarBreakHandler, RebarNoVanillaContainerBlock { val guiTitle: Component get() = (this as RebarBlock).nameTranslationKey + fun openWindow(player: Player) { + Window.builder() + .setUpperGui(guiBlocks[this] ?: error("GUI not found for block")) + .setTitle(guiTitle) + .open(player) + } + companion object : Listener { private val guiBlocks = IdentityHashMap() @@ -75,12 +82,7 @@ interface RebarGuiBlock : RebarBreakHandler, RebarNoVanillaContainerBlock { event.setUseInteractedBlock(Event.Result.DENY) event.setUseItemInHand(Event.Result.DENY) - Window.builder() - .setUpperGui(guiBlocks[guiBlock]!!) - .setTitle(guiBlock.guiTitle) - .setViewer(event.player) - .build() - .open() + guiBlock.openWindow(event.player) } @EventHandler diff --git a/rebar/src/main/kotlin/io/github/pylonmc/rebar/block/base/RebarSimpleMultiblock.kt b/rebar/src/main/kotlin/io/github/pylonmc/rebar/block/base/RebarSimpleMultiblock.kt index 77c5d2aba..3c616c541 100644 --- a/rebar/src/main/kotlin/io/github/pylonmc/rebar/block/base/RebarSimpleMultiblock.kt +++ b/rebar/src/main/kotlin/io/github/pylonmc/rebar/block/base/RebarSimpleMultiblock.kt @@ -1,15 +1,10 @@ package io.github.pylonmc.rebar.block.base import io.github.pylonmc.rebar.block.BlockStorage -import io.github.pylonmc.rebar.datatypes.RebarSerializers -import io.github.pylonmc.rebar.event.RebarBlockDeserializeEvent import io.github.pylonmc.rebar.event.RebarBlockPlaceEvent -import io.github.pylonmc.rebar.event.RebarBlockSerializeEvent -import io.github.pylonmc.rebar.event.RebarBlockUnloadEvent import io.github.pylonmc.rebar.util.getRelative import io.github.pylonmc.rebar.util.position.ChunkPosition import io.github.pylonmc.rebar.util.position.position -import io.github.pylonmc.rebar.util.rebarKey import io.github.pylonmc.rebar.util.rotateVectorToFace import io.github.pylonmc.rebar.waila.Waila import io.github.pylonmc.rebar.waila.WailaDisplay @@ -41,7 +36,7 @@ import kotlin.math.min * If you need something more flexible (eg: a fluid tank that can have up to 10 * fluid casings added to increase the capacity), see [RebarMultiblock]. */ -interface RebarSimpleMultiblock : RebarMultiblock, RebarGhostBlockHolder, RebarEntityCulledBlock { +interface RebarSimpleMultiblock : RebarMultiblock, RebarGhostBlockHolder, RebarEntityCulledBlock, RebarDirectionalBlock { /** * Represents a single block of a multiblock. @@ -122,7 +117,8 @@ interface RebarSimpleMultiblock : RebarMultiblock, RebarGhostBlockHolder, RebarE ) @JvmStatic - fun of(vararg blockDatas: BlockData) = MultiblockComponent( + fun of(vararg blockDatas: BlockData) = + MultiblockComponent( blockDatas.toList(), listOf() ) @@ -139,9 +135,6 @@ interface RebarSimpleMultiblock : RebarMultiblock, RebarGhostBlockHolder, RebarE } } - private val simpleMultiblockData: SimpleMultiblockData - get() = simpleMultiblocks.getOrPut(this) { SimpleMultiblockData(null) } - /** * The positions and corresponding components of the multiblock. * @@ -155,41 +148,6 @@ interface RebarSimpleMultiblock : RebarMultiblock, RebarGhostBlockHolder, RebarE */ fun getWaila(player: Player): WailaDisplay? - /** - * Sets the 'direction' we expect the multiblock to be built in. North is considered the default facing direction - - * ie setFacing(BlockFace.NORTH) will preserve the original multiblock structure without rotating it. - * - * Leave this unset to accept any direction. - */ - fun setMultiblockDirection(direction: BlockFace?) { - simpleMultiblockData.direction = direction - } - - /** - * The 'direction' we expect the multiblock to be built in. This is not the *actual* direction that - * the multiblock has been built in. - */ - fun getMultiblockDirection(): BlockFace? - = simpleMultiblockData.direction - - /** - * Returns all the valid configurations of the multiblock. If any of these is satisfied, the multiblock - * will be considered complete. - */ - fun validStructures(): List> { - val facing = simpleMultiblockData.direction - return if (facing == null) { - listOf( - components, - rotateComponentsToFace(components, BlockFace.EAST), - rotateComponentsToFace(components, BlockFace.SOUTH), - rotateComponentsToFace(components, BlockFace.WEST) - ) - } else { - listOf(rotateComponentsToFace(components, facing)) - } - } - // Just assumes any rotation of the multiblock is valid, probably not worth the extra logic to account for // different facing directions. /** @@ -217,17 +175,9 @@ interface RebarSimpleMultiblock : RebarMultiblock, RebarGhostBlockHolder, RebarE val maxCorner: Vector3i get() = Vector3i(horizontalRadius, components.keys.maxOf { it.y }, horizontalRadius) - fun getMultiblockBlock(position: Vector3i) - = block.getRelative(getRotatedPosition(position)) + fun getRotatedPosition(position: Vector3i) = rotateVectorToFace(position, facing) - fun getRotatedPosition(rotatedPosition: Vector3i): Vector3i { - val direction = getMultiblockDirection() - return if (direction != null) { - rotateVectorToFace(rotatedPosition, direction) - } else { - rotatedPosition - } - } + fun getMultiblockBlock(position: Vector3i) = block.getRelative(getRotatedPosition(position)) fun getMultiblockComponent(position: Vector3i) = BlockStorage.get(getMultiblockBlock(position)) @@ -239,7 +189,8 @@ interface RebarSimpleMultiblock : RebarMultiblock, RebarGhostBlockHolder, RebarE getMultiblockComponent(position) ?: throw IllegalStateException("There is no Rebar block at $position") fun getMultiblockComponentOrThrow(clazz: Class, position: Vector3i) = - getMultiblockComponent(clazz, position) ?: throw IllegalStateException("There is no Rebar block at $position or it is not of type $clazz") + getMultiblockComponent(clazz, position) + ?: throw IllegalStateException("There is no Rebar block at $position or it is not of type $clazz") override val chunksOccupied: Set get() { @@ -257,10 +208,8 @@ interface RebarSimpleMultiblock : RebarMultiblock, RebarGhostBlockHolder, RebarE override fun checkFormed(): Boolean { // Actual formed checking logic - val formed = validStructures().any { struct -> - struct.all { - it.value.matches(block.location.add(Vector.fromJOML(it.key)).block) - } + val formed = rotateComponentsToFace(components, facing).all { + it.value.matches(block.location.add(Vector.fromJOML(it.key)).block) } for ((position, component) in components) { @@ -292,9 +241,8 @@ interface RebarSimpleMultiblock : RebarMultiblock, RebarGhostBlockHolder, RebarE } } - override fun isPartOfMultiblock(otherBlock: Block): Boolean = validStructures().any { - it.contains((otherBlock.position - block.position).vector3i) - } + override fun isPartOfMultiblock(otherBlock: Block): Boolean = + (otherBlock.position - block.position).toVector3i() in rotateComponentsToFace(components, facing) override val culledEntityIds: Iterable get() = heldEntities.values @@ -302,12 +250,6 @@ interface RebarSimpleMultiblock : RebarMultiblock, RebarGhostBlockHolder, RebarE @ApiStatus.Internal companion object : Listener { - internal data class SimpleMultiblockData(var direction: BlockFace?) - - private val simpleMultiblockKey = rebarKey("simple_multiblock_data") - - private val simpleMultiblocks = IdentityHashMap() - @EventHandler private fun onPlace(event: RebarBlockPlaceEvent) { val block = event.rebarBlock @@ -317,33 +259,8 @@ interface RebarSimpleMultiblock : RebarMultiblock, RebarGhostBlockHolder, RebarE } } - @EventHandler - private fun onDeserialize(event: RebarBlockDeserializeEvent) { - val block = event.rebarBlock - if (block is RebarSimpleMultiblock) { - simpleMultiblocks[block] = event.pdc.get(simpleMultiblockKey, RebarSerializers.SIMPLE_MULTIBLOCK_DATA) - ?: error("Simple multiblock data not found for ${block.key}") - } - } - - @EventHandler - private fun onSerialize(event: RebarBlockSerializeEvent) { - val block = event.rebarBlock - if (block is RebarSimpleMultiblock) { - event.pdc.set(simpleMultiblockKey, RebarSerializers.SIMPLE_MULTIBLOCK_DATA, simpleMultiblocks[block]!!) - } - } - - @EventHandler - private fun onUnload(event: RebarBlockUnloadEvent) { - val block = event.rebarBlock - if (block is RebarSimpleMultiblock) { - simpleMultiblocks.remove(block) - } - } - @JvmStatic - fun rotateComponentsToFace(components: Map, face: BlockFace) - = components.mapKeys { rotateVectorToFace(it.key, face) } + fun rotateComponentsToFace(components: Map, face: BlockFace) = + components.mapKeys { rotateVectorToFace(it.key, face) } } } \ No newline at end of file diff --git a/rebar/src/main/kotlin/io/github/pylonmc/rebar/block/base/RebarTickingBlock.kt b/rebar/src/main/kotlin/io/github/pylonmc/rebar/block/base/RebarTickingBlock.kt index b5ce00e40..5ed6d8103 100644 --- a/rebar/src/main/kotlin/io/github/pylonmc/rebar/block/base/RebarTickingBlock.kt +++ b/rebar/src/main/kotlin/io/github/pylonmc/rebar/block/base/RebarTickingBlock.kt @@ -17,7 +17,7 @@ import kotlinx.coroutines.* import org.bukkit.event.EventHandler import org.bukkit.event.Listener import org.jetbrains.annotations.ApiStatus -import java.util.IdentityHashMap +import java.util.* /** * Represents a block that 'ticks' (does something at a fixed time interval). @@ -37,14 +37,21 @@ interface RebarTickingBlock { * The interval at which the [tick] function is called. You should generally use [setTickInterval] * in your place constructor instead of overriding this. */ - val tickInterval + val tickInterval: Int get() = tickingData.tickInterval + /** + * How many times the block ticks per second + */ + @get:ApiStatus.NonExtendable + val ticksPerSecond: Double + get() = 20.0 / tickInterval + /** * Whether the [tick] function should be called asynchronously. You should generally use * [setAsync] in your place constructor instead of overriding this. */ - val isAsync + val isAsync: Boolean get() = tickingData.isAsync /** @@ -103,7 +110,7 @@ interface RebarTickingBlock { private fun onSerialize(event: RebarBlockSerializeEvent) { val block = event.rebarBlock if (block is RebarTickingBlock) { - event.pdc.set(tickingBlockKey, RebarSerializers.TICKING_BLOCK_DATA, tickingBlocks[block]!!) + event.pdc.set(tickingBlockKey, RebarSerializers.TICKING_BLOCK_DATA, block.tickingData) } } diff --git a/rebar/src/main/kotlin/io/github/pylonmc/rebar/command/DualItemRegistryCommandArgument.kt b/rebar/src/main/kotlin/io/github/pylonmc/rebar/command/DualItemRegistryCommandArgument.kt index 9796192d0..caa36ab50 100644 --- a/rebar/src/main/kotlin/io/github/pylonmc/rebar/command/DualItemRegistryCommandArgument.kt +++ b/rebar/src/main/kotlin/io/github/pylonmc/rebar/command/DualItemRegistryCommandArgument.kt @@ -38,7 +38,7 @@ object DualItemRegistryCommandArgument : CustomArgumentType.Converted { @@ -130,6 +124,7 @@ interface ConfigAdapter { @JvmField val REBAR_FLUID = KEYED.fromRegistry(RebarRegistry.FLUIDS) @JvmField val FLUID_TEMPERATURE = ENUM.from() @JvmField val FLUID_OR_ITEM = FluidOrItemConfigAdapter + @JvmField val FLUID_WITH_AMOUNT = FluidWithAmountConfigAdapter @JvmField val RECIPE_INPUT = RecipeInputConfigAdapter @JvmField val RECIPE_INPUT_ITEM = RecipeInputItemAdapter @JvmField val RECIPE_INPUT_FLUID = RecipeInputFluidAdapter diff --git a/rebar/src/main/kotlin/io/github/pylonmc/rebar/config/adapter/FluidWithAmountConfigAdapter.kt b/rebar/src/main/kotlin/io/github/pylonmc/rebar/config/adapter/FluidWithAmountConfigAdapter.kt new file mode 100644 index 000000000..0142cef0f --- /dev/null +++ b/rebar/src/main/kotlin/io/github/pylonmc/rebar/config/adapter/FluidWithAmountConfigAdapter.kt @@ -0,0 +1,22 @@ +package io.github.pylonmc.rebar.config.adapter + +import io.github.pylonmc.rebar.fluid.FluidWithAmount +import org.bukkit.configuration.ConfigurationSection + +object FluidWithAmountConfigAdapter : ConfigAdapter { + + override val type = FluidWithAmount::class.java + + override fun convert(value: Any): FluidWithAmount { + return when (value) { + is Pair<*, *> -> { + val fluid = ConfigAdapter.REBAR_FLUID.convert(value.first!!) + val amount = ConfigAdapter.DOUBLE.convert(value.second!!) + FluidWithAmount(fluid, amount) + } + + is ConfigurationSection, is Map<*, *> -> convert(MapConfigAdapter.STRING_TO_ANY.convert(value).toList().single()) + else -> throw IllegalArgumentException("Cannot convert $value to FluidWithAmount") + } + } +} \ No newline at end of file diff --git a/rebar/src/main/kotlin/io/github/pylonmc/rebar/content/cargo/CargoDuct.kt b/rebar/src/main/kotlin/io/github/pylonmc/rebar/content/cargo/CargoDuct.kt index 81b023bc9..a2d49dc37 100644 --- a/rebar/src/main/kotlin/io/github/pylonmc/rebar/content/cargo/CargoDuct.kt +++ b/rebar/src/main/kotlin/io/github/pylonmc/rebar/content/cargo/CargoDuct.kt @@ -48,7 +48,7 @@ class CargoDuct : RebarBlock, RebarBreakHandler, RebarEntityHolderBlock, RebarEn connectedFaces = pdc.get(connectedFacesKey, connectedFacesType)!!.toMutableList() } - override fun postLoad() { + override fun postLoad(pdc: PersistentDataContainer) { for (face in connectedFaces) { val displayId = getHeldEntityUuid(ductDisplayName(face)) ?: continue EntityStorage.whenEntityLoads(displayId) { display: ItemDisplay -> diff --git a/rebar/src/main/kotlin/io/github/pylonmc/rebar/content/fluid/FluidPipeDisplay.kt b/rebar/src/main/kotlin/io/github/pylonmc/rebar/content/fluid/FluidPipeDisplay.kt index bae3d841d..6575fa7a5 100644 --- a/rebar/src/main/kotlin/io/github/pylonmc/rebar/content/fluid/FluidPipeDisplay.kt +++ b/rebar/src/main/kotlin/io/github/pylonmc/rebar/content/fluid/FluidPipeDisplay.kt @@ -7,7 +7,6 @@ import io.github.pylonmc.rebar.entity.display.ItemDisplayBuilder import io.github.pylonmc.rebar.entity.display.transform.LineBuilder import io.github.pylonmc.rebar.fluid.FluidManager import io.github.pylonmc.rebar.fluid.placement.FluidPipePlacementService -import io.github.pylonmc.rebar.item.RebarItem import io.github.pylonmc.rebar.item.builder.ItemStackBuilder import io.github.pylonmc.rebar.registry.RebarRegistry import io.github.pylonmc.rebar.util.rebarKey @@ -16,7 +15,7 @@ import org.bukkit.entity.ItemDisplay import org.bukkit.entity.Player import org.bukkit.inventory.ItemStack import org.bukkit.persistence.PersistentDataContainer -import java.util.UUID +import java.util.* /** * A display that visually represents a pipe. @@ -46,7 +45,7 @@ class FluidPipeDisplay : RebarEntity { // will fail to load if schema not found; no way around this val pipeSchema = pdc.get(PIPE_KEY, PIPE_TYPE)!! - this.pipe = pipeSchema.getRebarItem() as FluidPipe + this.pipe = pipeSchema.createNewRebarItem() as FluidPipe this.pipeAmount = pdc.get(PIPE_AMOUNT_KEY, RebarSerializers.INTEGER)!! @@ -101,7 +100,7 @@ class FluidPipeDisplay : RebarEntity { } else if (drops != null) { drops.add(itemToGive) } else { - val location = to.point.position.plus(from.point.position).location.multiply(0.5) + val location = to.point.position.plus(from.point.position).toLocation().multiply(0.5) location.getWorld().dropItemNaturally(location, itemToGive) } diff --git a/rebar/src/main/kotlin/io/github/pylonmc/rebar/datatypes/DelegatingPersistentDataType.kt b/rebar/src/main/kotlin/io/github/pylonmc/rebar/datatypes/DelegatingPersistentDataType.kt index 9feabbedc..e3aad8a4f 100644 --- a/rebar/src/main/kotlin/io/github/pylonmc/rebar/datatypes/DelegatingPersistentDataType.kt +++ b/rebar/src/main/kotlin/io/github/pylonmc/rebar/datatypes/DelegatingPersistentDataType.kt @@ -28,4 +28,14 @@ abstract class DelegatingPersistentDataType

( final override fun fromPrimitive(primitive: P, context: PersistentDataAdapterContext): C { return fromDelegatedType(delegate.fromPrimitive(primitive, context)) } +} + +@JvmSynthetic +inline fun

PersistentDataType.map( + crossinline to: (C) -> D, + crossinline from: (D) -> C +): PersistentDataType = object : DelegatingPersistentDataType(this) { + override fun getComplexType() = C::class.java + override fun toDelegatedType(complex: C): D = to(complex) + override fun fromDelegatedType(primitive: D): C = from(primitive) } \ No newline at end of file diff --git a/rebar/src/main/kotlin/io/github/pylonmc/rebar/datatypes/PairPersistentDataType.kt b/rebar/src/main/kotlin/io/github/pylonmc/rebar/datatypes/PairPersistentDataType.kt new file mode 100644 index 000000000..9314a6b54 --- /dev/null +++ b/rebar/src/main/kotlin/io/github/pylonmc/rebar/datatypes/PairPersistentDataType.kt @@ -0,0 +1,41 @@ +package io.github.pylonmc.rebar.datatypes + +import io.github.pylonmc.rebar.util.rebarKey +import org.bukkit.persistence.PersistentDataAdapterContext +import org.bukkit.persistence.PersistentDataContainer +import org.bukkit.persistence.PersistentDataType + +class PairPersistentDataType( + val first: PersistentDataType<*, F>, + val second: PersistentDataType<*, S> +) : PersistentDataType> { + + override fun getPrimitiveType() = PersistentDataContainer::class.java + + @Suppress("UNCHECKED_CAST") + override fun getComplexType() = Pair::class.java as Class> + + override fun toPrimitive(complex: Pair, context: PersistentDataAdapterContext): PersistentDataContainer { + val container = context.newPersistentDataContainer() + container.set(firstKey, first, complex.first) + container.set(secondKey, second, complex.second) + return container + } + + override fun fromPrimitive(primitive: PersistentDataContainer, context: PersistentDataAdapterContext): Pair { + val firstValue = primitive.get(firstKey, first)!! + val secondValue = primitive.get(secondKey, second)!! + return Pair(firstValue, secondValue) + } + + companion object { + private val firstKey = rebarKey("first") + private val secondKey = rebarKey("second") + + @JvmStatic + fun pairTypeFrom( + first: PersistentDataType<*, F>, + second: PersistentDataType<*, S> + ): PersistentDataType> = PairPersistentDataType(first, second) + } +} \ No newline at end of file diff --git a/rebar/src/main/kotlin/io/github/pylonmc/rebar/datatypes/PolymorphicPersistentDataType.kt b/rebar/src/main/kotlin/io/github/pylonmc/rebar/datatypes/PolymorphicPersistentDataType.kt new file mode 100644 index 000000000..2ae3c2b62 --- /dev/null +++ b/rebar/src/main/kotlin/io/github/pylonmc/rebar/datatypes/PolymorphicPersistentDataType.kt @@ -0,0 +1,67 @@ +package io.github.pylonmc.rebar.datatypes + +import io.github.pylonmc.rebar.util.rebarKey +import org.bukkit.persistence.PersistentDataAdapterContext +import org.bukkit.persistence.PersistentDataContainer +import org.bukkit.persistence.PersistentDataType + +/** + * A [PersistentDataType] that can serialize multiple different types, as long as they all share a common supertype + */ +class PolymorphicPersistentDataType(types: List>) : PersistentDataType { + + private val supertype: Class<*> = types.map { it.getComplexType() as Class<*> }.reduce { acc, clazz -> acc.findCommonSupertype(clazz) } + private val classToType = types.associateBy { it.getComplexType() as Class<*> } + private val tagToType = classToType.mapKeys { (key, _) -> key.name } + + override fun getPrimitiveType() = PersistentDataContainer::class.java + + @Suppress("UNCHECKED_CAST") + override fun getComplexType() = supertype as Class + + override fun toPrimitive( + complex: T, + context: PersistentDataAdapterContext + ): PersistentDataContainer { + val clazz = complex::class.java + val type = classToType[clazz] ?: throw IllegalArgumentException("Unsupported type: ${clazz.name}") + val pdc = context.newPersistentDataContainer() + pdc.set(TAG_KEY, PersistentDataType.STRING, clazz.name) + @Suppress("UNCHECKED_CAST") + pdc.set(VALUE_KEY, type as PersistentDataType<*, T>, complex) + return pdc + } + + override fun fromPrimitive( + primitive: PersistentDataContainer, + context: PersistentDataAdapterContext + ): T { + val tag = primitive.get(TAG_KEY, PersistentDataType.STRING)!! + val type = tagToType[tag] ?: throw IllegalArgumentException("Unsupported tag: $tag") + return primitive.get(VALUE_KEY, type)!! + } + + companion object { + private val TAG_KEY = rebarKey("tag") + private val VALUE_KEY = rebarKey("value") + + @JvmStatic + fun of(vararg types: PersistentDataType<*, out T>): PersistentDataType { + return PolymorphicPersistentDataType(types.toList()) + } + + @JvmStatic + fun of(types: Collection>): PersistentDataType { + return PolymorphicPersistentDataType(types.toList()) + } + } +} + +private fun Class<*>.findCommonSupertype(other: Class<*>): Class<*> { + if (this == other) return this + + val thisSupers = generateSequence(this) { it.superclass }.toSet() + val otherSupers = generateSequence(other) { it.superclass }.toSet() + + return thisSupers.intersect(otherSupers).first() +} \ No newline at end of file diff --git a/rebar/src/main/kotlin/io/github/pylonmc/rebar/datatypes/RebarSerializers.kt b/rebar/src/main/kotlin/io/github/pylonmc/rebar/datatypes/RebarSerializers.kt index a9ec243a9..48bdb3da3 100644 --- a/rebar/src/main/kotlin/io/github/pylonmc/rebar/datatypes/RebarSerializers.kt +++ b/rebar/src/main/kotlin/io/github/pylonmc/rebar/datatypes/RebarSerializers.kt @@ -65,6 +65,15 @@ object RebarSerializers { @JvmField val ENUM = EnumPersistentDataType + @JvmField + val PAIR = PairPersistentDataType + + /** + * @see [PolymorphicPersistentDataType] + */ + @JvmField + val POLYMORPHIC = PolymorphicPersistentDataType + @JvmField val NAMESPACED_KEY = NamespacedKeyPersistentDataType @@ -165,9 +174,6 @@ object RebarSerializers { @JvmSynthetic internal val RECIPE_PROCESSOR_DATA = RecipeProcessorDataPersistentDataType - @JvmSynthetic - internal val SIMPLE_MULTIBLOCK_DATA = SimpleMultiblockDataPersistentDataType - @JvmSynthetic internal val TICKING_BLOCK_DATA = TickingBlockPersistentDataType diff --git a/rebar/src/main/kotlin/io/github/pylonmc/rebar/datatypes/SimpleMultiblockDataPersistentDataType.kt b/rebar/src/main/kotlin/io/github/pylonmc/rebar/datatypes/SimpleMultiblockDataPersistentDataType.kt deleted file mode 100644 index 92944c8d9..000000000 --- a/rebar/src/main/kotlin/io/github/pylonmc/rebar/datatypes/SimpleMultiblockDataPersistentDataType.kt +++ /dev/null @@ -1,33 +0,0 @@ -package io.github.pylonmc.rebar.datatypes - -import io.github.pylonmc.rebar.block.base.RebarSimpleMultiblock -import io.github.pylonmc.rebar.util.rebarKey -import io.github.pylonmc.rebar.util.setNullable -import org.bukkit.persistence.PersistentDataAdapterContext -import org.bukkit.persistence.PersistentDataContainer -import org.bukkit.persistence.PersistentDataType - -internal object SimpleMultiblockDataPersistentDataType : PersistentDataType { - val facingKey = rebarKey("facing") - - override fun getPrimitiveType(): Class = PersistentDataContainer::class.java - - override fun getComplexType(): Class = RebarSimpleMultiblock.Companion.SimpleMultiblockData::class.java - - override fun fromPrimitive( - primitive: PersistentDataContainer, - context: PersistentDataAdapterContext - ): RebarSimpleMultiblock.Companion.SimpleMultiblockData { - val facing = primitive.get(facingKey, RebarSerializers.BLOCK_FACE) - return RebarSimpleMultiblock.Companion.SimpleMultiblockData(facing) - } - - override fun toPrimitive( - complex: RebarSimpleMultiblock.Companion.SimpleMultiblockData, - context: PersistentDataAdapterContext - ): PersistentDataContainer { - val pdc = context.newPersistentDataContainer() - pdc.setNullable(facingKey, RebarSerializers.BLOCK_FACE, complex.direction) - return pdc - } -} \ No newline at end of file diff --git a/rebar/src/main/kotlin/io/github/pylonmc/rebar/electricity/ElectricNetwork.kt b/rebar/src/main/kotlin/io/github/pylonmc/rebar/electricity/ElectricNetwork.kt new file mode 100644 index 000000000..2cf85b140 --- /dev/null +++ b/rebar/src/main/kotlin/io/github/pylonmc/rebar/electricity/ElectricNetwork.kt @@ -0,0 +1,365 @@ +package io.github.pylonmc.rebar.electricity + +import io.github.pylonmc.rebar.config.RebarConfig +import java.util.PriorityQueue +import java.util.UUID +import kotlin.collections.ArrayDeque +import kotlin.math.abs +import kotlin.math.min + +class ElectricNetwork { + + private val nodeMap: MutableMap = mutableMapOf() + val nodes: Set get() = nodeMap.values.toSet() + + private val producers = mutableSetOf() + private val consumers = mutableSetOf() + private val acceptors = mutableSetOf() + + /** + * A map of heuristics based on distance to consumers. + */ + private val heuristics = mutableMapOf>() + + fun addNode(node: ElectricNode) { + nodeMap[node.id] = node + when (node) { + is ElectricNode.Producer -> producers.add(node) + is ElectricNode.Consumer -> consumers.add(node) + is ElectricNode.Acceptor -> acceptors.add(node) + is ElectricNode.Connector -> {} + } + heuristics.clear() + } + + fun removeNode(node: ElectricNode) { + nodeMap.remove(node.id) + producers.remove(node) + consumers.remove(node) + acceptors.remove(node) + heuristics.clear() + } + + fun isPartOfNetwork(node: ElectricNode): Boolean = node.id in nodeMap + + fun tick() { + for (consumer in consumers) { + consumer.isPowered = false + } + + val surplusPower = producers.associateWithTo(mutableMapOf()) { it.power } + + // First, we distribute power from producers to consumers + val totalPowerProduced = producers.sumOf { it.power } + val validConsumers = consumers.associateWithTo(mutableMapOf()) { it.requiredPower } + var powerConsumedByConsumers = roundRobinFill( + validConsumers, + totalPowerProduced + ) + + // If any consumer isn't getting enough power, we remove the one with the lowest requirement and try again, + // until all remaining consumers are getting enough power, or we run out of consumers. + while (powerConsumedByConsumers.any { (consumer, power) -> power < consumer.requiredPower }) { + validConsumers.remove(validConsumers.minBy { it.value }.key) + powerConsumedByConsumers = roundRobinFill( + validConsumers, + totalPowerProduced + ) + } + + // Then we invert that: knowing how much power was consumed, we calculate how much was taken from each producer + val powerTakenFromProducers = roundRobinFill( + producers.associateWith { it.power }, + powerConsumedByConsumers.values.sum() + ).toMutableMap() + + for ((producer, taken) in powerTakenFromProducers) { + surplusPower[producer] = surplusPower[producer]!! - taken + } + + // Now that we know what consumes and produces what, we can try routing said power + val limits = mutableMapOf() + var edgeLoads = mapOf() + val disconnectedEdges = mutableSetOf() + for ((consumer, consumed) in powerConsumedByConsumers) { + var powerLeft = consumed + while (!(powerLeft roughlyEquals 0.0)) { + var noPath = 0 + for ((producer, produced) in powerTakenFromProducers) { + if (produced roughlyEquals 0.0) continue + val path = findBestPath(producer, consumer, disconnectedEdges) + if (path == null) { + noPath++ + break + } + + // Determine limits on edges if not already known + for (edge in path) { + if (edge in limits) continue + limits[edge] = ElectricityManager.getMaxPower(edge.from, edge.to) + } + + val loadResult = calculateLoadOnEdges(path, edgeLoads, limits, produced) + val powerDelivered = min(loadResult.finalPower, powerLeft) + edgeLoads = loadResult.currents + powerLeft -= powerDelivered + powerTakenFromProducers[producer] = powerTakenFromProducers[producer]!! - powerDelivered + for ((edge, load) in loadResult.currents) { + if (load roughlyEquals limits[edge]!!) { + disconnectedEdges.add(edge) + } + } + } + + for ((producer, produced) in powerTakenFromProducers.toList()) { + if (produced roughlyEquals 0.0) { + powerTakenFromProducers.remove(producer) + } + } + + if (noPath == powerTakenFromProducers.size) { + // no paths from any producer to this consumer, give up + break + } + } + + if (powerLeft roughlyEquals 0.0 && consumed roughlyEquals consumer.requiredPower) { + consumer.isPowered = true + } + } + + for ((producer, taken) in powerTakenFromProducers) { + surplusPower[producer] = surplusPower[producer]!! + taken + } + + do { + var notAccepted = 0 + acceptorLoop@ for (acceptor in acceptors) { + var remaining = surplusPower.values.sum() / acceptors.size + if (remaining roughlyEquals 0.0) { + notAccepted++ + continue + } + var noPath = 0 + for ((producer, surplus) in surplusPower) { + if (surplus roughlyEquals 0.0) continue + val path = findBestPath(producer, acceptor, disconnectedEdges) + if (path == null) { + noPath++ + } else { + // Determine limits on edges if not already known + for (edge in path) { + if (edge in limits) continue + limits[edge] = ElectricityManager.getMaxPower(edge.from, edge.to) + } + + val loadResult = calculateLoadOnEdges(path, edgeLoads, limits, surplus) + val accepted = acceptor.handler.onAccept(loadResult.finalPower * POWER_ADJUSTMENT) + if (accepted roughlyEquals 0.0) { + notAccepted++ + continue@acceptorLoop + } else { + edgeLoads = loadResult.currents + remaining -= accepted + surplusPower[producer] = surplusPower[producer]!! - accepted + for ((edge, load) in loadResult.currents) { + if (load roughlyEquals limits[edge]!!) { + disconnectedEdges.add(edge) + } + } + } + } + } + + if (noPath == surplusPower.size) { + // no paths from any producer to this acceptor, give up + notAccepted++ + } + + for ((producer, surplus) in surplusPower.toList()) { + if (surplus roughlyEquals 0.0) { + surplusPower.remove(producer) + } + } + } + } while (notAccepted != acceptors.size) + + for (producer in producers) { + val taken = producer.power * POWER_ADJUSTMENT - (surplusPower[producer] ?: 0.0) + producer.powerTakeHandler.accept(taken) + } + } + + private fun roundRobinFill(limits: Map, amount: Double): Map { + val filled = mutableMapOf() + var remaining = amount + val limits = limits.toMutableMap() + while (!(remaining roughlyEquals 0.0) && limits.isNotEmpty()) { + val fillAmount = remaining / limits.size + for ((key, limit) in limits.toList()) { + val toFill = min(limit, fillAmount) + filled.merge(key, toFill, Double::plus) + remaining -= toFill + if (toFill >= limit) { + limits.remove(key) + } else { + limits[key] = limit - toFill + } + } + } + return filled + } + + /** + * Calculates the load for edges based on a greedy best first search from the producer to the consumer, using the graph distance to consumers as the heuristic. + */ +// I would've used A*, but in this case, since the heuristic is perfect, greedy best first search is more efficient. + private fun findBestPath( + producer: ElectricNode, + consumer: ElectricNode, + disconnectedEdges: Set, + ): List? { + if (heuristics.isEmpty()) recalculateDistanceHeuristics() + val heuristic = heuristics[consumer] + ?: throw IllegalArgumentException("Target node is not a consumer in this network") + val visited = mutableSetOf() + val queue = PriorityQueue(compareBy { heuristic[it]!! }) + queue.add(producer) + val inQueue = mutableSetOf(producer) + val cameFrom = mutableMapOf() + + fun queueNeighbor(node: ElectricNode, neighborId: UUID) { + val neighbor = nodeMap[neighborId] ?: return + if (neighbor in visited) return + if (Edge(node, neighbor) in disconnectedEdges || Edge(neighbor, node) in disconnectedEdges) return + if (neighbor !in inQueue) { + queue.add(neighbor) + inQueue.add(neighbor) + cameFrom[neighbor] = node + } + } + + while (queue.isNotEmpty()) { + val current = queue.poll() + inQueue.remove(current) + if (current == consumer) { + // reconstruct path + val path = mutableListOf() + var node = consumer + while (node != producer) { + val prev = cameFrom[node]!! + path.add(Edge(prev, node)) + node = prev + } + return path.asReversed() + } + visited.add(current) + when (current) { + is ElectricNode.Connector -> { + for (neighborId in current.connections) { + queueNeighbor(current, neighborId) + } + } + + is ElectricNode.Leaf -> current.connection?.let { queueNeighbor(current, it) } + } + } + return null + } + + private fun calculateLoadOnEdges( + path: List, + existingLoads: Map, + limits: Map, + initialPower: Double + ): LoadResult { + if (initialPower roughlyEquals 0.0) { + return LoadResult(existingLoads, 0.0) + } + val loads = existingLoads.toMutableMap() + var currentPower = initialPower + for (edge in path) { + val remainingCapacity = limits[edge]!! - (loads[edge] ?: 0.0) + currentPower = min(currentPower, remainingCapacity) + loads.merge(edge, currentPower, Double::plus) + } + return if (currentPower roughlyEquals initialPower) { + LoadResult(loads, currentPower) + } else { + // some limit has been hit somewhere, recalculate based on actual power delivered + calculateLoadOnEdges(path, existingLoads, limits, currentPower) + } + } + + private data class LoadResult(val currents: Map, val finalPower: Double) + + private fun recalculateDistanceHeuristics() { + heuristics.clear() + for (consumer in consumers + acceptors) { + val queue = ArrayDeque>(listOf(consumer to 0)) + val visited = mutableSetOf() + val distanceMap = mutableMapOf() + + fun queueNeighbor(node: ElectricNode, neighborId: UUID) { + val neighbor = nodeMap[neighborId] ?: return + if (neighbor in visited) return + queue.add(neighbor to (distanceMap[node]!! + 1)) + } + + while (queue.isNotEmpty()) { + val (current, distance) = queue.removeFirst() + if (current in visited) continue + visited.add(current) + distanceMap[current] = distance + when (current) { + is ElectricNode.Connector -> { + for (neighborId in current.connections) { + queueNeighbor(current, neighborId) + } + } + + is ElectricNode.Leaf -> current.connection?.let { queueNeighbor(current, it) } + } + } + heuristics[consumer] = distanceMap + } + } + + private data class Edge(val from: ElectricNode, val to: ElectricNode) + + companion object { + + private val POWER_ADJUSTMENT = RebarConfig.ELECTRICITY_TICK_INTERVAL / 20.0 + + fun tryMerge(network1: ElectricNetwork, network2: ElectricNetwork): ElectricNetwork? { + if (network1.nodeMap.size > network2.nodeMap.size) { + return tryMerge(network2, network1) + } + + if (network1.nodeMap.values.any { network2.nodeMap.values.any(it::isConnectedTo) }) { + val merged = ElectricNetwork() + merged.nodeMap.putAll(network1.nodeMap) + merged.nodeMap.putAll(network2.nodeMap) + merged.producers.addAll(network1.producers) + merged.producers.addAll(network2.producers) + merged.consumers.addAll(network1.consumers) + merged.consumers.addAll(network2.consumers) + merged.acceptors.addAll(network1.acceptors) + merged.acceptors.addAll(network2.acceptors) + return merged + } else { + return null + } + } + } +} + +private infix fun Double.roughlyEquals(other: Double): Boolean = abs(this - other) < 1e-6 + +private infix fun Map.roughlyEquals(other: Map): Boolean { + if (this.keys != other.keys) return false + for (key in this.keys) { + if (!(this[key]!! roughlyEquals other[key]!!)) return false + } + return true +} \ No newline at end of file diff --git a/rebar/src/main/kotlin/io/github/pylonmc/rebar/electricity/ElectricNode.kt b/rebar/src/main/kotlin/io/github/pylonmc/rebar/electricity/ElectricNode.kt new file mode 100644 index 000000000..6d2bf7447 --- /dev/null +++ b/rebar/src/main/kotlin/io/github/pylonmc/rebar/electricity/ElectricNode.kt @@ -0,0 +1,403 @@ +package io.github.pylonmc.rebar.electricity + +import io.github.pylonmc.rebar.datatypes.RebarSerializers +import io.github.pylonmc.rebar.util.position.BlockPosition +import io.github.pylonmc.rebar.util.rebarKey +import io.github.pylonmc.rebar.util.setNullable +import org.bukkit.persistence.PersistentDataAdapterContext +import org.bukkit.persistence.PersistentDataContainer +import org.bukkit.persistence.PersistentDataType +import org.jetbrains.annotations.ApiStatus +import java.util.* +import java.util.function.Consumer as ConsumerFn + +sealed class ElectricNode( + val id: UUID, + val name: String, + val block: BlockPosition +) { + + val network: ElectricNetwork get() = ElectricityManager.getNodeNetwork(this) + + protected var onConnect = ConnectDisconnectHandler { _, _ -> } + protected var onDisconnect = ConnectDisconnectHandler { _, _ -> } + + abstract fun connect(other: ElectricNode) + abstract fun isConnectedTo(other: ElectricNode): Boolean + abstract fun disconnectFrom(other: ElectricNode) + + /** + * Registers a handler to be called when a connection is made between this node and another node. + * + * Passing a new handler will not replace the old one, but will cause both the old and new handlers to be called when a connection is made. + */ + fun onConnect(handler: ConnectDisconnectHandler) { + val oldOnConnect = onConnect + onConnect = { n1, n2 -> + oldOnConnect.handle(n1, n2) + handler.handle(n1, n2) + } + } + + /** + * Registers a handler to be called when a connection is broken between this node and another node. + * + * Passing a new handler will not replace the old one, but will cause both the old and new handlers to be called when a connection is broken. + */ + fun onDisconnect(handler: ConnectDisconnectHandler) { + val oldOnDisconnect = onDisconnect + onDisconnect = { n1, n2 -> + oldOnDisconnect.handle(n1, n2) + handler.handle(n1, n2) + } + } + + @FunctionalInterface + fun interface ConnectDisconnectHandler { + /** + * Called when a connection is made or broken between two nodes, depending on what it was registered. + * + * @param node the node that this handler is registered on + * @param other the other node that is being connected to or disconnected from + */ + fun handle(node: ElectricNode, other: ElectricNode) + } + + override fun equals(other: Any?) = other is ElectricNode && id == other.id + + override fun hashCode() = id.hashCode() + + override fun toString(): String { + return "Electric node \"$name\" of type ${this::class.simpleName} at $block with ID $id" + } + + class Connector private constructor( + id: UUID, + name: String, + block: BlockPosition, + @get:JvmSynthetic internal val connectionSet: MutableSet, + ) : ElectricNode(id, name, block) { + + constructor( + name: String, + block: BlockPosition + ) : this(UUID.randomUUID(), name, block, mutableSetOf()) + + val connections get() = connectionSet.toSet() + + override fun connect(other: ElectricNode) { + when (other) { + is Leaf -> other.connect(this) + is Connector -> { + connectionSet.add(other.id) + other.connectionSet.add(this.id) + + onConnect.handle(this, other) + other.onConnect.handle(other, this) + + ElectricityManager.mergeNetworks() + } + } + } + + override fun isConnectedTo(other: ElectricNode) = other.id in connectionSet + + override fun disconnectFrom(other: ElectricNode) { + when (other) { + is Leaf -> { + other.disconnect() + } + + is Connector -> { + connectionSet.remove(other.id) + other.connectionSet.remove(this.id) + + onDisconnect.handle(this, other) + other.onDisconnect.handle(other, this) + } + } + ElectricityManager.refreshNetworks(network, other.network) + } + + companion object { + + private val CONNECTIONS_KEY = rebarKey("connections") + private val CONNECTIONS_TYPE = RebarSerializers.SET.setTypeFrom(RebarSerializers.UUID) + + @get:JvmSynthetic + internal val PDC_TYPE = object : PersistentDataType { + override fun getPrimitiveType() = PersistentDataContainer::class.java + override fun getComplexType() = Connector::class.java + + override fun toPrimitive( + complex: Connector, + context: PersistentDataAdapterContext + ): PersistentDataContainer { + val pdc = context.newPersistentDataContainer() + pdc.set(ID_KEY, RebarSerializers.UUID, complex.id) + pdc.set(NAME_KEY, RebarSerializers.STRING, complex.name) + pdc.set(BLOCK_KEY, RebarSerializers.BLOCK_POSITION, complex.block) + pdc.set(CONNECTIONS_KEY, CONNECTIONS_TYPE, complex.connectionSet) + return pdc + } + + override fun fromPrimitive( + primitive: PersistentDataContainer, + context: PersistentDataAdapterContext + ): Connector { + val id = primitive.get(ID_KEY, RebarSerializers.UUID)!! + val name = primitive.get(NAME_KEY, RebarSerializers.STRING)!! + val block = primitive.get(BLOCK_KEY, RebarSerializers.BLOCK_POSITION)!! + val connectionSet = primitive.get(CONNECTIONS_KEY, CONNECTIONS_TYPE)!!.toMutableSet() + return Connector(id, name, block, connectionSet) + } + } + } + } + + /** + * Common parent class of non-connector nodes + */ + @ApiStatus.Internal + sealed class Leaf( + id: UUID, + name: String, + block: BlockPosition, + connection: UUID? + ) : ElectricNode(id, name, block) { + + var connection = connection + private set + + override fun connect(other: ElectricNode) { + if (connection != null) throw IllegalStateException("${this::class.simpleName} node is already connected") + connection = other.id + + when (other) { + is Leaf -> other.connection = this.id + is Connector -> other.connectionSet.add(this.id) + } + + onConnect.handle(this, other) + other.onConnect.handle(other, this) + + ElectricityManager.mergeNetworks() + } + + override fun isConnectedTo(other: ElectricNode) = connection == other.id + + override fun disconnectFrom(other: ElectricNode) { + if (other.id != connection) throw IllegalArgumentException("Not connected to $other") + disconnect() + } + + fun disconnect() { + val otherId = connection ?: return + connection = null + val other = ElectricityManager.getNodeById(otherId) ?: throw IllegalStateException("Connected node with ID $otherId not found") + when (other) { + is Leaf -> other.connection = null + is Connector -> other.connectionSet.remove(this.id) + } + + onDisconnect.handle(this, other) + other.onDisconnect.handle(other, this) + } + + companion object { + @get:JvmSynthetic + internal val CONNECTION_KEY = rebarKey("connection") + } + } + + class Producer private constructor( + id: UUID, + name: String, + block: BlockPosition, + connection: UUID?, + var power: Double + ) : Leaf(id, name, block, connection) { + constructor( + name: String, + block: BlockPosition, + power: Double + ) : this(UUID.randomUUID(), name, block, null, power) + + @get:JvmSynthetic + @set:JvmSynthetic + internal var powerTakeHandler = ConsumerFn { } + + fun onPowerTake(handler: ConsumerFn) { + powerTakeHandler = handler + } + + companion object { + + private val POWER_KEY = rebarKey("power") + + @get:JvmSynthetic + internal val PDC_TYPE = object : PersistentDataType { + override fun getPrimitiveType() = PersistentDataContainer::class.java + override fun getComplexType() = Producer::class.java + + override fun toPrimitive( + complex: Producer, + context: PersistentDataAdapterContext + ): PersistentDataContainer { + val pdc = context.newPersistentDataContainer() + pdc.set(ID_KEY, RebarSerializers.UUID, complex.id) + pdc.set(NAME_KEY, RebarSerializers.STRING, complex.name) + pdc.set(BLOCK_KEY, RebarSerializers.BLOCK_POSITION, complex.block) + pdc.setNullable(CONNECTION_KEY, RebarSerializers.UUID, complex.connection) + pdc.set(POWER_KEY, RebarSerializers.DOUBLE, complex.power) + return pdc + } + + override fun fromPrimitive( + primitive: PersistentDataContainer, + context: PersistentDataAdapterContext + ): Producer { + val id = primitive.get(ID_KEY, RebarSerializers.UUID)!! + val name = primitive.get(NAME_KEY, RebarSerializers.STRING)!! + val block = primitive.get(BLOCK_KEY, RebarSerializers.BLOCK_POSITION)!! + val connection = primitive.get(CONNECTION_KEY, RebarSerializers.UUID) + val power = primitive.get(POWER_KEY, RebarSerializers.DOUBLE)!! + return Producer(id, name, block, connection, power) + } + } + } + } + + class Consumer private constructor( + id: UUID, + name: String, + block: BlockPosition, + connection: UUID?, + var requiredPower: Double + ) : Leaf(id, name, block, connection) { + + constructor( + name: String, + block: BlockPosition, + requiredPower: Double + ) : this(UUID.randomUUID(), name, block, null, requiredPower) + + var isPowered: Boolean = false + @JvmSynthetic + internal set + + companion object { + + private val REQUIRED_POWER_KEY = rebarKey("required_power") + + @get:JvmSynthetic + internal val PDC_TYPE = object : PersistentDataType { + override fun getPrimitiveType() = PersistentDataContainer::class.java + override fun getComplexType() = Consumer::class.java + + override fun toPrimitive( + complex: Consumer, + context: PersistentDataAdapterContext + ): PersistentDataContainer { + val pdc = context.newPersistentDataContainer() + pdc.set(ID_KEY, RebarSerializers.UUID, complex.id) + pdc.set(NAME_KEY, RebarSerializers.STRING, complex.name) + pdc.set(BLOCK_KEY, RebarSerializers.BLOCK_POSITION, complex.block) + pdc.setNullable(CONNECTION_KEY, RebarSerializers.UUID, complex.connection) + pdc.set(REQUIRED_POWER_KEY, RebarSerializers.DOUBLE, complex.requiredPower) + return pdc + } + + override fun fromPrimitive( + primitive: PersistentDataContainer, + context: PersistentDataAdapterContext + ): Consumer { + val id = primitive.get(ID_KEY, RebarSerializers.UUID)!! + val name = primitive.get(NAME_KEY, RebarSerializers.STRING)!! + val block = primitive.get(BLOCK_KEY, RebarSerializers.BLOCK_POSITION)!! + val connection = primitive.get(CONNECTION_KEY, RebarSerializers.UUID) + val requiredPower = primitive.get(REQUIRED_POWER_KEY, RebarSerializers.DOUBLE)!! + return Consumer(id, name, block, connection, requiredPower) + } + } + } + } + + class Acceptor private constructor( + id: UUID, + name: String, + block: BlockPosition, + connection: UUID? + ) : Leaf(id, name, block, connection) { + + constructor( + name: String, + block: BlockPosition + ) : this(UUID.randomUUID(), name, block, null) + + @get:JvmSynthetic + @set:JvmSynthetic + internal var handler = AcceptorHandler { 0.0 } + + fun onAccept(handler: AcceptorHandler) { + this.handler = handler + } + + @FunctionalInterface + fun interface AcceptorHandler { + + /** + * Called when the acceptor is accepting power. The returned value is the amount of power that was accepted, + * which may be less than the required power if the acceptor cannot accept all of it. + */ + fun onAccept(power: Double): Double + } + + companion object { + + @get:JvmSynthetic + internal val PDC_TYPE = object : PersistentDataType { + override fun getPrimitiveType() = PersistentDataContainer::class.java + override fun getComplexType() = Acceptor::class.java + + override fun toPrimitive( + complex: Acceptor, + context: PersistentDataAdapterContext + ): PersistentDataContainer { + val pdc = context.newPersistentDataContainer() + pdc.set(ID_KEY, RebarSerializers.UUID, complex.id) + pdc.set(NAME_KEY, RebarSerializers.STRING, complex.name) + pdc.set(BLOCK_KEY, RebarSerializers.BLOCK_POSITION, complex.block) + pdc.setNullable(CONNECTION_KEY, RebarSerializers.UUID, complex.connection) + return pdc + } + + override fun fromPrimitive( + primitive: PersistentDataContainer, + context: PersistentDataAdapterContext + ): Acceptor { + val id = primitive.get(ID_KEY, RebarSerializers.UUID)!! + val name = primitive.get(NAME_KEY, RebarSerializers.STRING)!! + val block = primitive.get(BLOCK_KEY, RebarSerializers.BLOCK_POSITION)!! + val connection = primitive.get(CONNECTION_KEY, RebarSerializers.UUID) + return Acceptor(id, name, block, connection) + } + } + } + } + + companion object { + + @get:JvmSynthetic + internal val ID_KEY = rebarKey("id") + + @get:JvmSynthetic + internal val NAME_KEY = rebarKey("name") + + @get:JvmSynthetic + internal val BLOCK_KEY = rebarKey("block") + + @get:JvmSynthetic + internal val PDC_TYPE = RebarSerializers.POLYMORPHIC.of(Producer.PDC_TYPE, Consumer.PDC_TYPE, Connector.PDC_TYPE, Acceptor.PDC_TYPE) + } +} \ No newline at end of file diff --git a/rebar/src/main/kotlin/io/github/pylonmc/rebar/electricity/ElectricityManager.kt b/rebar/src/main/kotlin/io/github/pylonmc/rebar/electricity/ElectricityManager.kt new file mode 100644 index 000000000..376cc967f --- /dev/null +++ b/rebar/src/main/kotlin/io/github/pylonmc/rebar/electricity/ElectricityManager.kt @@ -0,0 +1,101 @@ +package io.github.pylonmc.rebar.electricity + +import io.github.pylonmc.rebar.Rebar +import io.github.pylonmc.rebar.config.RebarConfig +import io.github.pylonmc.rebar.util.delayTicks +import kotlinx.coroutines.launch +import java.util.UUID +import kotlin.collections.ArrayDeque + +object ElectricityManager { + + private val networks = mutableSetOf() + + private val nodes = mutableMapOf() + + private val transformers = mutableMapOf, Double>() + private val maxCurrents = mutableMapOf, Double>() + + init { + Rebar.scope.launch { + while (true) { + for (network in networks) { + network.tick() + } + delayTicks(RebarConfig.ELECTRICITY_TICK_INTERVAL.toLong()) + } + } + } + + @JvmStatic + fun addNode(node: ElectricNode) { + nodes[node.id] = node + networks.add(ElectricNetwork().also { it.addNode(node) }) + mergeNetworks() + } + + @JvmStatic + fun removeNode(node: ElectricNode) { + nodes.remove(node.id) + val network = node.network + network.removeNode(node) + if (node is ElectricNode.Connector) { + refreshNetworks(network) + } + } + + @JvmStatic + fun getNodeById(id: UUID): ElectricNode? = nodes[id] + + @JvmStatic + fun setMaxPower(from: ElectricNode, to: ElectricNode, max: Double) { + maxCurrents[from to to] = max + from.onDisconnect { node, other -> + if (other == to) { + maxCurrents.remove(node to other) + } + } + } + + @JvmStatic + fun getMaxPower(from: ElectricNode, to: ElectricNode): Double = + maxCurrents[from to to] ?: maxCurrents[to to from] ?: Double.MAX_VALUE + + @JvmSynthetic + internal fun refreshNetworks(vararg networks: ElectricNetwork) { + for (network in networks.toSet()) { + this.networks.remove(network) + for (node in network.nodes) { + this.networks.add(ElectricNetwork().also { it.addNode(node) }) + } + } + mergeNetworks() + } + + @JvmSynthetic + internal fun mergeNetworks() { + val candidates = ArrayDeque(networks) + networks.clear() + while (candidates.isNotEmpty()) { + var network = candidates.removeFirst() + do { + var merged = false + for (i in candidates.indices) { + val candidate = candidates[i] + val mergedNetwork = ElectricNetwork.tryMerge(network, candidate) + if (mergedNetwork != null) { + network = mergedNetwork + candidates.removeAt(i) + merged = true + break + } + } + } while (merged) + networks.add(network) + } + } + + @JvmSynthetic + internal fun getNodeNetwork(node: ElectricNode): ElectricNetwork = + networks.find { it.isPartOfNetwork(node) } ?: error("Node ${node.id} is not part of any network") +} \ No newline at end of file diff --git a/rebar/src/main/kotlin/io/github/pylonmc/rebar/electricity/VoltageRange.kt b/rebar/src/main/kotlin/io/github/pylonmc/rebar/electricity/VoltageRange.kt new file mode 100644 index 000000000..e6bc5527d --- /dev/null +++ b/rebar/src/main/kotlin/io/github/pylonmc/rebar/electricity/VoltageRange.kt @@ -0,0 +1,11 @@ +package io.github.pylonmc.rebar.electricity + +@JvmRecord +data class VoltageRange(val min: Double, val max: Double) { + + init { + require(min <= max) { "min must be less than or equal to max" } + } + + operator fun contains(voltage: Double) = voltage in min..max +} diff --git a/rebar/src/main/kotlin/io/github/pylonmc/rebar/electricity/WireConnectionService.kt b/rebar/src/main/kotlin/io/github/pylonmc/rebar/electricity/WireConnectionService.kt new file mode 100644 index 000000000..a95de790a --- /dev/null +++ b/rebar/src/main/kotlin/io/github/pylonmc/rebar/electricity/WireConnectionService.kt @@ -0,0 +1,320 @@ +package io.github.pylonmc.rebar.electricity + +import io.github.pylonmc.rebar.block.BlockStorage +import io.github.pylonmc.rebar.block.base.RebarElectricBlock +import io.github.pylonmc.rebar.datatypes.RebarSerializers +import io.github.pylonmc.rebar.entity.display.ItemDisplayBuilder +import io.github.pylonmc.rebar.entity.display.transform.TransformBuilder +import io.github.pylonmc.rebar.event.RebarBlockBreakEvent +import io.github.pylonmc.rebar.i18n.RebarArgument +import io.github.pylonmc.rebar.item.RebarItem +import io.github.pylonmc.rebar.item.base.RebarWire +import io.github.pylonmc.rebar.item.builder.ItemStackBuilder +import io.github.pylonmc.rebar.registry.RebarRegistry +import io.github.pylonmc.rebar.util.rebarKey +import net.kyori.adventure.text.Component +import net.kyori.adventure.text.format.NamedTextColor +import org.bukkit.Bukkit +import org.bukkit.FluidCollisionMode +import org.bukkit.GameMode +import org.bukkit.Location +import org.bukkit.entity.Interaction +import org.bukkit.entity.ItemDisplay +import org.bukkit.entity.Player +import org.bukkit.event.EventHandler +import org.bukkit.event.EventPriority +import org.bukkit.event.Listener +import org.bukkit.event.entity.PlayerDeathEvent +import org.bukkit.event.player.* +import org.bukkit.inventory.ItemStack +import org.joml.Matrix4f +import java.util.* +import kotlin.math.ceil +import kotlin.math.min + +internal object WireConnectionService : Listener { + + private val interactions = WeakHashMap() + private val locations = WeakHashMap() + private val CONNECTING_KEY = rebarKey("connecting") + private val CONNECTING_NODE_KEY = rebarKey("connecting_node") + private val WIRE_TYPE_KEY = rebarKey("wire_type") + + fun addInteraction(interaction: Interaction, node: ElectricNode) { + interactions[interaction] = node + locations[node] = interaction.location.add(0.0, interaction.height / 2, 0.0) + node.onDisconnect { thisNode, otherNode -> + val block = BlockStorage.getAs(thisNode.block) ?: return@onDisconnect + val connectionName = getConnectionName(thisNode, otherNode) + block.tryRemoveEntity(connectionName) + } + } + + @EventHandler(ignoreCancelled = true) + private fun onInteract(event: PlayerInteractEntityEvent) { + val thisNode = interactions[event.rightClicked] ?: return + val thisLocation = locations[thisNode] ?: return + val thisBlock = BlockStorage.getAsOrThrow(thisNode.block) + val player = event.player + val playerPdc = player.persistentDataContainer + val playerInv = player.inventory + + val connectingEntity = player.connectingEntity + if (connectingEntity == null) { + if (RebarItem.fromStack(playerInv.getItem(event.hand)) !is RebarWire) { + player.sendMessage(Component.translatable("rebar.message.electricity.need_wire")) + return + } + + val existingConnection = when (thisNode) { + is ElectricNode.Leaf -> thisNode.connection + is ElectricNode.Connector -> thisNode.connections.firstOrNull { + val node = ElectricityManager.getNodeById(it) ?: return@firstOrNull false + thisBlock.isHeldEntityPresent(getConnectionName(thisNode, node)) + } + }?.let(ElectricityManager::getNodeById) + + val playerLocation = player.eyeLocation.subtract(0.0, 0.5, 0.0) + val (wireItem, node) = if (existingConnection != null) { + val wireEntity = + thisBlock.getHeldEntityOrThrow(getConnectionName(thisNode, existingConnection)) as ItemDisplay + val wireType = RebarRegistry.ITEMS.getOrThrow(wireEntity.persistentDataContainer.get(WIRE_TYPE_KEY, RebarSerializers.NAMESPACED_KEY)!!) + val wireItem = wireType.createNewItemStack() + + if (player.gameMode != GameMode.CREATIVE) { + // Give the player the wire items back, dropping any excess on the ground + val mainHandItem = playerInv.itemInMainHand + + var amount = ceil(locations[existingConnection]!!.distance(thisLocation)).toInt() + val takenAmount = min(amount, wireItem.maxStackSize) + playerInv.setItemInMainHand(wireItem.asQuantity(takenAmount)) + amount -= takenAmount + + val noFit = mutableListOf() + while (amount > 0) { + val takenAmount = min(amount, wireItem.maxStackSize) + val excess = playerInv.addItem(wireItem.asQuantity(takenAmount)) + noFit.addAll(excess.values) + amount -= takenAmount + } + noFit.addAll(playerInv.addItem(mainHandItem).values) + + for (item in noFit) { + player.world.dropItem(player.location, item) + } + } + + thisNode.disconnectFrom(existingConnection) + wireItem to existingConnection + } else { + playerInv.getItem(event.hand) to thisNode + } + + val wire = RebarItem.from(wireItem) ?: return + val otherEnd = locations[node] ?: return + val display = ItemDisplayBuilder() + .transformation(getDisplayTransform(otherEnd, playerLocation)) + .material(wire.displayMaterial) + .build(getMidpoint(otherEnd, playerLocation)) + display.persistentDataContainer.set(CONNECTING_NODE_KEY, RebarSerializers.UUID, node.id) + playerPdc.set(CONNECTING_KEY, RebarSerializers.UUID, display.uniqueId) + checkCanRunWire(player, node, playerLocation) + event.isCancelled = true + return + } + + event.isCancelled = true + + val connectingNodeId = + connectingEntity.persistentDataContainer.get(CONNECTING_NODE_KEY, RebarSerializers.UUID) ?: return + val connectingNode = ElectricityManager.getNodeById(connectingNodeId) ?: return + if (connectingNode == thisNode) { + deleteConnecting(player) + return + } + + if (connectingNode.isConnectedTo(thisNode)) { + deleteConnecting(player) + connectingNode.disconnectFrom(thisNode) + return + } + + if (!checkCanRunWire(player, connectingNode, thisLocation)) return + player.sendActionBar(Component.empty()) + + if (thisNode is ElectricNode.Leaf && thisNode.connection != null) { + player.sendMessage(Component.translatable("rebar.message.electricity.already_connected")) + return + } + + val wireItem = playerInv.getItem(event.hand) + val wire = RebarItem.from(wireItem) ?: return + connectingNode.connect(thisNode) + ElectricityManager.setMaxPower(thisNode, connectingNode, wire.maxCurrent) + val connectingLocation = locations[connectingNode] ?: return + connectingEntity.setTransformationMatrix(getDisplayTransform(connectingLocation, thisLocation)) + connectingEntity.teleportAsync(getMidpoint(connectingLocation, thisLocation)) + + val connectingBlock = BlockStorage.getAsOrThrow(connectingNode.block) + thisBlock.addEntity(getConnectionName(thisNode, connectingNode), connectingEntity) + connectingBlock.addEntity(getConnectionName(connectingNode, thisNode), connectingEntity) + + connectingEntity.persistentDataContainer.set(WIRE_TYPE_KEY, RebarSerializers.NAMESPACED_KEY, (wire as RebarItem).key) + player.persistentDataContainer.remove(CONNECTING_KEY) + + if (player.gameMode != GameMode.CREATIVE) { + var remaining = ceil(connectingLocation.distance(thisLocation)).toInt() + while (remaining > 0) { + for (i in playerInv.contents.indices) { + val item = playerInv.getItem(i) ?: continue + if (!item.isSimilar(wireItem)) continue + val toRemove = minOf(item.amount, remaining) + item.amount -= toRemove + remaining -= toRemove + if (remaining <= 0) break + } + } + } + } + + private fun getConnectionName(from: ElectricNode, to: ElectricNode): String { + return "connection_${from.id}->${to.id}" + } + + private fun checkCanRunWire( + player: Player, + connectingFromNode: ElectricNode, + connectionLocation: Location + ): Boolean { + val connectingFromLocation = locations[connectingFromNode] ?: return false + val direction = connectionLocation.toVector().subtract(connectingFromLocation.toVector()).normalize() + val startLocation = connectingFromLocation.clone().add(direction.multiply(0.1)) + val endLocation = connectionLocation.clone().subtract(direction.multiply(0.1)) + val rayTraceResult = startLocation.world.rayTraceBlocks( + startLocation, + direction, + startLocation.distance(endLocation), + FluidCollisionMode.ALWAYS, + true + ) + if (rayTraceResult != null) { + player.sendActionBar(Component.translatable("rebar.message.electricity.blocking")) + return false + } + val wireItem = player.inventory.itemInMainHand + check(RebarItem.fromStack(wireItem) is RebarWire) { "Held item must be a wire" } + val totalWires = player.inventory.sumOf { if (it != null && it.isSimilar(wireItem)) it.amount else 0 } + val neededWires = ceil(connectingFromLocation.distance(connectionLocation)).toInt() + val hasEnough = neededWires <= totalWires || player.gameMode == GameMode.CREATIVE + player.sendActionBar( + Component.text() + .color(if (hasEnough) NamedTextColor.WHITE else NamedTextColor.RED) + .append( + Component.translatable( + "rebar.message.electricity.default", + RebarArgument.of("wires", neededWires), + RebarArgument.of("total", totalWires) + ) + ) + ) + return hasEnough + } + + @EventHandler(priority = EventPriority.LOW) + private fun onElectricBlockBreak(event: RebarBlockBreakEvent) { + val block = event.block as? RebarElectricBlock ?: return + for (player in Bukkit.getOnlinePlayers()) { + val connectingEntity = player.connectingEntity ?: continue + val nodeId = + connectingEntity.persistentDataContainer.get(CONNECTING_NODE_KEY, RebarSerializers.UUID) ?: continue + val node = ElectricityManager.getNodeById(nodeId) ?: continue + if (node in block.electricNodes) { + deleteConnecting(player) + } + } + } + + @EventHandler + private fun onPlayerMove(event: PlayerMoveEvent) { + if (!event.hasChangedPosition()) return + val player = event.player + val connectingEntity = player.connectingEntity ?: return + val connectingNode = connectingEntity.persistentDataContainer.get(CONNECTING_NODE_KEY, RebarSerializers.UUID) + ?.let(ElectricityManager::getNodeById) ?: return + val connectingLocation = locations[connectingNode] ?: return + val playerLocation = player.eyeLocation.subtract(0.0, 0.5, 0.0) + connectingEntity.teleportDuration = 1 + connectingEntity.interpolationDelay = 0 + connectingEntity.interpolationDuration = 1 + connectingEntity.setTransformationMatrix(getDisplayTransform(connectingLocation, playerLocation)) + connectingEntity.teleportAsync(getMidpoint(connectingLocation, playerLocation)) + checkCanRunWire(player, connectingNode, playerLocation) + } + + @EventHandler + private fun onPlayerDeath(event: PlayerDeathEvent) { + val player = event.entity + if (!player.persistentDataContainer.has(CONNECTING_KEY)) return + deleteConnecting(player) + } + + @EventHandler + private fun onPlayerQuit(event: PlayerQuitEvent) { + val player = event.player + if (!player.persistentDataContainer.has(CONNECTING_KEY)) return + deleteConnecting(player) + } + + @EventHandler + private fun onPlayerScroll(event: PlayerItemHeldEvent) { + val player = event.player + if (!player.persistentDataContainer.has(CONNECTING_KEY)) return + val item = player.inventory.getItem(event.newSlot)?.let(RebarItem::fromStack) + if (item !is RebarWire) { + deleteConnecting(player) + } else { + updateWireMaterial(player, item) + } + } + + @EventHandler + private fun onPlayerSwap(event: PlayerSwapHandItemsEvent) { + val player = event.player + if (!player.persistentDataContainer.has(CONNECTING_KEY)) return + val item = RebarItem.fromStack(event.mainHandItem) + if (item !is RebarWire) { + deleteConnecting(player) + } else { + updateWireMaterial(player, item) + } + } + + private fun deleteConnecting(player: Player) { + player.connectingEntity?.remove() + player.persistentDataContainer.remove(CONNECTING_KEY) + player.sendActionBar(Component.empty()) + } + + private fun updateWireMaterial(player: Player, item: RebarWire) { + val connectingEntity = player.connectingEntity ?: return + val material = item.displayMaterial + connectingEntity.setItemStack(ItemStackBuilder.of(material).addCustomModelDataString("wire").build()) + } + + private val Player.connectingEntity: ItemDisplay? + get() { + val connectingEntityId = persistentDataContainer.get(CONNECTING_KEY, RebarSerializers.UUID) ?: return null + return Bukkit.getEntity(connectingEntityId) as? ItemDisplay + } + + private fun getDisplayTransform(from: Location, to: Location): Matrix4f { + return TransformBuilder() + .lookAlong(from, to) + .scale(0.05, 0.05, from.distance(to)) + .buildForItemDisplay() + } + + private fun getMidpoint(a: Location, b: Location): Location { + return a.clone().add(b).multiply(0.5) + } +} \ No newline at end of file diff --git a/rebar/src/main/kotlin/io/github/pylonmc/rebar/entity/display/transform/Rotation.kt b/rebar/src/main/kotlin/io/github/pylonmc/rebar/entity/display/transform/Rotation.kt index 073784e37..af668fa4e 100644 --- a/rebar/src/main/kotlin/io/github/pylonmc/rebar/entity/display/transform/Rotation.kt +++ b/rebar/src/main/kotlin/io/github/pylonmc/rebar/entity/display/transform/Rotation.kt @@ -2,30 +2,25 @@ package io.github.pylonmc.rebar.entity.display.transform import org.joml.* -open class Rotation private constructor( - protected val vector: Vector3f?, - protected val quaternion: Quaternionf? -) : TransformComponent { +open class Rotation(protected val quaternion: Quaternionf) : TransformComponent { - constructor(rotation: Vector3f): this(rotation, null) + constructor(rotation: Vector3f) : this(Quaternionf().rotateXYZ(rotation.x, rotation.y, rotation.z)) + constructor(rotation: Vector3d) : this(Vector3f(rotation)) - constructor(rotation: Vector3d): this(Vector3f(rotation), null) + constructor(x: Float, y: Float, z: Float) : this(Vector3f(x, y, z)) + constructor(x: Double, y: Double, z: Double) : this(Vector3d(x, y, z)) - constructor(x: Float, y: Float, z: Float): this(Vector3f(x, y, z)) + constructor(axisAngle: AxisAngle4f) : this(Quaternionf().rotationAxis(axisAngle)) + constructor(axis: Vector3f, angle: Float) : this(Quaternionf().rotationAxis(angle, axis)) + constructor(x: Float, y: Float, z: Float, angle: Float) : this(Quaternionf().rotationAxis(angle, x, y, z)) - constructor(x: Double, y: Double, z: Double): this(Vector3d(x, y, z)) + constructor(axisAngle: AxisAngle4d) : this(axisAngle.x, axisAngle.y, axisAngle.z, axisAngle.angle) + constructor(axis: Vector3d, angle: Double) : this(axis.x, axis.y, axis.z, angle) + constructor(x: Double, y: Double, z: Double, angle: Double) : this(x.toFloat(), y.toFloat(), z.toFloat(), angle.toFloat()) - constructor(rotation: Quaterniond): this(null, Quaternionf(rotation)) - - constructor(rotation: Quaternionf): this(null, rotation) + constructor(rotation: Quaterniond) : this(Quaternionf(rotation)) override fun apply(matrix: Matrix4f) { - if (vector != null) { - matrix.mul(Matrix4f().rotateXYZ(vector)) - } else if (quaternion != null) { - matrix.mul(Matrix4f().rotate(quaternion)) - } else { - throw IllegalStateException() - } + matrix.mul(Matrix4f().rotate(quaternion)) } } \ No newline at end of file diff --git a/rebar/src/main/kotlin/io/github/pylonmc/rebar/entity/display/transform/RotationBackwards.kt b/rebar/src/main/kotlin/io/github/pylonmc/rebar/entity/display/transform/RotationBackwards.kt index bc33d17e2..79aaa3f2a 100644 --- a/rebar/src/main/kotlin/io/github/pylonmc/rebar/entity/display/transform/RotationBackwards.kt +++ b/rebar/src/main/kotlin/io/github/pylonmc/rebar/entity/display/transform/RotationBackwards.kt @@ -2,30 +2,20 @@ package io.github.pylonmc.rebar.entity.display.transform import org.joml.* -open class RotationBackwards private constructor( - protected val vector: Vector3f?, - protected val quaternion: Quaternionf? -) : TransformComponent { +open class RotationBackwards(quaternion: Quaternionf) : Rotation(quaternion.invert()) { + constructor(rotation: Vector3f) : this(Quaternionf().rotateXYZ(rotation.x, rotation.y, rotation.z)) + constructor(rotation: Vector3d) : this(Vector3f(rotation)) - constructor(rotation: Vector3f): this(rotation, null) + constructor(x: Float, y: Float, z: Float) : this(Vector3f(x, y, z)) + constructor(x: Double, y: Double, z: Double) : this(Vector3d(x, y, z)) - constructor(rotation: Vector3d): this(Vector3f(rotation), null) + constructor(axisAngle: AxisAngle4f) : this(Quaternionf().rotationAxis(axisAngle)) + constructor(axis: Vector3f, angle: Float) : this(Quaternionf().rotationAxis(angle, axis)) + constructor(x: Float, y: Float, z: Float, angle: Float) : this(Quaternionf().rotationAxis(angle, x, y, z)) - constructor(x: Float, y: Float, z: Float): this(Vector3f(x, y, z)) + constructor(axisAngle: AxisAngle4d) : this(axisAngle.x, axisAngle.y, axisAngle.z, axisAngle.angle) + constructor(axis: Vector3d, angle: Double) : this(axis.x, axis.y, axis.z, angle) + constructor(x: Double, y: Double, z: Double, angle: Double) : this(x.toFloat(), y.toFloat(), z.toFloat(), angle.toFloat()) - constructor(x: Double, y: Double, z: Double): this(Vector3d(x, y, z)) - - constructor(rotation: Quaterniond): this(null, Quaternionf(rotation)) - - constructor(rotation: Quaternionf): this(null, rotation) - - override fun apply(matrix: Matrix4f) { - if (vector != null) { - matrix.mul(Matrix4f().rotateXYZ(Vector3f(vector).mul(-1.0F))) - } else if (quaternion != null) { - matrix.mul(Matrix4f().rotate(Quaternionf(quaternion).invert())) - } else { - throw IllegalStateException() - } - } + constructor(rotation: Quaterniond) : this(Quaternionf(rotation)) } \ No newline at end of file diff --git a/rebar/src/main/kotlin/io/github/pylonmc/rebar/entity/display/transform/TransformBuilder.kt b/rebar/src/main/kotlin/io/github/pylonmc/rebar/entity/display/transform/TransformBuilder.kt index 5cbdc5f7b..21ba6cd6f 100644 --- a/rebar/src/main/kotlin/io/github/pylonmc/rebar/entity/display/transform/TransformBuilder.kt +++ b/rebar/src/main/kotlin/io/github/pylonmc/rebar/entity/display/transform/TransformBuilder.kt @@ -7,14 +7,15 @@ import org.joml.* @Suppress("unused") open class TransformBuilder(val components: ArrayDeque) { - constructor(): this(ArrayDeque()) + constructor() : this(ArrayDeque()) - constructor(other: TransformBuilder): this(ArrayDeque(other.components)) + constructor(other: TransformBuilder) : this(ArrayDeque(other.components)) fun add(component: TransformComponent) = apply { components.addLast(component) } + // @formatter:off fun translate(translation: Vector3f) = apply { add(Translation(translation)) } fun translate(translation: Vector3d) = apply { add(Translation(translation)) } fun translate(x: Float, y: Float, z: Float) = apply { add(Translation(x, y, z)) } @@ -33,6 +34,12 @@ open class TransformBuilder(val components: ArrayDeque) { fun rotate(rotation: Quaterniond) = apply { add(Rotation(rotation)) } fun rotate(x: Float, y: Float, z: Float) = apply { add(Rotation(x, y, z)) } fun rotate(x: Double, y: Double, z: Double) = apply { add(Rotation(x, y, z)) } + fun rotate(axisAngle: AxisAngle4f) = apply { add(Rotation(axisAngle)) } + fun rotate(axis: Vector3f, angle: Float) = apply { add(Rotation(axis, angle)) } + fun rotate(x: Float, y: Float, z: Float, angle: Float) = apply { add(Rotation(x, y, z, angle)) } + fun rotate(axisAngle: AxisAngle4d) = apply { add(Rotation(axisAngle)) } + fun rotate(axis: Vector3d, angle: Double) = apply { add(Rotation(axis, angle)) } + fun rotate(x: Double, y: Double, z: Double, angle: Double) = apply { add(Rotation(x, y, z, angle)) } fun rotateBackwards(rotation: Vector3f) = apply { add(RotationBackwards(rotation)) } fun rotateBackwards(rotation: Vector3d) = apply { add(RotationBackwards(rotation)) } @@ -40,17 +47,22 @@ open class TransformBuilder(val components: ArrayDeque) { fun rotateBackwards(rotation: Quaterniond) = apply { add(RotationBackwards(rotation)) } fun rotateBackwards(x: Float, y: Float, z: Float) = apply { add(RotationBackwards(x, y, z)) } fun rotateBackwards(x: Double, y: Double, z: Double) = apply { add(RotationBackwards(x, y, z)) } + fun rotateBackwards(axisAngle: AxisAngle4f) = apply { add(RotationBackwards(axisAngle)) } + fun rotateBackwards(axis: Vector3f, angle: Float) = apply { add(RotationBackwards(axis, angle)) } + fun rotateBackwards(x: Float, y: Float, z: Float, angle: Float) = apply { add(RotationBackwards(x, y, z, angle)) } + fun rotateBackwards(axisAngle: AxisAngle4d) = apply { add(RotationBackwards(axisAngle)) } + fun rotateBackwards(axis: Vector3d, angle: Double) = apply { add(RotationBackwards(axis, angle)) } + fun rotateBackwards(x: Double, y: Double, z: Double, angle: Double) = apply { add(RotationBackwards(x, y, z, angle)) } fun lookAlong(direction: Vector3f) = apply { add(LookAlong(direction)) } fun lookAlong(direction: Vector3d) = apply { add(LookAlong(direction)) } fun lookAlong(from: Vector3f, to: Vector3f) = apply { add(LookAlong(from, to)) } fun lookAlong(from: Vector3d, to: Vector3d) = apply { add(LookAlong(from, to)) } fun lookAlong(from: Location, to: Location) = apply { add(LookAlong(from, to)) } - fun lookAlong(xFrom: Float, yFrom: Float, zFrom: Float, xTo: Float, yTo: Float, zTo: Float) - = apply { add(LookAlong(xFrom, yFrom, zFrom, xTo, yTo, zTo)) } - fun lookAlong(xFrom: Double, yFrom: Double, zFrom: Double, xTo: Double, yTo: Double, zTo: Double) - = apply { add(LookAlong(xFrom, yFrom, zFrom, xTo, yTo, zTo)) } + fun lookAlong(xFrom: Float, yFrom: Float, zFrom: Float, xTo: Float, yTo: Float, zTo: Float) = apply { add(LookAlong(xFrom, yFrom, zFrom, xTo, yTo, zTo)) } + fun lookAlong(xFrom: Double, yFrom: Double, zFrom: Double, xTo: Double, yTo: Double, zTo: Double) = apply { add(LookAlong(xFrom, yFrom, zFrom, xTo, yTo, zTo)) } fun lookAlong(direction: BlockFace) = lookAlong(direction.direction.toVector3f()) + // @formatter:on open fun build(): Matrix4f { val matrix = Matrix4f() diff --git a/rebar/src/main/kotlin/io/github/pylonmc/rebar/event/RebarBlockInitializeEvent.kt b/rebar/src/main/kotlin/io/github/pylonmc/rebar/event/RebarBlockInitializeEvent.kt new file mode 100644 index 000000000..2ba47760d --- /dev/null +++ b/rebar/src/main/kotlin/io/github/pylonmc/rebar/event/RebarBlockInitializeEvent.kt @@ -0,0 +1,22 @@ +package io.github.pylonmc.rebar.event + +import io.github.pylonmc.rebar.block.RebarBlock +import org.bukkit.block.Block +import org.bukkit.event.Event +import org.bukkit.event.HandlerList + +/** + * Called after [RebarBlock.postInitialise] is called + */ +class RebarBlockInitializeEvent( + val block: Block, + val rebarBlock: RebarBlock, +) : Event() { + override fun getHandlers(): HandlerList + = handlerList + + companion object { + @JvmStatic + val handlerList: HandlerList = HandlerList() + } +} \ No newline at end of file diff --git a/rebar/src/main/kotlin/io/github/pylonmc/rebar/event/api/MultiListener.kt b/rebar/src/main/kotlin/io/github/pylonmc/rebar/event/api/MultiListener.kt index 65127f42b..e51951b55 100644 --- a/rebar/src/main/kotlin/io/github/pylonmc/rebar/event/api/MultiListener.kt +++ b/rebar/src/main/kotlin/io/github/pylonmc/rebar/event/api/MultiListener.kt @@ -1,16 +1,14 @@ package io.github.pylonmc.rebar.event.api import com.destroystokyo.paper.util.SneakyThrow -import io.github.pylonmc.rebar.Rebar import io.github.pylonmc.rebar.event.api.annotation.MultiHandler import io.github.pylonmc.rebar.event.api.annotation.UniversalHandler +import org.bukkit.Bukkit import org.bukkit.event.Event import org.bukkit.event.EventPriority import org.bukkit.event.Listener import org.bukkit.plugin.Plugin -import org.bukkit.plugin.PluginManager import java.lang.invoke.MethodHandles -import kotlin.jvm.java /** * A custom type of [Listener] that allows methods annotated with [MultiHandler] or [UniversalHandler] to be registered for multiple event priorities at once. @@ -20,8 +18,8 @@ import kotlin.jvm.java * All methods should be formatted as `fun methodName(event: Event, priority: EventPriority)` */ interface MultiListener : Listener { - fun register(plugin: Plugin, pluginManager: PluginManager) { - pluginManager.registerEvents(this, plugin) + fun register(plugin: Plugin) { + Bukkit.getPluginManager().registerEvents(this, plugin) val methods = this::class.java.declaredMethods for (method in methods) { @@ -53,7 +51,7 @@ interface MultiListener : Listener { @Suppress("UNCHECKED_CAST") val eventClass = method.parameterTypes[0] as Class priorities.forEach { priority -> - pluginManager.registerEvent( + Bukkit.getPluginManager().registerEvent( eventClass, this, priority, diff --git a/rebar/src/main/kotlin/io/github/pylonmc/rebar/fluid/FluidWithAmount.kt b/rebar/src/main/kotlin/io/github/pylonmc/rebar/fluid/FluidWithAmount.kt new file mode 100644 index 000000000..0c077fb2f --- /dev/null +++ b/rebar/src/main/kotlin/io/github/pylonmc/rebar/fluid/FluidWithAmount.kt @@ -0,0 +1,8 @@ +package io.github.pylonmc.rebar.fluid + +import io.github.pylonmc.rebar.recipe.FluidOrItem + +@JvmRecord +data class FluidWithAmount(val fluid: RebarFluid, val amount: Double) { + fun asFluidOrItem(): FluidOrItem = FluidOrItem.Fluid(fluid, amount) +} diff --git a/rebar/src/main/kotlin/io/github/pylonmc/rebar/fluid/placement/FluidPipePlacementService.kt b/rebar/src/main/kotlin/io/github/pylonmc/rebar/fluid/placement/FluidPipePlacementService.kt index 8245e135d..2e372e3db 100644 --- a/rebar/src/main/kotlin/io/github/pylonmc/rebar/fluid/placement/FluidPipePlacementService.kt +++ b/rebar/src/main/kotlin/io/github/pylonmc/rebar/fluid/placement/FluidPipePlacementService.kt @@ -19,7 +19,7 @@ import org.bukkit.event.player.PlayerQuitEvent import org.bukkit.event.player.PlayerSwapHandItemsEvent import org.bukkit.event.world.ChunkUnloadEvent import org.bukkit.inventory.EquipmentSlot -import java.util.UUID +import java.util.* internal object FluidPipePlacementService : Listener { /** diff --git a/rebar/src/main/kotlin/io/github/pylonmc/rebar/fluid/placement/FluidPipePlacementTask.kt b/rebar/src/main/kotlin/io/github/pylonmc/rebar/fluid/placement/FluidPipePlacementTask.kt index 936068a78..8c9016ac9 100644 --- a/rebar/src/main/kotlin/io/github/pylonmc/rebar/fluid/placement/FluidPipePlacementTask.kt +++ b/rebar/src/main/kotlin/io/github/pylonmc/rebar/fluid/placement/FluidPipePlacementTask.kt @@ -35,7 +35,7 @@ internal class FluidPipePlacementTask( .material(Material.WHITE_CONCRETE) .brightness(15) // transformation will be set later, make the block invisible now to prevent flash of white on place .transformation(TransformBuilder().scale(0f)) - .build(origin.position.location.toCenterLocation()) + .build(origin.position.toLocation().toCenterLocation()) var target = origin private set @@ -67,7 +67,7 @@ internal class FluidPipePlacementTask( } // Check if player has moved too far away - if (player.location.distance(origin.position.location) > RebarConfig.PIPE_PLACEMENT_CANCEL_DISTANCE) { + if (player.location.distance(origin.position.toLocation()) > RebarConfig.PIPE_PLACEMENT_CANCEL_DISTANCE) { FluidPipePlacementService.cancelConnection(player) return } @@ -112,9 +112,9 @@ internal class FluidPipePlacementTask( // Update transformation if (origin.position != target.position) { - val targetOffset = target.position.location.toVector().toVector3f() + val targetOffset = target.position.toLocation().toVector().toVector3f() .add(target.offset) - .sub(origin.position.location.toVector().toVector3f()) + .sub(origin.position.toLocation().toVector().toVector3f()) display.setTransformationMatrix( LineBuilder() .from(origin.offset) @@ -125,7 +125,7 @@ internal class FluidPipePlacementTask( } // Interpolate only if changing on the same axis - val difference = target.position.vector3i.sub(previousTargetPosition.vector3i) + val difference = target.position.toVector3i().sub(previousTargetPosition.toVector3i()) if (isCardinalDirection(difference)) { display.interpolationDelay = 0 display.interpolationDuration = RebarConfig.PIPE_PLACEMENT_TASK_INTERVAL_TICKS.toInt() @@ -200,7 +200,7 @@ internal class FluidPipePlacementTask( } val newTargetDistance = findClosestDistanceBetweenLineAndPoint( - Vector3f(origin.position.plus(newTargetOffset).vector3i), + Vector3f(origin.position.plus(newTargetOffset).toVector3i()), playerLookPosition, playerLookDirection ) @@ -250,8 +250,8 @@ internal class FluidPipePlacementTask( * 2) if the origin has a specific face, the target is in the direction of that face */ private fun isTargetInCorrectDirection(newTarget: FluidPipePlacementPoint): Boolean { - val originToTarget = newTarget.position.vector3i.sub(origin.position.vector3i) - val targetToOrigin = origin.position.vector3i.sub(newTarget.position.vector3i) + val originToTarget = newTarget.position.toVector3i().sub(origin.position.toVector3i()) + val targetToOrigin = origin.position.toVector3i().sub(newTarget.position.toVector3i()) return isCardinalDirection(originToTarget) && (origin.allowedFace == null || vectorToBlockFace(originToTarget) == origin.allowedFace!!) && (newTarget.allowedFace == null || vectorToBlockFace(targetToOrigin) == newTarget.allowedFace!!) @@ -265,7 +265,7 @@ internal class FluidPipePlacementTask( playerLookDirection: Vector3f, axis: Vector3i ): Vector3i { - val originPosition = origin.position.location.toCenterLocation().toVector().toVector3f() + val originPosition = origin.position.toLocation().toCenterLocation().toVector().toVector3f() val solution = findClosestPointBetweenSkewLines(playerLookPosition, playerLookDirection, originPosition, Vector3f(axis)) val lambda = Math.clamp( solution.roundToLong(), diff --git a/rebar/src/main/kotlin/io/github/pylonmc/rebar/gametest/GameTest.kt b/rebar/src/main/kotlin/io/github/pylonmc/rebar/gametest/GameTest.kt index 0f07cf8b1..3ec0b8841 100644 --- a/rebar/src/main/kotlin/io/github/pylonmc/rebar/gametest/GameTest.kt +++ b/rebar/src/main/kotlin/io/github/pylonmc/rebar/gametest/GameTest.kt @@ -82,17 +82,17 @@ class GameTest( /** * Returns the center location of the game test */ - fun location(): Location = center.location + fun location(): Location = center.toLocation() /** * Returns a location relative to the center of the game test */ - fun location(location: Location): Location = location.clone().add(center.location) + fun location(location: Location): Location = location.clone().add(center.toLocation()) /** * Returns a location relative to the center of the game test */ - fun location(x: Double, y: Double, z: Double): Location = center.location.clone().add(x, y, z) + fun location(x: Double, y: Double, z: Double): Location = center.toLocation().clone().add(x, y, z) companion object { @JvmSynthetic diff --git a/rebar/src/main/kotlin/io/github/pylonmc/rebar/guide/button/FluidButton.kt b/rebar/src/main/kotlin/io/github/pylonmc/rebar/guide/button/FluidButton.kt index 35bfdb4db..9aed610ee 100644 --- a/rebar/src/main/kotlin/io/github/pylonmc/rebar/guide/button/FluidButton.kt +++ b/rebar/src/main/kotlin/io/github/pylonmc/rebar/guide/button/FluidButton.kt @@ -1,5 +1,6 @@ package io.github.pylonmc.rebar.guide.button +import io.github.pylonmc.rebar.fluid.FluidWithAmount import io.github.pylonmc.rebar.fluid.RebarFluid import io.github.pylonmc.rebar.guide.pages.fluid.FluidRecipesPage import io.github.pylonmc.rebar.guide.pages.fluid.FluidUsagesPage @@ -51,6 +52,8 @@ open class FluidButton( constructor(input: RecipeInput.Fluid) : this(input.amountMillibuckets, *input.fluids.toTypedArray()) + constructor(fluidWithAmount: FluidWithAmount) : this(fluidWithAmount.amount, fluidWithAmount.fluid) + val fluids = fluids.shuffled() val currentFluid: RebarFluid get() = this.fluids[(Bukkit.getCurrentTick() / 20) % this.fluids.size] diff --git a/rebar/src/main/kotlin/io/github/pylonmc/rebar/guide/button/ItemButton.kt b/rebar/src/main/kotlin/io/github/pylonmc/rebar/guide/button/ItemButton.kt index 498363934..887a2d3d9 100644 --- a/rebar/src/main/kotlin/io/github/pylonmc/rebar/guide/button/ItemButton.kt +++ b/rebar/src/main/kotlin/io/github/pylonmc/rebar/guide/button/ItemButton.kt @@ -210,7 +210,7 @@ class ItemButton @JvmOverloads constructor( return clonedNotRebar } else { // Rebar item handling - val clonedRebar = rebarItem.schema.getItemStack() + val clonedRebar = rebarItem.schema.createNewItemStack() clonedRebar.amount = if (click.clickType.isShiftClick) { clonedRebar.maxStackSize } else { 1 } return clonedRebar } diff --git a/rebar/src/main/kotlin/io/github/pylonmc/rebar/guide/pages/SearchItemsAndFluidsPage.kt b/rebar/src/main/kotlin/io/github/pylonmc/rebar/guide/pages/SearchItemsAndFluidsPage.kt index a7af5e960..f61cb5291 100644 --- a/rebar/src/main/kotlin/io/github/pylonmc/rebar/guide/pages/SearchItemsAndFluidsPage.kt +++ b/rebar/src/main/kotlin/io/github/pylonmc/rebar/guide/pages/SearchItemsAndFluidsPage.kt @@ -21,7 +21,7 @@ class SearchItemsAndFluidsPage : SearchPage(rebarKey("search")) { it.key !in RebarGuide.hiddenItems || (it.key in RebarGuide.adminOnlyItems && player.hasPermission("rebar.guide.cheat")) }.map { item -> val name = GlobalTranslator.render(Component.translatable("${item.key.namespace}.item.${item.key.key}.name"), player.locale()) - ItemButton(item.getItemStack()) to name.plainText.lowercase(player.locale()) + ItemButton(item.createNewItemStack()) to name.plainText.lowercase(player.locale()) }.toMutableList() fun getFluidButtons(player: Player): MutableList> = RebarRegistry.FLUIDS.filter { diff --git a/rebar/src/main/kotlin/io/github/pylonmc/rebar/guide/pages/research/ResearchItemsPage.kt b/rebar/src/main/kotlin/io/github/pylonmc/rebar/guide/pages/research/ResearchItemsPage.kt index c95181809..a3f6fe71f 100644 --- a/rebar/src/main/kotlin/io/github/pylonmc/rebar/guide/pages/research/ResearchItemsPage.kt +++ b/rebar/src/main/kotlin/io/github/pylonmc/rebar/guide/pages/research/ResearchItemsPage.kt @@ -15,7 +15,7 @@ import xyz.xenondevs.invui.item.Item class ResearchItemsPage(research: Research) : SimpleStaticGuidePage( KEY, research.unlocks.map { - ItemButton(RebarRegistry.ITEMS[it]!!.getItemStack()) + ItemButton(RebarRegistry.ITEMS[it]!!.createNewItemStack()) }.toMutableList() ) { diff --git a/rebar/src/main/kotlin/io/github/pylonmc/rebar/item/ItemTypeWrapper.kt b/rebar/src/main/kotlin/io/github/pylonmc/rebar/item/ItemTypeWrapper.kt index dcb629fe8..1215b1d59 100644 --- a/rebar/src/main/kotlin/io/github/pylonmc/rebar/item/ItemTypeWrapper.kt +++ b/rebar/src/main/kotlin/io/github/pylonmc/rebar/item/ItemTypeWrapper.kt @@ -25,7 +25,7 @@ sealed interface ItemTypeWrapper : Keyed { */ @JvmRecord data class Rebar(val item: RebarItemSchema) : ItemTypeWrapper { - override fun createItemStack() = item.getItemStack() + override fun createItemStack() = item.createNewItemStack() override fun getKey() = item.key } diff --git a/rebar/src/main/kotlin/io/github/pylonmc/rebar/item/RebarItem.kt b/rebar/src/main/kotlin/io/github/pylonmc/rebar/item/RebarItem.kt index 3aa83f695..7af2693f9 100644 --- a/rebar/src/main/kotlin/io/github/pylonmc/rebar/item/RebarItem.kt +++ b/rebar/src/main/kotlin/io/github/pylonmc/rebar/item/RebarItem.kt @@ -114,7 +114,7 @@ open class RebarItem(val stack: ItemStack) : Keyed { RebarRegistry.ITEMS.register(schema) // pre-merge configs and check for constructor errors - schema.getRebarItem() + schema.createNewRebarItem() } @JvmStatic @@ -155,7 +155,7 @@ open class RebarItem(val stack: ItemStack) : Keyed { } @JvmSynthetic - inline fun from(stack: ItemStack?): T? { + inline fun from(stack: ItemStack?): T? { val rebarItem = fromStack(stack) ?: return null return rebarItem as? T } diff --git a/rebar/src/main/kotlin/io/github/pylonmc/rebar/item/RebarItemSchema.kt b/rebar/src/main/kotlin/io/github/pylonmc/rebar/item/RebarItemSchema.kt index ec81547f0..e37956823 100644 --- a/rebar/src/main/kotlin/io/github/pylonmc/rebar/item/RebarItemSchema.kt +++ b/rebar/src/main/kotlin/io/github/pylonmc/rebar/item/RebarItemSchema.kt @@ -12,7 +12,6 @@ import io.github.pylonmc.rebar.util.findConstructorMatching import io.github.pylonmc.rebar.util.getAddon import io.github.pylonmc.rebar.util.position.position import io.github.pylonmc.rebar.util.rebarKey -import io.papermc.paper.command.brigadier.argument.ArgumentTypes.blockPosition import org.bukkit.Keyed import org.bukkit.NamespacedKey import org.bukkit.inventory.ItemStack @@ -46,12 +45,12 @@ class RebarItemSchema @JvmOverloads internal constructor( /** * Return's a clone of the [template] [ItemStack] */ - fun getItemStack(): ItemStack = template.clone() + fun createNewItemStack(): ItemStack = template.clone() /** * Return's a new instance of the [RebarItem] from the [itemClass] using a copy of the [template] [ItemStack] */ - fun getRebarItem(): RebarItem = itemClass.cast(loadConstructor.invoke(getItemStack())) + fun createNewRebarItem(): RebarItem = itemClass.cast(loadConstructor.invoke(createNewItemStack())) val research: Research? get() = RebarRegistry.RESEARCHES.find { key in it.unlocks } diff --git a/rebar/src/main/kotlin/io/github/pylonmc/rebar/item/base/RebarWire.kt b/rebar/src/main/kotlin/io/github/pylonmc/rebar/item/base/RebarWire.kt new file mode 100644 index 000000000..4c189b7d7 --- /dev/null +++ b/rebar/src/main/kotlin/io/github/pylonmc/rebar/item/base/RebarWire.kt @@ -0,0 +1,8 @@ +package io.github.pylonmc.rebar.item.base + +import org.bukkit.Material + +interface RebarWire { + val maxCurrent: Double + val displayMaterial: Material +} \ No newline at end of file diff --git a/rebar/src/main/kotlin/io/github/pylonmc/rebar/item/research/Research.kt b/rebar/src/main/kotlin/io/github/pylonmc/rebar/item/research/Research.kt index 3ca774eaa..d931156f9 100644 --- a/rebar/src/main/kotlin/io/github/pylonmc/rebar/item/research/Research.kt +++ b/rebar/src/main/kotlin/io/github/pylonmc/rebar/item/research/Research.kt @@ -9,11 +9,11 @@ import io.github.pylonmc.rebar.i18n.RebarArgument import io.github.pylonmc.rebar.item.RebarItem import io.github.pylonmc.rebar.item.RebarItemSchema import io.github.pylonmc.rebar.item.research.Research.Companion.canPickUp -import io.github.pylonmc.rebar.util.ConfettiParticle import io.github.pylonmc.rebar.recipe.FluidOrItem import io.github.pylonmc.rebar.recipe.RecipeType import io.github.pylonmc.rebar.recipe.vanilla.VanillaRecipeType import io.github.pylonmc.rebar.registry.RebarRegistry +import io.github.pylonmc.rebar.util.ConfettiParticle import io.github.pylonmc.rebar.util.persistentData import io.github.pylonmc.rebar.util.rebarKey import net.kyori.adventure.sound.Sound diff --git a/rebar/src/main/kotlin/io/github/pylonmc/rebar/recipe/RecipeInput.kt b/rebar/src/main/kotlin/io/github/pylonmc/rebar/recipe/RecipeInput.kt index 3ea9128e0..69d7e7cea 100644 --- a/rebar/src/main/kotlin/io/github/pylonmc/rebar/recipe/RecipeInput.kt +++ b/rebar/src/main/kotlin/io/github/pylonmc/rebar/recipe/RecipeInput.kt @@ -1,5 +1,6 @@ package io.github.pylonmc.rebar.recipe +import io.github.pylonmc.rebar.fluid.FluidWithAmount import io.github.pylonmc.rebar.fluid.RebarFluid import io.github.pylonmc.rebar.item.ItemTypeWrapper import org.bukkit.Tag @@ -28,7 +29,7 @@ sealed interface RecipeInput { return contains(itemStack) } - operator fun contains(itemStack: ItemStack): Boolean = ItemTypeWrapper(itemStack) in items + operator fun contains(itemStack: ItemStack?): Boolean = itemStack != null && ItemTypeWrapper(itemStack) in items } @JvmRecord @@ -46,7 +47,9 @@ sealed interface RecipeInput { return contains(fluid) } - operator fun contains(fluid: RebarFluid): Boolean = fluid in fluids + fun matches(fluidWithAmount: FluidWithAmount) = matches(fluidWithAmount.fluid, fluidWithAmount.amount) + + operator fun contains(fluid: RebarFluid?): Boolean = fluid != null && fluid in fluids } companion object { diff --git a/rebar/src/main/kotlin/io/github/pylonmc/rebar/recipe/RecipeListener.kt b/rebar/src/main/kotlin/io/github/pylonmc/rebar/recipe/RecipeListener.kt index 2682faf50..fac730c6a 100644 --- a/rebar/src/main/kotlin/io/github/pylonmc/rebar/recipe/RecipeListener.kt +++ b/rebar/src/main/kotlin/io/github/pylonmc/rebar/recipe/RecipeListener.kt @@ -1,6 +1,5 @@ package io.github.pylonmc.rebar.recipe -import io.github.pylonmc.rebar.item.RebarItem import io.github.pylonmc.rebar.item.RebarItemSchema import io.github.pylonmc.rebar.item.base.* import io.github.pylonmc.rebar.item.research.Research.Companion.canCraft @@ -74,7 +73,7 @@ internal object RebarRecipeListener : Listener { return } - val resultItem = firstSchema.getItemStack() + val resultItem = firstSchema.createNewItemStack() val durability = max(firstItem.getData(DataComponentTypes.MAX_DAMAGE)!!, secondItem.getData(DataComponentTypes.MAX_DAMAGE)!!) val firstRemaining = firstItem.getData(DataComponentTypes.MAX_DAMAGE)!! - firstItem.getData(DataComponentTypes.DAMAGE)!! val secondRemaining = secondItem.getData(DataComponentTypes.MAX_DAMAGE)!! - secondItem.getData(DataComponentTypes.DAMAGE)!! diff --git a/rebar/src/main/kotlin/io/github/pylonmc/rebar/util/PlayerInput.kt b/rebar/src/main/kotlin/io/github/pylonmc/rebar/util/PlayerInput.kt new file mode 100644 index 000000000..8bfa504ee --- /dev/null +++ b/rebar/src/main/kotlin/io/github/pylonmc/rebar/util/PlayerInput.kt @@ -0,0 +1,85 @@ +package io.github.pylonmc.rebar.util + +import io.github.pylonmc.rebar.Rebar +import io.github.pylonmc.rebar.util.PlayerInput.requestInput +import io.papermc.paper.event.player.AsyncChatEvent +import kotlinx.coroutines.future.await +import kotlinx.coroutines.launch +import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer +import org.bukkit.entity.Player +import org.bukkit.event.EventHandler +import org.bukkit.event.EventPriority +import org.bukkit.event.Listener +import org.bukkit.event.player.PlayerQuitEvent +import java.util.UUID +import java.util.concurrent.CompletableFuture +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.locks.ReentrantLock +import kotlin.concurrent.withLock + +object PlayerInput : Listener { + + private val handlerLock = ReentrantLock() + private val handlers = ConcurrentHashMap>() + + @EventHandler(ignoreCancelled = true) + private fun onPlayerInput(event: AsyncChatEvent) { + val id = event.player.uniqueId + val handlers = handlerLock.withLock { handlers.remove(id) } ?: return + if (handlers.isEmpty()) return + event.isCancelled = true + val message = PlainTextComponentSerializer.plainText().serialize(event.message()) + for (handler in handlers) { + if (!event.isAsynchronous || handler.allowAsync) { + handler.future.complete(message) + } else { + Rebar.scope.launch { + handler.future.complete(message) + } + } + } + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + private fun onPlayerQuit(event: PlayerQuitEvent) { + val id = event.player.uniqueId + val handlers = handlerLock.withLock { handlers.remove(id) } ?: return + for (handler in handlers) { + handler.future.complete(null) + } + } + + /** + * Returns a future that will be completed with the player's next chat message, or null if they disconnect before sending a message. + * The message sending will be canceled to prevent it from appearing in chat. + * + * If [allowAsync] is false and the player sends a message asynchronously, the future will be completed on the main thread on the next tick. + * Conversely, if [allowAsync] is true, the future will be completed immediately on message send. + */ + @JvmStatic + fun requestInput(player: Player, allowAsync: Boolean): CompletableFuture { + val future = CompletableFuture() + handlerLock.withLock { + handlers.getOrPut(player.uniqueId, ::mutableListOf).add(Handler(future, allowAsync)) + } + return future + } + + /** + * Returns a future that will be completed with the player's next chat message, or null if they disconnect before sending a message. + * The message sending will be canceled to prevent it from appearing in chat. + * + * This is a convenience overload of [requestInput] that defaults to not allowing asynchronous messages, so the future will always be completed on the main thread. + */ + @JvmStatic + fun requestInput(player: Player): CompletableFuture = requestInput(player, allowAsync = false) + + private data class Handler(val future: CompletableFuture, val allowAsync: Boolean) +} + +/** + * Suspends until the player sends a chat message, then returns the message, or null if they disconnect before sending a message. + * The message sending will be canceled to prevent it from appearing in chat. + */ +@JvmSynthetic +suspend fun Player.requestInput(): String? = PlayerInput.requestInput(this, allowAsync = true).await() \ No newline at end of file diff --git a/rebar/src/main/kotlin/io/github/pylonmc/rebar/util/RebarUtils.kt b/rebar/src/main/kotlin/io/github/pylonmc/rebar/util/RebarUtils.kt index fda9cfff8..d13f5e5ce 100644 --- a/rebar/src/main/kotlin/io/github/pylonmc/rebar/util/RebarUtils.kt +++ b/rebar/src/main/kotlin/io/github/pylonmc/rebar/util/RebarUtils.kt @@ -500,7 +500,7 @@ val Component.plainText: String */ fun blocksOnPath(from: BlockPosition, to: BlockPosition): List { val originBlock = from.block - val offset = to.location + val offset = to.toLocation() .subtract(originBlock.location) .toVector().toVector3i() diff --git a/rebar/src/main/kotlin/io/github/pylonmc/rebar/util/Vectors.kt b/rebar/src/main/kotlin/io/github/pylonmc/rebar/util/Vectors.kt new file mode 100644 index 000000000..2439eb7ed --- /dev/null +++ b/rebar/src/main/kotlin/io/github/pylonmc/rebar/util/Vectors.kt @@ -0,0 +1,119 @@ +package io.github.pylonmc.rebar.util + +import org.bukkit.util.Vector +import org.joml.Vector3f +import org.joml.Vector3i + +object Vectors { + + @JvmStatic + @get:JvmName("zero") + val zero: Vector + get() = Vector(0.0, 0.0, 0.0) + + @JvmStatic + @get:JvmName("positiveX") + val positiveX: Vector + get() = Vector(1.0, 0.0, 0.0) + + @JvmStatic + @get:JvmName("negativeX") + val negativeX: Vector + get() = Vector(-1.0, 0.0, 0.0) + + @JvmStatic + @get:JvmName("positiveY") + val positiveY: Vector + get() = Vector(0.0, 1.0, 0.0) + + @JvmStatic + @get:JvmName("negativeY") + val negativeY: Vector + get() = Vector(0.0, -1.0, 0.0) + + @JvmStatic + @get:JvmName("positiveZ") + val positiveZ: Vector + get() = Vector(0.0, 0.0, 1.0) + + @JvmStatic + @get:JvmName("negativeZ") + val negativeZ: Vector + get() = Vector(0.0, 0.0, -1.0) +} + +object Vector3is { + + @JvmStatic + @get:JvmName("zero") + val zero: Vector3i + get() = Vector3i(0, 0, 0) + + @JvmStatic + @get:JvmName("positiveX") + val positiveX: Vector3i + get() = Vector3i(1, 0, 0) + + @JvmStatic + @get:JvmName("negativeX") + val negativeX: Vector3i + get() = Vector3i(-1, 0, 0) + + @JvmStatic + @get:JvmName("positiveY") + val positiveY: Vector3i + get() = Vector3i(0, 1, 0) + + @JvmStatic + @get:JvmName("negativeY") + val negativeY: Vector3i + get() = Vector3i(0, -1, 0) + + @JvmStatic + @get:JvmName("positiveZ") + val positiveZ: Vector3i + get() = Vector3i(0, 0, 1) + + @JvmStatic + @get:JvmName("negativeZ") + val negativeZ: Vector3i + get() = Vector3i(0, 0, -1) +} + +object Vector3fs { + + @JvmStatic + @get:JvmName("zero") + val zero: Vector3f + get() = Vector3f(0.0f, 0.0f, 0.0f) + + @JvmStatic + @get:JvmName("positiveX") + val positiveX: Vector3f + get() = Vector3f(1.0f, 0.0f, 0.0f) + + @JvmStatic + @get:JvmName("negativeX") + val negativeX: Vector3f + get() = Vector3f(-1.0f, 0.0f, 0.0f) + + @JvmStatic + @get:JvmName("positiveY") + val positiveY: Vector3f + get() = Vector3f(0.0f, 1.0f, 0.0f) + + @JvmStatic + @get:JvmName("negativeY") + val negativeY: Vector3f + get() = Vector3f(0.0f, -1.0f, 0.0f) + + @JvmStatic + @get:JvmName("positiveZ") + val positiveZ: Vector3f + get() = Vector3f(0.0f, 0.0f, 1.0f) + + @JvmStatic + @get:JvmName("negativeZ") + val negativeZ: Vector3f + get() = Vector3f(0.0f, 0.0f, -1.0f) +} \ No newline at end of file diff --git a/rebar/src/main/kotlin/io/github/pylonmc/rebar/util/gui/unit/UnitFormat.kt b/rebar/src/main/kotlin/io/github/pylonmc/rebar/util/gui/unit/UnitFormat.kt index 588e5a1a6..29ddc8777 100644 --- a/rebar/src/main/kotlin/io/github/pylonmc/rebar/util/gui/unit/UnitFormat.kt +++ b/rebar/src/main/kotlin/io/github/pylonmc/rebar/util/gui/unit/UnitFormat.kt @@ -8,7 +8,7 @@ import java.math.BigDecimal import java.math.MathContext import java.math.RoundingMode import java.time.Duration -import java.util.EnumSet +import java.util.* /** * Handles formatting of a specific unit. Call [format] to format a value using this unit. @@ -36,13 +36,13 @@ class UnitFormat @JvmOverloads constructor( name: String, color: TextColor, abbreviate: Boolean, - prefix: MetricPrefix? = null, + prefix: MetricPrefix = MetricPrefix.NONE, ) : this( name = name, singular = Component.translatable("rebar.unit.$name.singular"), plural = Component.translatable("rebar.unit.$name.plural"), abbreviation = Component.translatable("rebar.unit.$name.abbr").takeIf { abbreviate }, - defaultPrefix = prefix ?: MetricPrefix.NONE, + defaultPrefix = prefix, defaultStyle = Style.style(color), ) @@ -185,8 +185,7 @@ class UnitFormat @JvmOverloads constructor( val BLOCKS_PER_SECOND = UnitFormat( "blocks_per_second", TextColor.color(0x0ae256), - abbreviate = true, - prefix = MetricPrefix.NONE + abbreviate = true ) @JvmField @@ -264,22 +263,6 @@ class UnitFormat @JvmOverloads constructor( abbreviate = true, ) - @JvmField - val JOULES = UnitFormat( - "joules", - TextColor.color(0xF2A900), - abbreviate = true, - prefix = MetricPrefix.NONE - ) - - @JvmField - val WATTS = UnitFormat( - "watts", - TextColor.color(0xF2A900), - abbreviate = true, - prefix = MetricPrefix.NONE - ) - @JvmField val EXPERIENCE = UnitFormat( "experience", @@ -320,8 +303,28 @@ class UnitFormat @JvmOverloads constructor( val CYCLES_PER_SECOND = UnitFormat( "cycles_per_second", TextColor.color(0xb672bf), - abbreviate = true, - prefix = MetricPrefix.NONE + abbreviate = true + ) + + @JvmField + val JOULES = UnitFormat( + "joules", + TextColor.color(0xF2A900), + abbreviate = true + ) + + @JvmField + val WATTS = UnitFormat( + "watts", + TextColor.color(0xF2A900), + abbreviate = true + ) + + @JvmField + val WATTS_PER_MILLIBUCKET = UnitFormat( + "watts_per_millibucket", + TextColor.color(0xF2A900), + abbreviate = true ) /** diff --git a/rebar/src/main/kotlin/io/github/pylonmc/rebar/util/position/BlockPosition.kt b/rebar/src/main/kotlin/io/github/pylonmc/rebar/util/position/BlockPosition.kt index 00874b37e..3f76065f4 100644 --- a/rebar/src/main/kotlin/io/github/pylonmc/rebar/util/position/BlockPosition.kt +++ b/rebar/src/main/kotlin/io/github/pylonmc/rebar/util/position/BlockPosition.kt @@ -8,7 +8,7 @@ import org.bukkit.block.BlockFace import org.bukkit.util.BoundingBox import org.bukkit.util.Vector import org.joml.Vector3i -import java.util.UUID +import java.util.* /** * Represents the position of a block (x, y, z, and world). @@ -103,17 +103,13 @@ class BlockPosition(val worldId: UUID?, val x: Int, val y: Int, val z: Int) { return BlockPosition(worldId, x / value, y / value, z / value) } - val vector3i: Vector3i - get() = Vector3i(x, y, z) + fun toVector3i(): Vector3i = Vector3i(x, y, z) - val vector: Vector - get() = Vector(x.toDouble(), y.toDouble(), z.toDouble()) + fun toLocation(): Location = Location(world, x.toDouble(), y.toDouble(), z.toDouble()) - val location: Location - get() = Location(world, x.toDouble(), y.toDouble(), z.toDouble()) + fun toVector(): Vector = Vector(x.toDouble(), y.toDouble(), z.toDouble()) - val boundingBox: BoundingBox - get() = BoundingBox(x.toDouble(), y.toDouble(), z.toDouble(), x + 1.0, y + 1.0, z + 1.0) + fun toBoundingBox(): BoundingBox = BoundingBox(x.toDouble(), y.toDouble(), z.toDouble(), x + 1.0, y + 1.0, z + 1.0) val block: Block get() = world?.getBlockAt(x, y, z) ?: error("World is null") diff --git a/rebar/src/main/resources/config.yml b/rebar/src/main/resources/config.yml index 4a9b983e5..5fc1ea651 100644 --- a/rebar/src/main/resources/config.yml +++ b/rebar/src/main/resources/config.yml @@ -17,6 +17,9 @@ fluid-tick-interval: 5 # The interval (in Minecraft ticks) between cargo ticks cargo-tick-interval: 20 +# The interval (in Minecraft ticks) between electricity network ticks +electricity-tick-interval: 5 + # The global multiplier for the number of items transferred per cargo tick (must be a whole number) cargo-transfer-rate-multiplier: 1 diff --git a/rebar/src/main/resources/lang/en.yml b/rebar/src/main/resources/lang/en.yml index dda0b05b8..9469a2a79 100644 --- a/rebar/src/main/resources/lang/en.yml +++ b/rebar/src/main/resources/lang/en.yml @@ -114,6 +114,12 @@ message: waila: type-disabled: "This server no longer permits the your previously set %type% WAILA display type, it has been reset to default" + electricity: + default: "Right click a port to connect, or stop holding a wire to cancel (requires %wires%/%total% wires)" + already_connected: "This node is already connected to another node" + blocking: "There is a block in the way of this connection" + need_wire: "You need to be holding a wire" + gui: scroll: up: "Scroll up" @@ -575,14 +581,6 @@ unit: singular: "second" plural: "seconds" abbr: "s" - joules: - singular: "joule" - plural: "joules" - abbr: "J" - watts: - singular: "watt" - plural: "watts" - abbr: "W" experience: singular: "experience point" plural: "experience points" @@ -605,3 +603,15 @@ unit: singular: "cycle per second" plural: "cycles per second" abbr: "cycles/s" + joules: + singular: "joule" + plural: "joules" + abbr: "J" + watts: + singular: "watt" + plural: "watts" + abbr: "W" + watts_per_millibucket: + singular: "watt per millibucket" + plural: "watts per millibucket" + abbr: "W/mB"