From 6918417af633d3a5be17f93648bf6e3446570f3e Mon Sep 17 00:00:00 2001 From: CoderJoe Date: Thu, 5 Mar 2026 23:21:34 -0600 Subject: [PATCH 1/7] Extract shared AtlasBlock base class, BlockRegistry, and BlockDescriptor into core package PowerBlock and FluidBlock now extend AtlasBlock, which owns the shared start/stop/updateVisualState lifecycle. PowerBlockRegistry and FluidBlockRegistry extend the generic BlockRegistry. Each concrete block class gains a companion BlockDescriptor with self-describing metadata and overrides for facing/baseBlockId properties. --- .../com/coderjoe/atlas/core/AtlasBlock.kt | 71 ++++++++++++ .../coderjoe/atlas/core/BlockDescriptor.kt | 20 ++++ .../com/coderjoe/atlas/core/BlockRegistry.kt | 78 +++++++++++++ .../com/coderjoe/atlas/fluid/FluidBlock.kt | 62 ++--------- .../atlas/fluid/FluidBlockRegistry.kt | 57 ++-------- .../atlas/fluid/block/FluidContainer.kt | 16 ++- .../coderjoe/atlas/fluid/block/FluidPipe.kt | 16 ++- .../coderjoe/atlas/fluid/block/FluidPump.kt | 14 +++ .../com/coderjoe/atlas/power/PowerBlock.kt | 66 ++--------- .../atlas/power/PowerBlockRegistry.kt | 103 ++---------------- .../coderjoe/atlas/power/block/PowerCable.kt | 16 ++- .../atlas/power/block/SmallBattery.kt | 16 ++- .../coderjoe/atlas/power/block/SmallDrill.kt | 15 +++ .../atlas/power/block/SmallSolarPanel.kt | 16 ++- .../kotlin/com/coderjoe/atlas/TestHelper.kt | 34 +++--- 15 files changed, 324 insertions(+), 276 deletions(-) create mode 100644 src/main/kotlin/com/coderjoe/atlas/core/AtlasBlock.kt create mode 100644 src/main/kotlin/com/coderjoe/atlas/core/BlockDescriptor.kt create mode 100644 src/main/kotlin/com/coderjoe/atlas/core/BlockRegistry.kt diff --git a/src/main/kotlin/com/coderjoe/atlas/core/AtlasBlock.kt b/src/main/kotlin/com/coderjoe/atlas/core/AtlasBlock.kt new file mode 100644 index 0000000..ce22002 --- /dev/null +++ b/src/main/kotlin/com/coderjoe/atlas/core/AtlasBlock.kt @@ -0,0 +1,71 @@ +package com.coderjoe.atlas.core + +import com.coderjoe.atlas.Atlas +import com.nexomc.nexo.api.NexoBlocks +import org.bukkit.Location +import org.bukkit.Material +import org.bukkit.block.BlockFace +import org.bukkit.plugin.java.JavaPlugin +import org.bukkit.scheduler.BukkitTask + +abstract class AtlasBlock( + val location: Location +) { + private var updateTask: BukkitTask? = null + protected val plugin: JavaPlugin get() = testPlugin ?: JavaPlugin.getPlugin(Atlas::class.java) + protected open val updateIntervalTicks: Long = 20L + private var currentVisualState: String? = null + + companion object { + @JvmStatic + internal var testPlugin: JavaPlugin? = null + } + + protected abstract fun blockUpdate() + abstract fun getVisualStateBlockId(): String + abstract fun getRegistry(): BlockRegistry<*> + + open val facing: BlockFace get() = BlockFace.SELF + open val baseBlockId: String get() = "" + + protected fun updateVisualState() { + val newState = getVisualStateBlockId() + if (newState != currentVisualState) { + val registry = getRegistry() + val key = BlockRegistry.locationKey(location) + registry.updatingLocations.add(key) + try { + location.block.setType(Material.AIR, false) + NexoBlocks.place(newState, location) + currentVisualState = newState + } finally { + registry.updatingLocations.remove(key) + } + } + } + + fun start() { + currentVisualState = NexoBlocks.customBlockMechanic(location.block)?.itemID + + plugin.server.scheduler.runTask(plugin, Runnable { + updateVisualState() + }) + + updateTask = plugin.server.scheduler.runTaskTimer(plugin, Runnable { + try { + blockUpdate() + updateVisualState() + } catch (e: Exception) { + plugin.logger.warning("Error in block tick at ${location.blockX},${location.blockY},${location.blockZ}: ${e.message}") + } + }, updateIntervalTicks, updateIntervalTicks) + + plugin.logger.info("${this::class.simpleName} at ${location.blockX},${location.blockY},${location.blockZ} started") + } + + fun stop() { + updateTask?.cancel() + updateTask = null + plugin.logger.info("${this::class.simpleName} at ${location.blockX},${location.blockY},${location.blockZ} stopped") + } +} diff --git a/src/main/kotlin/com/coderjoe/atlas/core/BlockDescriptor.kt b/src/main/kotlin/com/coderjoe/atlas/core/BlockDescriptor.kt new file mode 100644 index 0000000..11c4921 --- /dev/null +++ b/src/main/kotlin/com/coderjoe/atlas/core/BlockDescriptor.kt @@ -0,0 +1,20 @@ +package com.coderjoe.atlas.core + +import org.bukkit.Location +import org.bukkit.block.BlockFace + +enum class PlacementType { + SIMPLE, + DIRECTIONAL, + DIRECTIONAL_OPPOSITE +} + +data class BlockDescriptor( + val baseBlockId: String, + val displayName: String, + val description: String, + val placementType: PlacementType, + val directionalVariants: Map, + val allRegistrableIds: List, + val constructor: (Location, BlockFace) -> AtlasBlock +) diff --git a/src/main/kotlin/com/coderjoe/atlas/core/BlockRegistry.kt b/src/main/kotlin/com/coderjoe/atlas/core/BlockRegistry.kt new file mode 100644 index 0000000..9e9d836 --- /dev/null +++ b/src/main/kotlin/com/coderjoe/atlas/core/BlockRegistry.kt @@ -0,0 +1,78 @@ +package com.coderjoe.atlas.core + +import org.bukkit.Location +import org.bukkit.block.BlockFace +import org.bukkit.plugin.java.JavaPlugin +import java.util.concurrent.ConcurrentHashMap + +open class BlockRegistry(protected val plugin: JavaPlugin) { + protected val blocks = ConcurrentHashMap() + protected val blockIds = ConcurrentHashMap() + val updatingLocations: MutableSet = ConcurrentHashMap.newKeySet() + + companion object { + fun locationKey(location: Location): String { + return "${location.world?.name}:${location.blockX},${location.blockY},${location.blockZ}" + } + } + + fun register(block: T, blockId: String) { + val key = locationKey(block.location) + blocks[key] = block + blockIds[key] = blockId + block.start() + plugin.logger.info("Registered ${block::class.simpleName} at ${block.location.blockX},${block.location.blockY},${block.location.blockZ}") + } + + fun unregister(location: Location): T? { + val key = locationKey(location) + val block = blocks.remove(key) + blockIds.remove(key) + block?.stop() + if (block != null) { + plugin.logger.info("Unregistered ${block::class.simpleName} at ${location.blockX},${location.blockY},${location.blockZ}") + } + return block + } + + fun getBlock(location: Location): T? { + return blocks[locationKey(location)] + } + + fun getAdjacentBlock(location: Location, face: BlockFace): T? { + val offset = face.direction + return getBlock(Location(location.world, + (location.blockX + offset.blockX).toDouble(), + (location.blockY + offset.blockY).toDouble(), + (location.blockZ + offset.blockZ).toDouble())) + } + + fun getAdjacentBlocks(location: Location): List { + val offsets = listOf( + intArrayOf(1, 0, 0), intArrayOf(-1, 0, 0), + intArrayOf(0, 1, 0), intArrayOf(0, -1, 0), + intArrayOf(0, 0, 1), intArrayOf(0, 0, -1) + ) + return offsets.mapNotNull { (dx, dy, dz) -> + getBlock(Location(location.world, (location.blockX + dx).toDouble(), (location.blockY + dy).toDouble(), (location.blockZ + dz).toDouble())) + } + } + + fun getAllBlocksWithIds(): List> { + return blocks.entries.mapNotNull { entry -> + val block = entry.value + val blockId = blockIds[entry.key] + if (blockId != null) Pair(block, blockId) else null + } + } + + fun getAllBlocks(): Collection { + return blocks.values + } + + fun stopAll() { + plugin.logger.info("Stopping ${blocks.size} blocks...") + blocks.values.forEach { it.stop() } + blocks.clear() + } +} diff --git a/src/main/kotlin/com/coderjoe/atlas/fluid/FluidBlock.kt b/src/main/kotlin/com/coderjoe/atlas/fluid/FluidBlock.kt index 407ecba..1073fcc 100644 --- a/src/main/kotlin/com/coderjoe/atlas/fluid/FluidBlock.kt +++ b/src/main/kotlin/com/coderjoe/atlas/fluid/FluidBlock.kt @@ -1,24 +1,13 @@ package com.coderjoe.atlas.fluid -import com.coderjoe.atlas.Atlas -import com.nexomc.nexo.api.NexoBlocks +import com.coderjoe.atlas.core.AtlasBlock +import com.coderjoe.atlas.core.BlockRegistry import org.bukkit.Location -import org.bukkit.Material -import org.bukkit.plugin.java.JavaPlugin -import org.bukkit.scheduler.BukkitTask abstract class FluidBlock( - val location: Location, + location: Location, var storedFluid: FluidType = FluidType.NONE -) { - private var updateTask: BukkitTask? = null - protected val plugin: JavaPlugin get() = testPlugin ?: JavaPlugin.getPlugin(Atlas::class.java) - protected open val updateIntervalTicks: Long = 20L - - companion object { - @JvmStatic - internal var testPlugin: JavaPlugin? = null - } +) : AtlasBlock(location) { open fun hasFluid(): Boolean = storedFluid != FluidType.NONE @@ -35,47 +24,12 @@ abstract class FluidBlock( } protected abstract fun fluidUpdate() - abstract fun getVisualStateBlockId(): String - private var currentVisualState: String? = null - - protected fun updateVisualState() { - val newState = getVisualStateBlockId() - if (newState != currentVisualState) { - val key = FluidBlockRegistry.locationKey(location) - val registry = FluidBlockRegistry.instance ?: return - registry.updatingLocations.add(key) - try { - location.block.setType(Material.AIR, false) - NexoBlocks.place(newState, location) - currentVisualState = newState - } finally { - registry.updatingLocations.remove(key) - } - } - } - - fun start() { - currentVisualState = NexoBlocks.customBlockMechanic(location.block)?.itemID - - plugin.server.scheduler.runTask(plugin, Runnable { - updateVisualState() - }) - - updateTask = plugin.server.scheduler.runTaskTimer(plugin, Runnable { - try { - fluidUpdate() - updateVisualState() - } catch (e: Exception) { - plugin.logger.warning("Error in fluid block tick at ${location.blockX},${location.blockY},${location.blockZ}: ${e.message}") - } - }, updateIntervalTicks, updateIntervalTicks) - plugin.logger.info("${this::class.simpleName} at ${location.blockX},${location.blockY},${location.blockZ} started") + override fun blockUpdate() { + fluidUpdate() } - fun stop() { - updateTask?.cancel() - updateTask = null - plugin.logger.info("${this::class.simpleName} at ${location.blockX},${location.blockY},${location.blockZ} stopped") + override fun getRegistry(): BlockRegistry<*> { + return FluidBlockRegistry.instance ?: throw IllegalStateException("FluidBlockRegistry not initialized") } } diff --git a/src/main/kotlin/com/coderjoe/atlas/fluid/FluidBlockRegistry.kt b/src/main/kotlin/com/coderjoe/atlas/fluid/FluidBlockRegistry.kt index 740bcba..8496eeb 100644 --- a/src/main/kotlin/com/coderjoe/atlas/fluid/FluidBlockRegistry.kt +++ b/src/main/kotlin/com/coderjoe/atlas/fluid/FluidBlockRegistry.kt @@ -1,71 +1,30 @@ package com.coderjoe.atlas.fluid +import com.coderjoe.atlas.core.BlockRegistry import org.bukkit.Location import org.bukkit.block.BlockFace import org.bukkit.plugin.java.JavaPlugin -import java.util.concurrent.ConcurrentHashMap -class FluidBlockRegistry(private val plugin: JavaPlugin) { - private val fluidBlocks = ConcurrentHashMap() - private val blockIds = ConcurrentHashMap() - val updatingLocations: MutableSet = ConcurrentHashMap.newKeySet() +class FluidBlockRegistry(plugin: JavaPlugin) : BlockRegistry(plugin) { companion object { var instance: FluidBlockRegistry? = null private set - fun locationKey(location: Location): String { - return "${location.world?.name}:${location.blockX},${location.blockY},${location.blockZ}" - } + fun locationKey(location: Location): String = BlockRegistry.locationKey(location) } init { instance = this } - fun registerFluidBlock(fluidBlock: FluidBlock, blockId: String) { - val key = locationKey(fluidBlock.location) - fluidBlocks[key] = fluidBlock - blockIds[key] = blockId - fluidBlock.start() - plugin.logger.info("Registered ${fluidBlock::class.simpleName} at ${fluidBlock.location.blockX},${fluidBlock.location.blockY},${fluidBlock.location.blockZ}") - } - - fun unregisterFluidBlock(location: Location): FluidBlock? { - val key = locationKey(location) - val fluidBlock = fluidBlocks.remove(key) - blockIds.remove(key) - fluidBlock?.stop() - if (fluidBlock != null) { - plugin.logger.info("Unregistered ${fluidBlock::class.simpleName} at ${location.blockX},${location.blockY},${location.blockZ}") - } - return fluidBlock - } - - fun getFluidBlock(location: Location): FluidBlock? { - return fluidBlocks[locationKey(location)] - } + fun registerFluidBlock(fluidBlock: FluidBlock, blockId: String) = register(fluidBlock, blockId) - fun getAdjacentFluidBlock(location: Location, face: BlockFace): FluidBlock? { - val offset = face.direction - return getFluidBlock(Location(location.world, - (location.blockX + offset.blockX).toDouble(), - (location.blockY + offset.blockY).toDouble(), - (location.blockZ + offset.blockZ).toDouble())) - } + fun unregisterFluidBlock(location: Location): FluidBlock? = unregister(location) - fun stopAll() { - plugin.logger.info("Stopping ${fluidBlocks.size} fluid blocks...") - fluidBlocks.values.forEach { it.stop() } - fluidBlocks.clear() - } + fun getFluidBlock(location: Location): FluidBlock? = getBlock(location) - fun getAllFluidBlocksWithIds(): List> { - return fluidBlocks.entries.mapNotNull { entry -> - val fluidBlock = entry.value - val blockId = blockIds[entry.key] - if (blockId != null) Pair(fluidBlock, blockId) else null - } - } + fun getAdjacentFluidBlock(location: Location, face: BlockFace): FluidBlock? = getAdjacentBlock(location, face) + fun getAllFluidBlocksWithIds(): List> = getAllBlocksWithIds() } diff --git a/src/main/kotlin/com/coderjoe/atlas/fluid/block/FluidContainer.kt b/src/main/kotlin/com/coderjoe/atlas/fluid/block/FluidContainer.kt index 4ea37a9..6cce10b 100644 --- a/src/main/kotlin/com/coderjoe/atlas/fluid/block/FluidContainer.kt +++ b/src/main/kotlin/com/coderjoe/atlas/fluid/block/FluidContainer.kt @@ -1,12 +1,14 @@ package com.coderjoe.atlas.fluid.block +import com.coderjoe.atlas.core.BlockDescriptor +import com.coderjoe.atlas.core.PlacementType import com.coderjoe.atlas.fluid.FluidBlock import com.coderjoe.atlas.fluid.FluidBlockRegistry import com.coderjoe.atlas.fluid.FluidType import org.bukkit.Location import org.bukkit.block.BlockFace -class FluidContainer(location: Location, val facing: BlockFace) : FluidBlock(location) { +class FluidContainer(location: Location, override val facing: BlockFace) : FluidBlock(location) { var storedAmount: Int = 0 private set @@ -67,8 +69,20 @@ class FluidContainer(location: Location, val facing: BlockFace) : FluidBlock(loc } return null } + + val descriptor = BlockDescriptor( + baseBlockId = BLOCK_ID, + displayName = "Fluid Container", + description = "Container - stores up to $MAX_CAPACITY units of fluid", + placementType = PlacementType.DIRECTIONAL, + directionalVariants = DIRECTIONAL_IDS, + allRegistrableIds = ALL_VARIANT_IDS, + constructor = { loc, facing -> FluidContainer(loc, facing) } + ) } + override val baseBlockId: String = BLOCK_ID + override fun hasFluid(): Boolean = storedAmount > 0 override fun storeFluid(type: FluidType): Boolean { diff --git a/src/main/kotlin/com/coderjoe/atlas/fluid/block/FluidPipe.kt b/src/main/kotlin/com/coderjoe/atlas/fluid/block/FluidPipe.kt index 56c7575..43860c2 100644 --- a/src/main/kotlin/com/coderjoe/atlas/fluid/block/FluidPipe.kt +++ b/src/main/kotlin/com/coderjoe/atlas/fluid/block/FluidPipe.kt @@ -1,12 +1,14 @@ package com.coderjoe.atlas.fluid.block +import com.coderjoe.atlas.core.BlockDescriptor +import com.coderjoe.atlas.core.PlacementType import com.coderjoe.atlas.fluid.FluidBlock import com.coderjoe.atlas.fluid.FluidBlockRegistry import com.coderjoe.atlas.fluid.FluidType import org.bukkit.Location import org.bukkit.block.BlockFace -class FluidPipe(location: Location, val facing: BlockFace) : FluidBlock(location) { +class FluidPipe(location: Location, override val facing: BlockFace) : FluidBlock(location) { override val updateIntervalTicks: Long = 20L @@ -43,8 +45,20 @@ class FluidPipe(location: Location, val facing: BlockFace) : FluidBlock(location ) fun facingFromBlockId(blockId: String): BlockFace? = ID_TO_FACING[blockId] + + val descriptor = BlockDescriptor( + baseBlockId = BLOCK_ID, + displayName = "Fluid Pipe", + description = "Pipe - transports fluid in facing direction", + placementType = PlacementType.DIRECTIONAL, + directionalVariants = DIRECTIONAL_IDS, + allRegistrableIds = DIRECTIONAL_IDS.values.toList() + WATER_FILLED_IDS.values.toList() + LAVA_FILLED_IDS.values.toList(), + constructor = { loc, facing -> FluidPipe(loc, facing) } + ) } + override val baseBlockId: String = BLOCK_ID + override fun getVisualStateBlockId(): String = when (storedFluid) { FluidType.WATER -> WATER_FILLED_IDS[facing]!! FluidType.LAVA -> LAVA_FILLED_IDS[facing]!! diff --git a/src/main/kotlin/com/coderjoe/atlas/fluid/block/FluidPump.kt b/src/main/kotlin/com/coderjoe/atlas/fluid/block/FluidPump.kt index 2c542ff..386b6ab 100644 --- a/src/main/kotlin/com/coderjoe/atlas/fluid/block/FluidPump.kt +++ b/src/main/kotlin/com/coderjoe/atlas/fluid/block/FluidPump.kt @@ -1,5 +1,7 @@ package com.coderjoe.atlas.fluid.block +import com.coderjoe.atlas.core.BlockDescriptor +import com.coderjoe.atlas.core.PlacementType import com.coderjoe.atlas.fluid.FluidBlock import com.coderjoe.atlas.fluid.FluidType import com.coderjoe.atlas.power.PowerBlockRegistry @@ -36,8 +38,20 @@ class FluidPump(location: Location) : FluidBlock(location) { BlockFace.NORTH, BlockFace.SOUTH, BlockFace.EAST, BlockFace.WEST, BlockFace.UP, BlockFace.DOWN ) + + val descriptor = BlockDescriptor( + baseBlockId = BLOCK_ID, + displayName = "Fluid Pump", + description = "Pump - extracts fluid from adjacent cauldrons (1 power/s)", + placementType = PlacementType.SIMPLE, + directionalVariants = emptyMap(), + allRegistrableIds = listOf(BLOCK_ID, BLOCK_ID_ACTIVE, BLOCK_ID_ACTIVE_LAVA), + constructor = { loc, _ -> FluidPump(loc) } + ) } + override val baseBlockId: String = BLOCK_ID + fun canRemoveFluidFrom(direction: BlockFace): Boolean { val cauldron = cauldronFace ?: return false return direction == cauldron.oppositeFace && hasFluid() diff --git a/src/main/kotlin/com/coderjoe/atlas/power/PowerBlock.kt b/src/main/kotlin/com/coderjoe/atlas/power/PowerBlock.kt index 22278fb..671de83 100644 --- a/src/main/kotlin/com/coderjoe/atlas/power/PowerBlock.kt +++ b/src/main/kotlin/com/coderjoe/atlas/power/PowerBlock.kt @@ -1,28 +1,15 @@ package com.coderjoe.atlas.power -import com.coderjoe.atlas.Atlas -import com.nexomc.nexo.api.NexoBlocks +import com.coderjoe.atlas.core.AtlasBlock +import com.coderjoe.atlas.core.BlockRegistry import org.bukkit.Location -import org.bukkit.Material -import org.bukkit.plugin.java.JavaPlugin -import org.bukkit.scheduler.BukkitTask -/** - * Represents a block that participates in the power network - */ abstract class PowerBlock( - val location: Location, + location: Location, val maxStorage: Int, var currentPower: Int = 0 -) { - private var updateTask: BukkitTask? = null - protected val plugin: JavaPlugin get() = testPlugin ?: JavaPlugin.getPlugin(Atlas::class.java) - protected open val updateIntervalTicks: Long = 100L +) : AtlasBlock(location) { - companion object { - @JvmStatic - internal var testPlugin: JavaPlugin? = null - } protected open val canReceivePower: Boolean = true fun hasPower(): Boolean = currentPower > 0 @@ -43,49 +30,12 @@ abstract class PowerBlock( } protected abstract fun powerUpdate() - abstract fun getVisualStateBlockId(): String - private var currentVisualState: String? = null - - protected fun updateVisualState() { - val newState = getVisualStateBlockId() - if (newState != currentVisualState) { - val key = PowerBlockRegistry.locationKey(location) - val registry = PowerBlockRegistry.instance ?: return - registry.updatingLocations.add(key) - try { - location.block.setType(Material.AIR, false) - NexoBlocks.place(newState, location) - currentVisualState = newState - } finally { - registry.updatingLocations.remove(key) - } - } - } - - fun start() { - // Snapshot the current block so the deferred update is a no-op when already correct - currentVisualState = NexoBlocks.customBlockMechanic(location.block)?.itemID - - // Defer to next tick — corrects visual state if it doesn't match (e.g. after persistence load) - plugin.server.scheduler.runTask(plugin, Runnable { - updateVisualState() - }) - - updateTask = plugin.server.scheduler.runTaskTimer(plugin, Runnable { - try { - powerUpdate() - updateVisualState() - } catch (e: Exception) { - plugin.logger.warning("Error in power block tick at ${location.blockX},${location.blockY},${location.blockZ}: ${e.message}") - } - }, updateIntervalTicks, updateIntervalTicks) - plugin.logger.info("${this::class.simpleName} at ${location.blockX},${location.blockY},${location.blockZ} started - updating every ${updateIntervalTicks / 20} seconds") + override fun blockUpdate() { + powerUpdate() } - fun stop() { - updateTask?.cancel() - updateTask = null - plugin.logger.info("${this::class.simpleName} at ${location.blockX},${location.blockY},${location.blockZ} stopped") + override fun getRegistry(): BlockRegistry<*> { + return PowerBlockRegistry.instance ?: throw IllegalStateException("PowerBlockRegistry not initialized") } } diff --git a/src/main/kotlin/com/coderjoe/atlas/power/PowerBlockRegistry.kt b/src/main/kotlin/com/coderjoe/atlas/power/PowerBlockRegistry.kt index 7deccec..da1c0b9 100644 --- a/src/main/kotlin/com/coderjoe/atlas/power/PowerBlockRegistry.kt +++ b/src/main/kotlin/com/coderjoe/atlas/power/PowerBlockRegistry.kt @@ -1,117 +1,34 @@ package com.coderjoe.atlas.power +import com.coderjoe.atlas.core.BlockRegistry import org.bukkit.Location import org.bukkit.block.BlockFace import org.bukkit.plugin.java.JavaPlugin -import java.util.concurrent.ConcurrentHashMap -/** - * Registry to track all active power blocks in the world - */ -class PowerBlockRegistry(private val plugin: JavaPlugin) { - private val powerBlocks = ConcurrentHashMap() - private val blockIds = ConcurrentHashMap() // Maps location key to block ID - val updatingLocations: MutableSet = ConcurrentHashMap.newKeySet() +class PowerBlockRegistry(plugin: JavaPlugin) : BlockRegistry(plugin) { companion object { var instance: PowerBlockRegistry? = null private set - fun locationKey(location: Location): String { - return "${location.world?.name}:${location.blockX},${location.blockY},${location.blockZ}" - } + fun locationKey(location: Location): String = BlockRegistry.locationKey(location) } init { instance = this } - /** - * Registers and starts a power block - */ - fun registerPowerBlock(powerBlock: PowerBlock, blockId: String) { - val key = locationKey(powerBlock.location) - powerBlocks[key] = powerBlock - blockIds[key] = blockId - powerBlock.start() - plugin.logger.info("Registered ${powerBlock::class.simpleName} at ${powerBlock.location.blockX},${powerBlock.location.blockY},${powerBlock.location.blockZ}") - } - - /** - * Unregisters and stops a power block at the given location - */ - fun unregisterPowerBlock(location: Location): PowerBlock? { - val key = locationKey(location) - val powerBlock = powerBlocks.remove(key) - blockIds.remove(key) - powerBlock?.stop() - if (powerBlock != null) { - plugin.logger.info("Unregistered ${powerBlock::class.simpleName} at ${location.blockX},${location.blockY},${location.blockZ}") - } - return powerBlock - } - - /** - * Gets a power block at the given location - */ - fun getPowerBlock(location: Location): PowerBlock? { - return powerBlocks[locationKey(location)] - } + fun registerPowerBlock(powerBlock: PowerBlock, blockId: String) = register(powerBlock, blockId) - /** - * Stops all power blocks (called on plugin disable) - */ - fun stopAll() { - plugin.logger.info("Stopping ${powerBlocks.size} power blocks...") - powerBlocks.values.forEach { it.stop() } - powerBlocks.clear() - } + fun unregisterPowerBlock(location: Location): PowerBlock? = unregister(location) - /** - * Gets all registered power blocks - */ - fun getAllPowerBlocks(): Collection { - return powerBlocks.values - } + fun getPowerBlock(location: Location): PowerBlock? = getBlock(location) - /** - * Gets all power blocks with their block IDs for persistence - */ - fun getAllPowerBlocksWithIds(): List> { - return powerBlocks.entries.mapNotNull { entry -> - val powerBlock = entry.value - val blockId = blockIds[entry.key] - if (blockId != null) { - Pair(powerBlock, blockId) - } else { - null - } - } - } + fun getAdjacentPowerBlock(location: Location, face: BlockFace): PowerBlock? = getAdjacentBlock(location, face) - /** - * Gets the power block adjacent to the given location in the specified direction - */ - fun getAdjacentPowerBlock(location: Location, face: BlockFace): PowerBlock? { - val offset = face.direction - return getPowerBlock(Location(location.world, - (location.blockX + offset.blockX).toDouble(), - (location.blockY + offset.blockY).toDouble(), - (location.blockZ + offset.blockZ).toDouble())) - } + fun getAdjacentPowerBlocks(location: Location): List = getAdjacentBlocks(location) - /** - * Gets all power blocks adjacent (6 directions) to the given location - */ - fun getAdjacentPowerBlocks(location: Location): List { - val offsets = listOf( - intArrayOf(1, 0, 0), intArrayOf(-1, 0, 0), - intArrayOf(0, 1, 0), intArrayOf(0, -1, 0), - intArrayOf(0, 0, 1), intArrayOf(0, 0, -1) - ) - return offsets.mapNotNull { (dx, dy, dz) -> - getPowerBlock(Location(location.world, (location.blockX + dx).toDouble(), (location.blockY + dy).toDouble(), (location.blockZ + dz).toDouble())) - } - } + fun getAllPowerBlocksWithIds(): List> = getAllBlocksWithIds() + fun getAllPowerBlocks(): Collection = getAllBlocks() } diff --git a/src/main/kotlin/com/coderjoe/atlas/power/block/PowerCable.kt b/src/main/kotlin/com/coderjoe/atlas/power/block/PowerCable.kt index dc57c91..2af5b8c 100644 --- a/src/main/kotlin/com/coderjoe/atlas/power/block/PowerCable.kt +++ b/src/main/kotlin/com/coderjoe/atlas/power/block/PowerCable.kt @@ -1,11 +1,13 @@ package com.coderjoe.atlas.power.block +import com.coderjoe.atlas.core.BlockDescriptor +import com.coderjoe.atlas.core.PlacementType import com.coderjoe.atlas.power.PowerBlock import com.coderjoe.atlas.power.PowerBlockRegistry import org.bukkit.Location import org.bukkit.block.BlockFace -class PowerCable(location: Location, val facing: BlockFace) : PowerBlock(location, maxStorage = 1) { +class PowerCable(location: Location, override val facing: BlockFace) : PowerBlock(location, maxStorage = 1) { companion object { const val BLOCK_ID = "power_cable" @@ -31,8 +33,20 @@ class PowerCable(location: Location, val facing: BlockFace) : PowerBlock(locatio ) fun facingFromBlockId(blockId: String): BlockFace? = ID_TO_FACING[blockId] + + val descriptor = BlockDescriptor( + baseBlockId = BLOCK_ID, + displayName = "Power Cable", + description = "Cable - transfers power in facing direction", + placementType = PlacementType.DIRECTIONAL, + directionalVariants = DIRECTIONAL_IDS, + allRegistrableIds = DIRECTIONAL_IDS.values.toList(), + constructor = { loc, facing -> PowerCable(loc, facing) } + ) } + override val baseBlockId: String = BLOCK_ID + override val updateIntervalTicks: Long = 20L // 1 second override fun getVisualStateBlockId(): String = diff --git a/src/main/kotlin/com/coderjoe/atlas/power/block/SmallBattery.kt b/src/main/kotlin/com/coderjoe/atlas/power/block/SmallBattery.kt index 997cffb..7706b8a 100644 --- a/src/main/kotlin/com/coderjoe/atlas/power/block/SmallBattery.kt +++ b/src/main/kotlin/com/coderjoe/atlas/power/block/SmallBattery.kt @@ -1,5 +1,7 @@ package com.coderjoe.atlas.power.block +import com.coderjoe.atlas.core.BlockDescriptor +import com.coderjoe.atlas.core.PlacementType import com.coderjoe.atlas.power.PowerBlock import com.coderjoe.atlas.power.PowerBlockRegistry import org.bukkit.Location @@ -7,7 +9,7 @@ import org.bukkit.block.BlockFace class SmallBattery(location: Location, facing: BlockFace) : PowerBlock(location, maxStorage = 10) { - val facing: BlockFace = if (facing == BlockFace.SELF) BlockFace.DOWN else facing + override val facing: BlockFace = if (facing == BlockFace.SELF) BlockFace.DOWN else facing override val canReceivePower: Boolean = true override val updateIntervalTicks: Long = 20L @@ -23,8 +25,20 @@ class SmallBattery(location: Location, facing: BlockFace) : PowerBlock(location, ) val ALL_VARIANT_IDS: List = CHARGE_VARIANT_IDS.values.toList() + + val descriptor = BlockDescriptor( + baseBlockId = BLOCK_ID, + displayName = "Small Battery", + description = "Storage - holds up to 10 power", + placementType = PlacementType.SIMPLE, + directionalVariants = emptyMap(), + allRegistrableIds = ALL_VARIANT_IDS, + constructor = { loc, facing -> SmallBattery(loc, facing) } + ) } + override val baseBlockId: String = BLOCK_ID + private fun chargeLevel(): Int = when (currentPower) { 0 -> 0 in 1..3 -> 1 diff --git a/src/main/kotlin/com/coderjoe/atlas/power/block/SmallDrill.kt b/src/main/kotlin/com/coderjoe/atlas/power/block/SmallDrill.kt index 0aa37d0..a58ff68 100644 --- a/src/main/kotlin/com/coderjoe/atlas/power/block/SmallDrill.kt +++ b/src/main/kotlin/com/coderjoe/atlas/power/block/SmallDrill.kt @@ -1,5 +1,7 @@ package com.coderjoe.atlas.power.block +import com.coderjoe.atlas.core.BlockDescriptor +import com.coderjoe.atlas.core.PlacementType import com.coderjoe.atlas.power.PowerBlock import com.coderjoe.atlas.power.PowerBlockRegistry import org.bukkit.Location @@ -28,8 +30,21 @@ class SmallDrill(location: Location, facing: BlockFace? = null) : PowerBlock(loc ) val ALL_DIRECTIONAL_IDS: List = DIRECTIONAL_IDS.values.toList() + + val descriptor = BlockDescriptor( + baseBlockId = BLOCK_ID, + displayName = "Small Drill", + description = "Machine - consumes 10 power/s", + placementType = PlacementType.DIRECTIONAL_OPPOSITE, + directionalVariants = DIRECTIONAL_IDS, + allRegistrableIds = ALL_DIRECTIONAL_IDS, + constructor = { loc, facing -> SmallDrill(loc, facing) } + ) } + override val baseBlockId: String = BLOCK_ID + override val facing: BlockFace get() = miningDirection + override fun getVisualStateBlockId(): String = DIRECTIONAL_IDS[miningDirection] ?: BLOCK_ID diff --git a/src/main/kotlin/com/coderjoe/atlas/power/block/SmallSolarPanel.kt b/src/main/kotlin/com/coderjoe/atlas/power/block/SmallSolarPanel.kt index 4a22cd1..4d878f3 100644 --- a/src/main/kotlin/com/coderjoe/atlas/power/block/SmallSolarPanel.kt +++ b/src/main/kotlin/com/coderjoe/atlas/power/block/SmallSolarPanel.kt @@ -1,8 +1,10 @@ package com.coderjoe.atlas.power.block +import com.coderjoe.atlas.core.BlockDescriptor +import com.coderjoe.atlas.core.PlacementType import com.coderjoe.atlas.power.PowerBlock -import com.coderjoe.atlas.power.PowerBlockFactory import org.bukkit.Location +import org.bukkit.block.BlockFace class SmallSolarPanel(location: Location): PowerBlock(location, maxStorage = 1) { @@ -11,8 +13,20 @@ class SmallSolarPanel(location: Location): PowerBlock(location, maxStorage = 1) companion object { const val BLOCK_ID = "small_solar_panel" + + val descriptor = BlockDescriptor( + baseBlockId = BLOCK_ID, + displayName = "Small Solar Panel", + description = "Generator - produces 1 power/min during daytime", + placementType = PlacementType.SIMPLE, + directionalVariants = emptyMap(), + allRegistrableIds = listOf(BLOCK_ID), + constructor = { loc, _ -> SmallSolarPanel(loc) } + ) } + override val baseBlockId: String = BLOCK_ID + override fun getVisualStateBlockId(): String = when (currentPower) { 0 -> "small_solar_panel" else -> "small_solar_panel_full" diff --git a/src/test/kotlin/com/coderjoe/atlas/TestHelper.kt b/src/test/kotlin/com/coderjoe/atlas/TestHelper.kt index 0d3e177..3ef6f0f 100644 --- a/src/test/kotlin/com/coderjoe/atlas/TestHelper.kt +++ b/src/test/kotlin/com/coderjoe/atlas/TestHelper.kt @@ -1,5 +1,7 @@ package com.coderjoe.atlas +import com.coderjoe.atlas.core.AtlasBlock +import com.coderjoe.atlas.core.BlockRegistry import com.coderjoe.atlas.fluid.FluidBlock import com.coderjoe.atlas.fluid.FluidBlockFactory import com.coderjoe.atlas.fluid.FluidBlockRegistry @@ -33,9 +35,8 @@ object TestHelper { dataFolder = File(System.getProperty("java.io.tmpdir"), "atlas-test-${System.nanoTime()}") dataFolder.mkdirs() - // Set test plugin hooks on base classes (avoids JavaPlugin.getPlugin() call) - PowerBlock.testPlugin = mockPlugin - FluidBlock.testPlugin = mockPlugin + // Set test plugin hook on base class (avoids JavaPlugin.getPlugin() call) + AtlasBlock.testPlugin = mockPlugin every { mockPlugin.server } returns mockServer every { mockPlugin.logger } returns Logger.getLogger("TestAtlas") @@ -57,8 +58,7 @@ object TestHelper { fun teardown() { unmockkAll() - PowerBlock.testPlugin = null - FluidBlock.testPlugin = null + AtlasBlock.testPlugin = null clearRegistries() clearFactories() dataFolder.deleteRecursively() @@ -69,46 +69,46 @@ object TestHelper { } fun PowerBlock.callPowerUpdate() { - val method = this::class.java.getDeclaredMethod("powerUpdate") + val method = PowerBlock::class.java.getDeclaredMethod("powerUpdate") method.isAccessible = true method.invoke(this) } fun FluidBlock.callFluidUpdate() { - val method = this::class.java.getDeclaredMethod("fluidUpdate") + val method = FluidBlock::class.java.getDeclaredMethod("fluidUpdate") method.isAccessible = true method.invoke(this) } fun addToRegistry(registry: PowerBlockRegistry, block: PowerBlock, blockId: String) { - val powerBlocksField = PowerBlockRegistry::class.java.getDeclaredField("powerBlocks") - powerBlocksField.isAccessible = true + val blocksField = BlockRegistry::class.java.getDeclaredField("blocks") + blocksField.isAccessible = true @Suppress("UNCHECKED_CAST") - val powerBlocks = powerBlocksField.get(registry) as java.util.concurrent.ConcurrentHashMap + val blocks = blocksField.get(registry) as java.util.concurrent.ConcurrentHashMap - val blockIdsField = PowerBlockRegistry::class.java.getDeclaredField("blockIds") + val blockIdsField = BlockRegistry::class.java.getDeclaredField("blockIds") blockIdsField.isAccessible = true @Suppress("UNCHECKED_CAST") val blockIds = blockIdsField.get(registry) as java.util.concurrent.ConcurrentHashMap val key = PowerBlockRegistry.locationKey(block.location) - powerBlocks[key] = block + blocks[key] = block blockIds[key] = blockId } fun addToRegistry(registry: FluidBlockRegistry, block: FluidBlock, blockId: String) { - val fluidBlocksField = FluidBlockRegistry::class.java.getDeclaredField("fluidBlocks") - fluidBlocksField.isAccessible = true + val blocksField = BlockRegistry::class.java.getDeclaredField("blocks") + blocksField.isAccessible = true @Suppress("UNCHECKED_CAST") - val fluidBlocks = fluidBlocksField.get(registry) as java.util.concurrent.ConcurrentHashMap + val blocks = blocksField.get(registry) as java.util.concurrent.ConcurrentHashMap - val blockIdsField = FluidBlockRegistry::class.java.getDeclaredField("blockIds") + val blockIdsField = BlockRegistry::class.java.getDeclaredField("blockIds") blockIdsField.isAccessible = true @Suppress("UNCHECKED_CAST") val blockIds = blockIdsField.get(registry) as java.util.concurrent.ConcurrentHashMap val key = FluidBlockRegistry.locationKey(block.location) - fluidBlocks[key] = block + blocks[key] = block blockIds[key] = blockId } From 6e5323d2adea6407d555f153509c73108a4e3c3b Mon Sep 17 00:00:00 2001 From: CoderJoe Date: Thu, 5 Mar 2026 23:50:19 -0600 Subject: [PATCH 2/7] Extract generic BlockFactory base class for PowerBlockFactory and FluidBlockFactory Both factories now extend BlockFactory, which owns the registration map and create/isRegistered/getRegisteredBlockIds/clear methods. Domain factories retain their convenience delegate methods (createPowerBlock, createFluidBlock). --- .../com/coderjoe/atlas/core/BlockFactory.kt | 28 ++++++++++++++ .../coderjoe/atlas/fluid/FluidBlockFactory.kt | 19 ++-------- .../coderjoe/atlas/power/PowerBlockFactory.kt | 37 ++----------------- .../kotlin/com/coderjoe/atlas/TestHelper.kt | 15 +------- 4 files changed, 36 insertions(+), 63 deletions(-) create mode 100644 src/main/kotlin/com/coderjoe/atlas/core/BlockFactory.kt diff --git a/src/main/kotlin/com/coderjoe/atlas/core/BlockFactory.kt b/src/main/kotlin/com/coderjoe/atlas/core/BlockFactory.kt new file mode 100644 index 0000000..cc0c2d3 --- /dev/null +++ b/src/main/kotlin/com/coderjoe/atlas/core/BlockFactory.kt @@ -0,0 +1,28 @@ +package com.coderjoe.atlas.core + +import org.bukkit.Location +import org.bukkit.block.BlockFace + +open class BlockFactory { + private val blockConstructors = mutableMapOf T>() + + fun register(blockId: String, constructor: (Location, BlockFace) -> T) { + blockConstructors[blockId] = constructor + } + + fun create(blockId: String, location: Location, facing: BlockFace = BlockFace.SELF): T? { + return blockConstructors[blockId]?.invoke(location, facing) + } + + fun isRegistered(blockId: String): Boolean { + return blockConstructors.containsKey(blockId) + } + + fun getRegisteredBlockIds(): Set { + return blockConstructors.keys + } + + fun clear() { + blockConstructors.clear() + } +} diff --git a/src/main/kotlin/com/coderjoe/atlas/fluid/FluidBlockFactory.kt b/src/main/kotlin/com/coderjoe/atlas/fluid/FluidBlockFactory.kt index 33d8898..6adb444 100644 --- a/src/main/kotlin/com/coderjoe/atlas/fluid/FluidBlockFactory.kt +++ b/src/main/kotlin/com/coderjoe/atlas/fluid/FluidBlockFactory.kt @@ -1,25 +1,12 @@ package com.coderjoe.atlas.fluid +import com.coderjoe.atlas.core.BlockFactory import org.bukkit.Location import org.bukkit.block.BlockFace -object FluidBlockFactory { - private val blockConstructors = mutableMapOf FluidBlock>() - - fun register(blockId: String, constructor: (Location, BlockFace) -> FluidBlock) { - blockConstructors[blockId] = constructor - println("FluidBlockFactory: Registered block ID '$blockId'") - } +object FluidBlockFactory : BlockFactory() { fun createFluidBlock(blockId: String, location: Location, facing: BlockFace = BlockFace.SELF): FluidBlock? { - return blockConstructors[blockId]?.invoke(location, facing) - } - - fun isRegistered(blockId: String): Boolean { - return blockConstructors.containsKey(blockId) - } - - fun getRegisteredBlockIds(): Set { - return blockConstructors.keys + return create(blockId, location, facing) } } diff --git a/src/main/kotlin/com/coderjoe/atlas/power/PowerBlockFactory.kt b/src/main/kotlin/com/coderjoe/atlas/power/PowerBlockFactory.kt index 7944c40..e8e20bf 100644 --- a/src/main/kotlin/com/coderjoe/atlas/power/PowerBlockFactory.kt +++ b/src/main/kotlin/com/coderjoe/atlas/power/PowerBlockFactory.kt @@ -1,43 +1,12 @@ package com.coderjoe.atlas.power +import com.coderjoe.atlas.core.BlockFactory import org.bukkit.Location import org.bukkit.block.BlockFace -/** - * Factory for creating PowerBlock instances based on block IDs - */ -object PowerBlockFactory { - private val blockConstructors = mutableMapOf PowerBlock>() +object PowerBlockFactory : BlockFactory() { - /** - * Registers a PowerBlock type with its block ID - * @param blockId The Nexo block ID (e.g., "small_solar_panel") - * @param constructor A function that creates the PowerBlock instance given a location and facing direction - */ - fun register(blockId: String, constructor: (Location, BlockFace) -> PowerBlock) { - blockConstructors[blockId] = constructor - println("PowerBlockFactory: Registered block ID '$blockId'") - } - - /** - * Creates a PowerBlock instance for the given block ID, location, and facing direction - * @return PowerBlock instance, or null if the block ID is not registered - */ fun createPowerBlock(blockId: String, location: Location, facing: BlockFace = BlockFace.SELF): PowerBlock? { - return blockConstructors[blockId]?.invoke(location, facing) - } - - /** - * Checks if a block ID is registered as a power block - */ - fun isRegistered(blockId: String): Boolean { - return blockConstructors.containsKey(blockId) - } - - /** - * Gets all registered block IDs - */ - fun getRegisteredBlockIds(): Set { - return blockConstructors.keys + return create(blockId, location, facing) } } diff --git a/src/test/kotlin/com/coderjoe/atlas/TestHelper.kt b/src/test/kotlin/com/coderjoe/atlas/TestHelper.kt index 3ef6f0f..0669d0f 100644 --- a/src/test/kotlin/com/coderjoe/atlas/TestHelper.kt +++ b/src/test/kotlin/com/coderjoe/atlas/TestHelper.kt @@ -127,18 +127,7 @@ object TestHelper { } private fun clearFactories() { - try { - val field = PowerBlockFactory::class.java.getDeclaredField("blockConstructors") - field.isAccessible = true - @Suppress("UNCHECKED_CAST") - (field.get(PowerBlockFactory) as MutableMap<*, *>).clear() - } catch (_: Exception) {} - - try { - val field = FluidBlockFactory::class.java.getDeclaredField("blockConstructors") - field.isAccessible = true - @Suppress("UNCHECKED_CAST") - (field.get(FluidBlockFactory) as MutableMap<*, *>).clear() - } catch (_: Exception) {} + PowerBlockFactory.clear() + FluidBlockFactory.clear() } } From 09b43dfd4d6eccb4436379be0ceefb4d76a84948 Mon Sep 17 00:00:00 2001 From: CoderJoe Date: Thu, 5 Mar 2026 23:52:47 -0600 Subject: [PATCH 3/7] Extract generic BlockPersistence and simplify domain persistence and data classes BlockPersistence handles common location/world/facing serialization. PowerBlockPersistence and FluidBlockPersistence are now thin wrappers providing serialize/restore lambdas for domain-specific fields. Data classes use block.facing property instead of when chains. --- .../coderjoe/atlas/core/BlockPersistence.kt | 109 ++++++++++++++++ .../coderjoe/atlas/fluid/FluidBlockData.kt | 8 +- .../atlas/fluid/FluidBlockPersistence.kt | 117 ++++-------------- .../coderjoe/atlas/power/PowerBlockData.kt | 9 +- .../atlas/power/PowerBlockPersistence.kt | 116 ++++------------- 5 files changed, 159 insertions(+), 200 deletions(-) create mode 100644 src/main/kotlin/com/coderjoe/atlas/core/BlockPersistence.kt diff --git a/src/main/kotlin/com/coderjoe/atlas/core/BlockPersistence.kt b/src/main/kotlin/com/coderjoe/atlas/core/BlockPersistence.kt new file mode 100644 index 0000000..48d23e5 --- /dev/null +++ b/src/main/kotlin/com/coderjoe/atlas/core/BlockPersistence.kt @@ -0,0 +1,109 @@ +package com.coderjoe.atlas.core + +import org.bukkit.Location +import org.bukkit.block.BlockFace +import org.bukkit.configuration.file.YamlConfiguration +import org.bukkit.plugin.java.JavaPlugin +import java.io.File + +class BlockPersistence( + private val plugin: JavaPlugin, + private val fileName: String, + private val yamlKey: String, + private val factory: BlockFactory, + private val serialize: (T, String) -> Map, + private val restore: (T, Map) -> Unit +) { + private val dataFile = File(plugin.dataFolder, fileName) + + fun save(registry: BlockRegistry) { + val config = YamlConfiguration() + val blocksWithIds = registry.getAllBlocksWithIds() + + plugin.logger.info("Saving ${blocksWithIds.size} blocks to $fileName...") + + val blockDataList = mutableListOf>() + + for ((block, blockId) in blocksWithIds) { + val map = mutableMapOf( + "blockId" to blockId, + "world" to (block.location.world?.name ?: "world"), + "x" to block.location.blockX, + "y" to block.location.blockY, + "z" to block.location.blockZ + ) + val facing = block.facing + if (facing != BlockFace.SELF) { + map["facing"] = facing.name + } + map.putAll(serialize(block, blockId)) + blockDataList.add(map) + } + + config.set(yamlKey, blockDataList) + + try { + config.save(dataFile) + plugin.logger.info("Successfully saved ${blockDataList.size} blocks to $fileName") + } catch (e: Exception) { + plugin.logger.severe("Failed to save blocks to $fileName: ${e.message}") + e.printStackTrace() + } + } + + fun load(registry: BlockRegistry) { + if (!dataFile.exists()) { + plugin.logger.info("No $fileName data file found, starting fresh") + return + } + + val config = YamlConfiguration.loadConfiguration(dataFile) + val blockDataList = config.getMapList(yamlKey) + + plugin.logger.info("Loading ${blockDataList.size} blocks from $fileName...") + + var loadedCount = 0 + var failedCount = 0 + + for (blockDataMap in blockDataList) { + try { + val blockId = blockDataMap["blockId"] as? String ?: continue + val worldName = blockDataMap["world"] as? String ?: continue + val x = (blockDataMap["x"] as? Number)?.toInt() ?: continue + val y = (blockDataMap["y"] as? Number)?.toInt() ?: continue + val z = (blockDataMap["z"] as? Number)?.toInt() ?: continue + val facingStr = blockDataMap["facing"] as? String + + val world = plugin.server.getWorld(worldName) + if (world == null) { + plugin.logger.warning("Failed to load block at $worldName $x,$y,$z - world not found") + failedCount++ + continue + } + + val location = Location(world, x.toDouble(), y.toDouble(), z.toDouble()) + val facing = if (facingStr != null) { + try { BlockFace.valueOf(facingStr) } catch (_: Exception) { BlockFace.SELF } + } else { + BlockFace.SELF + } + + val block = factory.create(blockId, location, facing) + if (block != null) { + @Suppress("UNCHECKED_CAST") + restore(block, blockDataMap as Map) + registry.register(block, blockId) + loadedCount++ + } else { + plugin.logger.warning("Failed to create block for ID: $blockId at $x,$y,$z") + failedCount++ + } + } catch (e: Exception) { + plugin.logger.warning("Failed to load block: ${e.message}") + failedCount++ + } + } + + plugin.logger.info("Loaded $loadedCount blocks from $fileName, $failedCount failed") + } +} diff --git a/src/main/kotlin/com/coderjoe/atlas/fluid/FluidBlockData.kt b/src/main/kotlin/com/coderjoe/atlas/fluid/FluidBlockData.kt index 18450e5..3a6d6bc 100644 --- a/src/main/kotlin/com/coderjoe/atlas/fluid/FluidBlockData.kt +++ b/src/main/kotlin/com/coderjoe/atlas/fluid/FluidBlockData.kt @@ -1,8 +1,6 @@ package com.coderjoe.atlas.fluid import com.coderjoe.atlas.fluid.block.FluidContainer -import com.coderjoe.atlas.fluid.block.FluidPipe -import com.coderjoe.atlas.fluid.block.FluidPump import org.bukkit.Location import org.bukkit.block.BlockFace import org.bukkit.plugin.java.JavaPlugin @@ -20,11 +18,7 @@ data class FluidBlockData( companion object { fun fromFluidBlock(fluidBlock: FluidBlock, blockId: String): FluidBlockData { val loc = fluidBlock.location - val facing = when (fluidBlock) { - is FluidPipe -> fluidBlock.facing.name - is FluidContainer -> fluidBlock.facing.name - else -> null - } + val facing = fluidBlock.facing.let { if (it == BlockFace.SELF) null else it.name } val storedAmount = when (fluidBlock) { is FluidContainer -> fluidBlock.storedAmount else -> null diff --git a/src/main/kotlin/com/coderjoe/atlas/fluid/FluidBlockPersistence.kt b/src/main/kotlin/com/coderjoe/atlas/fluid/FluidBlockPersistence.kt index 1dc4695..97f2f48 100644 --- a/src/main/kotlin/com/coderjoe/atlas/fluid/FluidBlockPersistence.kt +++ b/src/main/kotlin/com/coderjoe/atlas/fluid/FluidBlockPersistence.kt @@ -1,105 +1,40 @@ package com.coderjoe.atlas.fluid +import com.coderjoe.atlas.core.BlockPersistence import com.coderjoe.atlas.fluid.block.FluidContainer -import org.bukkit.configuration.file.YamlConfiguration import org.bukkit.plugin.java.JavaPlugin -import java.io.File -class FluidBlockPersistence(private val plugin: JavaPlugin) { - private val dataFile = File(plugin.dataFolder, "fluid_blocks.yml") - - fun save(registry: FluidBlockRegistry) { - val config = YamlConfiguration() - val fluidBlocksWithIds = registry.getAllFluidBlocksWithIds() - - plugin.logger.info("Saving ${fluidBlocksWithIds.size} fluid blocks to disk...") - - val blockDataList = mutableListOf>() - - for ((fluidBlock, blockId) in fluidBlocksWithIds) { - val data = FluidBlockData.fromFluidBlock(fluidBlock, blockId) +class FluidBlockPersistence(plugin: JavaPlugin) { + private val persistence = BlockPersistence( + plugin = plugin, + fileName = "fluid_blocks.yml", + yamlKey = "fluid_blocks", + factory = FluidBlockFactory, + serialize = { block, _ -> val map = mutableMapOf( - "blockId" to data.blockId, - "world" to data.world, - "x" to data.x, - "y" to data.y, - "z" to data.z, - "fluidType" to data.fluidType + "fluidType" to block.storedFluid.name ) - if (data.facing != null) { - map["facing"] = data.facing - } - if (data.storedAmount != null) { - map["storedAmount"] = data.storedAmount + if (block is FluidContainer) { + map["storedAmount"] = block.storedAmount } - blockDataList.add(map) - } - - config.set("fluid_blocks", blockDataList) - - try { - config.save(dataFile) - plugin.logger.info("Successfully saved ${blockDataList.size} fluid blocks") - } catch (e: Exception) { - plugin.logger.severe("Failed to save fluid blocks: ${e.message}") - e.printStackTrace() - } - } - - fun load(registry: FluidBlockRegistry) { - if (!dataFile.exists()) { - plugin.logger.info("No fluid blocks data file found, starting fresh") - return - } - - val config = YamlConfiguration.loadConfiguration(dataFile) - val blockDataList = config.getMapList("fluid_blocks") - - plugin.logger.info("Loading ${blockDataList.size} fluid blocks from disk...") - - var loadedCount = 0 - var failedCount = 0 - - for (blockDataMap in blockDataList) { - try { - val blockId = blockDataMap["blockId"] as? String ?: continue - val world = blockDataMap["world"] as? String ?: continue - val x = (blockDataMap["x"] as? Number)?.toInt() ?: continue - val y = (blockDataMap["y"] as? Number)?.toInt() ?: continue - val z = (blockDataMap["z"] as? Number)?.toInt() ?: continue - val fluidType = blockDataMap["fluidType"] as? String ?: "NONE" - val facing = blockDataMap["facing"] as? String - val storedAmount = (blockDataMap["storedAmount"] as? Number)?.toInt() - - val data = FluidBlockData(blockId, world, x, y, z, fluidType, facing, storedAmount) - val location = data.toLocation(plugin) - - if (location == null) { - plugin.logger.warning("Failed to load fluid block at $world $x,$y,$z - world not found") - failedCount++ - continue - } - - val fluidBlock = FluidBlockFactory.createFluidBlock(blockId, location, data.toBlockFace()) - - if (fluidBlock != null) { - if (fluidBlock is FluidContainer && data.storedAmount != null) { - fluidBlock.restoreState(data.toFluidType(), data.storedAmount) - } else { - fluidBlock.storedFluid = data.toFluidType() - } - registry.registerFluidBlock(fluidBlock, blockId) - loadedCount++ + map + }, + restore = { block, data -> + val fluidTypeName = data["fluidType"] as? String ?: "NONE" + val fluidType = try { FluidType.valueOf(fluidTypeName) } catch (_: Exception) { FluidType.NONE } + if (block is FluidContainer) { + val storedAmount = (data["storedAmount"] as? Number)?.toInt() + if (storedAmount != null) { + block.restoreState(fluidType, storedAmount) } else { - plugin.logger.warning("Failed to create fluid block for ID: $blockId at $x,$y,$z") - failedCount++ + block.storedFluid = fluidType } - } catch (e: Exception) { - plugin.logger.warning("Failed to load fluid block: ${e.message}") - failedCount++ + } else { + block.storedFluid = fluidType } } + ) - plugin.logger.info("Loaded $loadedCount fluid blocks, $failedCount failed") - } + fun save(registry: FluidBlockRegistry) = persistence.save(registry) + fun load(registry: FluidBlockRegistry) = persistence.load(registry) } diff --git a/src/main/kotlin/com/coderjoe/atlas/power/PowerBlockData.kt b/src/main/kotlin/com/coderjoe/atlas/power/PowerBlockData.kt index ebdb0da..ff79f0d 100644 --- a/src/main/kotlin/com/coderjoe/atlas/power/PowerBlockData.kt +++ b/src/main/kotlin/com/coderjoe/atlas/power/PowerBlockData.kt @@ -1,7 +1,5 @@ package com.coderjoe.atlas.power -import com.coderjoe.atlas.power.block.PowerCable -import com.coderjoe.atlas.power.block.SmallBattery import com.coderjoe.atlas.power.block.SmallDrill import org.bukkit.Location import org.bukkit.block.BlockFace @@ -19,12 +17,7 @@ data class PowerBlockData( companion object { fun fromPowerBlock(powerBlock: PowerBlock, blockId: String): PowerBlockData { val loc = powerBlock.location - val facing = when (powerBlock) { - is PowerCable -> powerBlock.facing.name - is SmallDrill -> powerBlock.miningDirection.name - is SmallBattery -> powerBlock.facing.name - else -> null - } + val facing = powerBlock.facing.let { if (it == BlockFace.SELF) null else it.name } val enabled = if (powerBlock is SmallDrill) powerBlock.enabled else null return PowerBlockData( blockId = blockId, diff --git a/src/main/kotlin/com/coderjoe/atlas/power/PowerBlockPersistence.kt b/src/main/kotlin/com/coderjoe/atlas/power/PowerBlockPersistence.kt index cf7ec6a..edfa771 100644 --- a/src/main/kotlin/com/coderjoe/atlas/power/PowerBlockPersistence.kt +++ b/src/main/kotlin/com/coderjoe/atlas/power/PowerBlockPersistence.kt @@ -1,107 +1,35 @@ package com.coderjoe.atlas.power +import com.coderjoe.atlas.core.BlockPersistence import com.coderjoe.atlas.power.block.SmallDrill -import org.bukkit.configuration.file.YamlConfiguration import org.bukkit.plugin.java.JavaPlugin -import java.io.File -/** - * Handles saving and loading PowerBlock data to/from disk - */ -class PowerBlockPersistence(private val plugin: JavaPlugin) { - private val dataFile = File(plugin.dataFolder, "power_blocks.yml") - - fun save(registry: PowerBlockRegistry) { - val config = YamlConfiguration() - val powerBlocksWithIds = registry.getAllPowerBlocksWithIds() - - plugin.logger.info("Saving ${powerBlocksWithIds.size} power blocks to disk...") - - val blockDataList = mutableListOf>() - - for ((powerBlock, blockId) in powerBlocksWithIds) { - val data = PowerBlockData.fromPowerBlock(powerBlock, blockId) +class PowerBlockPersistence(plugin: JavaPlugin) { + private val persistence = BlockPersistence( + plugin = plugin, + fileName = "power_blocks.yml", + yamlKey = "power_blocks", + factory = PowerBlockFactory, + serialize = { block, _ -> val map = mutableMapOf( - "blockId" to data.blockId, - "world" to data.world, - "x" to data.x, - "y" to data.y, - "z" to data.z, - "currentPower" to data.currentPower + "currentPower" to block.currentPower ) - if (data.facing != null) { - map["facing"] = data.facing - } - if (data.enabled != null) { - map["enabled"] = data.enabled + if (block is SmallDrill) { + map["enabled"] = block.enabled } - blockDataList.add(map) - } - - config.set("power_blocks", blockDataList) - - try { - config.save(dataFile) - plugin.logger.info("Successfully saved ${blockDataList.size} power blocks") - } catch (e: Exception) { - plugin.logger.severe("Failed to save power blocks: ${e.message}") - e.printStackTrace() - } - } - - fun load(registry: PowerBlockRegistry) { - if (!dataFile.exists()) { - plugin.logger.info("No power blocks data file found, starting fresh") - return - } - - val config = YamlConfiguration.loadConfiguration(dataFile) - val blockDataList = config.getMapList("power_blocks") - - plugin.logger.info("Loading ${blockDataList.size} power blocks from disk...") - - var loadedCount = 0 - var failedCount = 0 - - for (blockDataMap in blockDataList) { - try { - val blockId = blockDataMap["blockId"] as? String ?: continue - val world = blockDataMap["world"] as? String ?: continue - val x = (blockDataMap["x"] as? Number)?.toInt() ?: continue - val y = (blockDataMap["y"] as? Number)?.toInt() ?: continue - val z = (blockDataMap["z"] as? Number)?.toInt() ?: continue - val currentPower = (blockDataMap["currentPower"] as? Number)?.toInt() ?: 0 - val facing = blockDataMap["facing"] as? String - val enabled = blockDataMap["enabled"] as? Boolean - - val data = PowerBlockData(blockId, world, x, y, z, currentPower, facing, enabled) - val location = data.toLocation(plugin) - - if (location == null) { - plugin.logger.warning("Failed to load power block at $world $x,$y,$z - world not found") - failedCount++ - continue - } - - val powerBlock = PowerBlockFactory.createPowerBlock(blockId, location, data.toBlockFace()) - - if (powerBlock != null) { - powerBlock.currentPower = currentPower - if (powerBlock is SmallDrill && data.enabled != null) { - powerBlock.enabled = data.enabled - } - registry.registerPowerBlock(powerBlock, blockId) - loadedCount++ - } else { - plugin.logger.warning("Failed to create power block for ID: $blockId at $x,$y,$z") - failedCount++ + map + }, + restore = { block, data -> + block.currentPower = (data["currentPower"] as? Number)?.toInt() ?: 0 + if (block is SmallDrill) { + val enabled = data["enabled"] as? Boolean + if (enabled != null) { + block.enabled = enabled } - } catch (e: Exception) { - plugin.logger.warning("Failed to load power block: ${e.message}") - failedCount++ } } + ) - plugin.logger.info("Loaded $loadedCount power blocks, $failedCount failed") - } + fun save(registry: PowerBlockRegistry) = persistence.save(registry) + fun load(registry: PowerBlockRegistry) = persistence.load(registry) } From 7987aab74609140675e30ed69de739c0883756de Mon Sep 17 00:00:00 2001 From: CoderJoe Date: Thu, 5 Mar 2026 23:59:24 -0600 Subject: [PATCH 4/7] Replace duplicate PowerBlockListener and FluidBlockListener with unified AtlasBlockListener Add BlockSystem to bundle registry, factory, descriptors, and dialog handler per system. AtlasBlockListener iterates systems generically for placement, break, and interact events. Delete PowerBlockListener and FluidBlockListener. Update Atlas.kt to register a single unified listener. Rewrite listener tests to target AtlasBlockListener. --- src/main/kotlin/com/coderjoe/atlas/Atlas.kt | 53 ++++- .../coderjoe/atlas/core/AtlasBlockListener.kt | 167 ++++++++++++++ .../com/coderjoe/atlas/core/BlockSystem.kt | 19 ++ .../atlas/fluid/FluidBlockListener.kt | 180 --------------- .../atlas/power/PowerBlockListener.kt | 207 ------------------ .../atlas/fluid/FluidBlockListenerTest.kt | 46 ++-- .../atlas/power/PowerBlockListenerTest.kt | 57 ++--- 7 files changed, 267 insertions(+), 462 deletions(-) create mode 100644 src/main/kotlin/com/coderjoe/atlas/core/AtlasBlockListener.kt create mode 100644 src/main/kotlin/com/coderjoe/atlas/core/BlockSystem.kt delete mode 100644 src/main/kotlin/com/coderjoe/atlas/fluid/FluidBlockListener.kt delete mode 100644 src/main/kotlin/com/coderjoe/atlas/power/PowerBlockListener.kt diff --git a/src/main/kotlin/com/coderjoe/atlas/Atlas.kt b/src/main/kotlin/com/coderjoe/atlas/Atlas.kt index 41cae52..3ae7be2 100644 --- a/src/main/kotlin/com/coderjoe/atlas/Atlas.kt +++ b/src/main/kotlin/com/coderjoe/atlas/Atlas.kt @@ -1,13 +1,15 @@ package com.coderjoe.atlas +import com.coderjoe.atlas.core.AtlasBlockListener +import com.coderjoe.atlas.core.BlockSystem import com.coderjoe.atlas.fluid.FluidBlockDialog +import com.coderjoe.atlas.fluid.FluidBlockFactory import com.coderjoe.atlas.fluid.FluidBlockInitializer -import com.coderjoe.atlas.fluid.FluidBlockListener import com.coderjoe.atlas.fluid.FluidBlockPersistence import com.coderjoe.atlas.fluid.FluidBlockRegistry import com.coderjoe.atlas.power.PowerBlockDialog +import com.coderjoe.atlas.power.PowerBlockFactory import com.coderjoe.atlas.power.PowerBlockInitializer -import com.coderjoe.atlas.power.PowerBlockListener import com.coderjoe.atlas.power.PowerBlockPersistence import com.coderjoe.atlas.power.PowerBlockRegistry import org.bukkit.plugin.java.JavaPlugin @@ -44,6 +46,32 @@ class Atlas : JavaPlugin() { initPowerSystem() initFluidSystem() + // Register unified listener + val powerSystem = BlockSystem( + name = "power", + registry = powerBlockRegistry, + factory = PowerBlockFactory, + descriptors = powerDescriptors(), + showDialog = { player, block -> + PowerBlockDialog.showPowerDialog(player, block as com.coderjoe.atlas.power.PowerBlock) + } + ) + + val fluidSystem = BlockSystem( + name = "fluid", + registry = fluidBlockRegistry, + factory = FluidBlockFactory, + descriptors = fluidDescriptors(), + showDialog = { player, block -> + FluidBlockDialog.showFluidDialog(player, block as com.coderjoe.atlas.fluid.FluidBlock) + } + ) + + server.pluginManager.registerEvents( + AtlasBlockListener(this, listOf(powerSystem, fluidSystem)), + this + ) + // Auto-save every 5 minutes (6000 ticks) autoSaveTask = server.scheduler.runTaskTimer(this, Runnable { powerBlockPersistence.save(powerBlockRegistry) @@ -84,8 +112,6 @@ class Atlas : JavaPlugin() { powerBlockPersistence = PowerBlockPersistence(this) powerBlockPersistence.load(powerBlockRegistry) - server.pluginManager.registerEvents(PowerBlockListener(this, powerBlockRegistry), this) - logger.info("Power system initialized") } @@ -95,8 +121,23 @@ class Atlas : JavaPlugin() { fluidBlockPersistence = FluidBlockPersistence(this) fluidBlockPersistence.load(fluidBlockRegistry) - server.pluginManager.registerEvents(FluidBlockListener(this, fluidBlockRegistry), this) - logger.info("Fluid system initialized") } + + private fun powerDescriptors(): Map { + return listOf( + com.coderjoe.atlas.power.block.SmallSolarPanel.descriptor, + com.coderjoe.atlas.power.block.SmallDrill.descriptor, + com.coderjoe.atlas.power.block.SmallBattery.descriptor, + com.coderjoe.atlas.power.block.PowerCable.descriptor + ).associateBy { it.baseBlockId } + } + + private fun fluidDescriptors(): Map { + return listOf( + com.coderjoe.atlas.fluid.block.FluidPump.descriptor, + com.coderjoe.atlas.fluid.block.FluidPipe.descriptor, + com.coderjoe.atlas.fluid.block.FluidContainer.descriptor + ).associateBy { it.baseBlockId } + } } diff --git a/src/main/kotlin/com/coderjoe/atlas/core/AtlasBlockListener.kt b/src/main/kotlin/com/coderjoe/atlas/core/AtlasBlockListener.kt new file mode 100644 index 0000000..9eabb5f --- /dev/null +++ b/src/main/kotlin/com/coderjoe/atlas/core/AtlasBlockListener.kt @@ -0,0 +1,167 @@ +package com.coderjoe.atlas.core + +import com.nexomc.nexo.api.NexoBlocks +import com.nexomc.nexo.api.NexoItems +import org.bukkit.Material +import org.bukkit.block.BlockFace +import org.bukkit.event.EventHandler +import org.bukkit.event.Listener +import org.bukkit.event.block.Action +import org.bukkit.event.block.BlockBreakEvent +import org.bukkit.event.block.BlockPlaceEvent +import org.bukkit.event.player.PlayerInteractEvent +import org.bukkit.plugin.java.JavaPlugin + +class AtlasBlockListener( + private val plugin: JavaPlugin, + private val systems: List> +) : Listener { + + @EventHandler + fun onBlockPlace(event: BlockPlaceEvent) { + val location = event.block.location + val key = BlockRegistry.locationKey(location) + + if (systems.any { it.registry.updatingLocations.contains(key) }) return + + val mechanic = NexoBlocks.customBlockMechanic(event.block) ?: return + val blockId = mechanic.itemID + + for (system in systems) { + val baseDescriptor = system.findDescriptorByBaseId(blockId) + if (baseDescriptor != null) { + handlePlacement(event, system, baseDescriptor, blockId) + return + } + + val descriptor = system.findDescriptorForBlockId(blockId) + if (descriptor != null) { + val facing = resolveFacingFromVariant(descriptor, blockId) + createAndRegister(system, blockId, event.block.location, facing) + return + } + } + } + + private fun handlePlacement( + event: BlockPlaceEvent, + system: BlockSystem<*>, + descriptor: BlockDescriptor, + blockId: String + ) { + when (descriptor.placementType) { + PlacementType.SIMPLE -> { + val location = event.block.location.clone() + val facing = if (descriptor.directionalVariants.isEmpty()) { + getPlayerFacing(event) + } else { + BlockFace.SELF + } + createAndRegister(system, blockId, location, facing) + } + PlacementType.DIRECTIONAL -> { + val facing = getPlayerFacing(event) + val variantId = descriptor.directionalVariants[facing] ?: return + val location = event.block.location.clone() + plugin.server.scheduler.runTask(plugin, Runnable { + location.block.setType(Material.AIR, false) + NexoBlocks.place(variantId, location) + createAndRegister(system, variantId, location, facing) + }) + } + PlacementType.DIRECTIONAL_OPPOSITE -> { + val facing = getPlayerFacing(event).oppositeFace + val variantId = descriptor.directionalVariants[facing] ?: blockId + val location = event.block.location.clone() + plugin.server.scheduler.runTask(plugin, Runnable { + location.block.setType(Material.AIR, false) + NexoBlocks.place(variantId, location) + createAndRegister(system, variantId, location, facing) + }) + } + } + } + + @Suppress("UNCHECKED_CAST") + private fun createAndRegister( + system: BlockSystem<*>, + blockId: String, + location: org.bukkit.Location, + facing: BlockFace + ) { + val factory = system.factory as BlockFactory + val registry = system.registry as BlockRegistry + val block = factory.create(blockId, location, facing) + if (block != null) { + registry.register(block, blockId) + } + } + + @EventHandler + fun onBlockBreak(event: BlockBreakEvent) { + val location = event.block.location + val key = BlockRegistry.locationKey(location) + + if (systems.any { it.registry.updatingLocations.contains(key) }) return + + for (system in systems) { + val block = system.registry.unregister(location) + if (block != null) { + val baseItemId = block.baseBlockId.ifEmpty { null } + if (baseItemId != null) { + val itemBuilder = NexoItems.itemFromId(baseItemId) + if (itemBuilder != null) { + val dropLocation = event.block.location.add(0.5, 0.5, 0.5) + event.block.world.dropItemNaturally(dropLocation, itemBuilder.build()) + event.isDropItems = false + } + } + return + } + } + } + + @EventHandler + fun onPlayerInteract(event: PlayerInteractEvent) { + if (event.action != Action.RIGHT_CLICK_BLOCK) return + if (event.player.isSneaking) return + val clickedBlock = event.clickedBlock ?: return + val location = clickedBlock.location + + for (system in systems) { + val block = system.registry.getBlock(location) + if (block != null) { + system.showDialog(event.player, block) + event.isCancelled = true + return + } + } + } + + private fun resolveFacingFromVariant(descriptor: BlockDescriptor, blockId: String): BlockFace { + for ((face, id) in descriptor.directionalVariants) { + if (id == blockId) return face + } + return BlockFace.SELF + } + + companion object { + fun getPlayerFacing(event: BlockPlaceEvent): BlockFace { + val against = event.blockAgainst.location + val placed = event.block.location + val dx = placed.blockX - against.blockX + val dy = placed.blockY - against.blockY + val dz = placed.blockZ - against.blockZ + + return when { + dy > 0 -> BlockFace.UP + dy < 0 -> BlockFace.DOWN + dx > 0 -> BlockFace.EAST + dx < 0 -> BlockFace.WEST + dz > 0 -> BlockFace.SOUTH + dz < 0 -> BlockFace.NORTH + else -> event.player.facing + } + } + } +} diff --git a/src/main/kotlin/com/coderjoe/atlas/core/BlockSystem.kt b/src/main/kotlin/com/coderjoe/atlas/core/BlockSystem.kt new file mode 100644 index 0000000..22cc42f --- /dev/null +++ b/src/main/kotlin/com/coderjoe/atlas/core/BlockSystem.kt @@ -0,0 +1,19 @@ +package com.coderjoe.atlas.core + +import org.bukkit.entity.Player + +class BlockSystem( + val name: String, + val registry: BlockRegistry, + val factory: BlockFactory, + val descriptors: Map, + val showDialog: (Player, AtlasBlock) -> Unit +) { + fun findDescriptorForBlockId(blockId: String): BlockDescriptor? { + return descriptors.values.find { blockId in it.allRegistrableIds } + } + + fun findDescriptorByBaseId(blockId: String): BlockDescriptor? { + return descriptors[blockId] + } +} diff --git a/src/main/kotlin/com/coderjoe/atlas/fluid/FluidBlockListener.kt b/src/main/kotlin/com/coderjoe/atlas/fluid/FluidBlockListener.kt deleted file mode 100644 index c80026d..0000000 --- a/src/main/kotlin/com/coderjoe/atlas/fluid/FluidBlockListener.kt +++ /dev/null @@ -1,180 +0,0 @@ -package com.coderjoe.atlas.fluid - -import com.coderjoe.atlas.fluid.block.FluidContainer -import com.coderjoe.atlas.fluid.block.FluidPipe -import com.coderjoe.atlas.fluid.block.FluidPump -import com.nexomc.nexo.api.NexoBlocks -import com.nexomc.nexo.api.NexoItems -import org.bukkit.Material -import org.bukkit.block.BlockFace -import org.bukkit.event.EventHandler -import org.bukkit.event.Listener -import org.bukkit.event.block.Action -import org.bukkit.event.block.BlockBreakEvent -import org.bukkit.event.block.BlockPlaceEvent -import org.bukkit.event.player.PlayerInteractEvent -import org.bukkit.plugin.java.JavaPlugin - -class FluidBlockListener( - private val plugin: JavaPlugin, - private val registry: FluidBlockRegistry -) : Listener { - - @EventHandler - fun onBlockPlace(event: BlockPlaceEvent) { - val key = FluidBlockRegistry.locationKey(event.block.location) - if (registry.updatingLocations.contains(key)) return - - val mechanic = NexoBlocks.customBlockMechanic(event.block) ?: return - val blockId = mechanic.itemID - - // Handle fluid_pump placement - if (blockId == FluidPump.BLOCK_ID) { - val location = event.block.location.clone() - val fluidBlock = FluidBlockFactory.createFluidBlock(blockId, location) - if (fluidBlock != null) { - registry.registerFluidBlock(fluidBlock, blockId) - } else { - plugin.logger.warning("Failed to create fluid block for: $blockId") - } - return - } - - // Handle fluid_container base item: swap to directional variant - if (blockId == FluidContainer.BLOCK_ID) { - val facing = getPlayerFacing(event) - val variantId = FluidContainer.DIRECTIONAL_IDS[facing] - if (variantId == null) { - plugin.logger.warning("No directional variant for facing $facing") - return - } - - plugin.logger.info("Swapping fluid_container to directional variant: $variantId (facing $facing)") - - val location = event.block.location.clone() - plugin.server.scheduler.runTask(plugin, Runnable { - location.block.setType(Material.AIR, false) - NexoBlocks.place(variantId, location) - - val fluidBlock = FluidBlockFactory.createFluidBlock(variantId, location, facing) - if (fluidBlock != null) { - registry.registerFluidBlock(fluidBlock, variantId) - } else { - plugin.logger.warning("Failed to create fluid block for variant: $variantId") - } - }) - return - } - - // Handle fluid_pipe base item: swap to directional variant - if (blockId == FluidPipe.BLOCK_ID) { - val facing = getPlayerFacing(event) - val variantId = FluidPipe.DIRECTIONAL_IDS[facing] - if (variantId == null) { - plugin.logger.warning("No directional variant for facing $facing") - return - } - - plugin.logger.info("Swapping fluid_pipe to directional variant: $variantId (facing $facing)") - - val location = event.block.location.clone() - plugin.server.scheduler.runTask(plugin, Runnable { - location.block.setType(Material.AIR, false) - NexoBlocks.place(variantId, location) - - val fluidBlock = FluidBlockFactory.createFluidBlock(variantId, location, facing) - if (fluidBlock != null) { - registry.registerFluidBlock(fluidBlock, variantId) - } else { - plugin.logger.warning("Failed to create fluid block for variant: $variantId") - } - }) - return - } - - // Handle directional variant placed directly (pipe) - val pipeFacing = FluidPipe.facingFromBlockId(blockId) - if (pipeFacing != null) { - plugin.logger.info("Directional fluid pipe placed: $blockId (facing $pipeFacing)") - val fluidBlock = FluidBlockFactory.createFluidBlock(blockId, event.block.location, pipeFacing) - if (fluidBlock != null) { - registry.registerFluidBlock(fluidBlock, blockId) - } - return - } - - // Handle directional variant placed directly (container) - val containerFacing = FluidContainer.facingFromBlockId(blockId) - if (containerFacing != null) { - plugin.logger.info("Directional fluid container placed: $blockId (facing $containerFacing)") - val fluidBlock = FluidBlockFactory.createFluidBlock(blockId, event.block.location, containerFacing) - if (fluidBlock != null) { - registry.registerFluidBlock(fluidBlock, blockId) - } - return - } - - // Handle any other registered fluid block - if (FluidBlockFactory.isRegistered(blockId)) { - val fluidBlock = FluidBlockFactory.createFluidBlock(blockId, event.block.location.clone()) - if (fluidBlock != null) { - registry.registerFluidBlock(fluidBlock, blockId) - } - } - } - - @EventHandler - fun onBlockBreak(event: BlockBreakEvent) { - val key = FluidBlockRegistry.locationKey(event.block.location) - if (registry.updatingLocations.contains(key)) return - - val fluidBlock = registry.unregisterFluidBlock(event.block.location) ?: return - - plugin.logger.info("Fluid block removed: ${fluidBlock::class.simpleName} at ${event.block.location}") - - val baseItemId = when (fluidBlock) { - is FluidPump -> FluidPump.BLOCK_ID - is FluidPipe -> FluidPipe.BLOCK_ID - is FluidContainer -> FluidContainer.BLOCK_ID - else -> null - } - - if (baseItemId != null) { - val itemBuilder = NexoItems.itemFromId(baseItemId) - if (itemBuilder != null) { - val dropLocation = event.block.location.add(0.5, 0.5, 0.5) - event.block.world.dropItemNaturally(dropLocation, itemBuilder.build()) - event.isDropItems = false - } - } - } - - @EventHandler - fun onPlayerInteract(event: PlayerInteractEvent) { - if (event.action != Action.RIGHT_CLICK_BLOCK) return - if (event.player.isSneaking) return - val block = event.clickedBlock ?: return - val fluidBlock = registry.getFluidBlock(block.location) ?: return - - FluidBlockDialog.showFluidDialog(event.player, fluidBlock) - event.isCancelled = true - } - - private fun getPlayerFacing(event: BlockPlaceEvent): BlockFace { - val against = event.blockAgainst.location - val placed = event.block.location - val dx = placed.blockX - against.blockX - val dy = placed.blockY - against.blockY - val dz = placed.blockZ - against.blockZ - - return when { - dy > 0 -> BlockFace.UP - dy < 0 -> BlockFace.DOWN - dx > 0 -> BlockFace.EAST - dx < 0 -> BlockFace.WEST - dz > 0 -> BlockFace.SOUTH - dz < 0 -> BlockFace.NORTH - else -> event.player.facing - } - } -} diff --git a/src/main/kotlin/com/coderjoe/atlas/power/PowerBlockListener.kt b/src/main/kotlin/com/coderjoe/atlas/power/PowerBlockListener.kt deleted file mode 100644 index 810e23d..0000000 --- a/src/main/kotlin/com/coderjoe/atlas/power/PowerBlockListener.kt +++ /dev/null @@ -1,207 +0,0 @@ -package com.coderjoe.atlas.power - -import com.coderjoe.atlas.power.block.PowerCable -import com.coderjoe.atlas.power.block.SmallBattery -import com.coderjoe.atlas.power.block.SmallDrill -import com.coderjoe.atlas.power.block.SmallSolarPanel -import com.nexomc.nexo.api.NexoBlocks -import com.nexomc.nexo.api.NexoItems -import org.bukkit.Material -import org.bukkit.block.BlockFace -import org.bukkit.event.EventHandler -import org.bukkit.event.Listener -import org.bukkit.event.block.Action -import org.bukkit.event.block.BlockBreakEvent -import org.bukkit.event.block.BlockPlaceEvent -import org.bukkit.event.player.PlayerInteractEvent -import org.bukkit.plugin.java.JavaPlugin - -/** - * Listens for block placement and breaking to manage PowerBlock lifecycle - * Automatically detects and registers any block registered with PowerBlockFactory - */ -class PowerBlockListener( - private val plugin: JavaPlugin, - private val registry: PowerBlockRegistry -) : Listener { - - @EventHandler - fun onBlockPlace(event: BlockPlaceEvent) { - val key = PowerBlockRegistry.locationKey(event.block.location) - if (registry.updatingLocations.contains(key)) return - - plugin.logger.info("Block placed event triggered at ${event.block.location}") - - val mechanic = NexoBlocks.customBlockMechanic(event.block) - if (mechanic == null) { - plugin.logger.info("Not a Nexo custom block") - return - } - - val blockId = mechanic.itemID - plugin.logger.info("Nexo block placed with ID: $blockId") - - // Handle small_battery base item: determine facing for pull direction - if (blockId == SmallBattery.BLOCK_ID) { - val facing = getPlayerFacing(event) - plugin.logger.info("SmallBattery placed facing $facing") - - val location = event.block.location.clone() - val powerBlock = PowerBlockFactory.createPowerBlock(blockId, location, facing) - if (powerBlock != null) { - registry.registerPowerBlock(powerBlock, blockId) - } else { - plugin.logger.warning("Failed to create power block for: $blockId") - } - return - } - - // Handle power_cable base item: swap to directional variant - if (blockId == PowerCable.BLOCK_ID) { - val facing = getPlayerFacing(event) - val variantId = PowerCable.DIRECTIONAL_IDS[facing] - if (variantId == null) { - plugin.logger.warning("No directional variant for facing $facing") - return - } - - plugin.logger.info("Swapping power_cable to directional variant: $variantId (facing $facing)") - - // Schedule the swap for next tick so it doesn't conflict with the place event - val location = event.block.location.clone() - plugin.server.scheduler.runTask(plugin, Runnable { - // Set to air without using NexoBlocks.remove() to prevent item drops - location.block.setType(Material.AIR, false) - NexoBlocks.place(variantId, location) - - val powerBlock = PowerBlockFactory.createPowerBlock(variantId, location, facing) - if (powerBlock != null) { - registry.registerPowerBlock(powerBlock, variantId) - } else { - plugin.logger.warning("Failed to create power block for variant: $variantId") - } - }) - return - } - - // Handle small_drill: direction based on placement face, swap to directional variant - if (blockId == SmallDrill.BLOCK_ID) { - val facing = getPlayerFacing(event).oppositeFace - val variantId = SmallDrill.DIRECTIONAL_IDS[facing] ?: SmallDrill.BLOCK_ID - plugin.logger.info("SmallDrill placed, mining direction: $facing, variant: $variantId") - - val location = event.block.location.clone() - plugin.server.scheduler.runTask(plugin, Runnable { - location.block.setType(Material.AIR, false) - NexoBlocks.place(variantId, location) - - val powerBlock = PowerBlockFactory.createPowerBlock(variantId, location, facing) - if (powerBlock != null) { - registry.registerPowerBlock(powerBlock, variantId) - } else { - plugin.logger.warning("Failed to create power block for variant: $variantId") - } - }) - return - } - - // Handle directional variant placed directly (e.g., from Nexo) - val facing = PowerCable.facingFromBlockId(blockId) - if (facing != null) { - plugin.logger.info("Directional power cable placed: $blockId (facing $facing)") - val powerBlock = PowerBlockFactory.createPowerBlock(blockId, event.block.location, facing) - if (powerBlock != null) { - registry.registerPowerBlock(powerBlock, blockId) - } else { - plugin.logger.warning("Failed to create power block for ID: $blockId") - } - return - } - - plugin.logger.info("Registered power blocks: ${PowerBlockFactory.getRegisteredBlockIds()}") - - if (PowerBlockFactory.isRegistered(blockId)) { - plugin.logger.info("Power block placed: $blockId at ${event.block.location}") - - val powerBlock = PowerBlockFactory.createPowerBlock(blockId, event.block.location.clone()) - - if (powerBlock != null) { - registry.registerPowerBlock(powerBlock, blockId) - } else { - plugin.logger.warning("Failed to create power block for ID: $blockId") - } - } else { - plugin.logger.info("Block ID '$blockId' is not registered as a power block") - } - } - - @EventHandler - fun onBlockBreak(event: BlockBreakEvent) { - val key = PowerBlockRegistry.locationKey(event.block.location) - if (registry.updatingLocations.contains(key)) return - - val mechanic = NexoBlocks.customBlockMechanic(event.block) - - if (mechanic != null) { - plugin.logger.info("Block broken: ${mechanic.itemID} at ${event.block.location}") - } - - val powerBlock = registry.unregisterPowerBlock(event.block.location) - - if (powerBlock != null) { - plugin.logger.info("Power block removed with ${powerBlock.currentPower}/${powerBlock.maxStorage} power") - - // Manually drop the base item for visual-state variants - // since Nexo may not handle drops for programmatically-placed blocks - val baseItemId = when (powerBlock) { - is SmallBattery -> SmallBattery.BLOCK_ID - is PowerCable -> PowerCable.BLOCK_ID - is SmallDrill -> SmallDrill.BLOCK_ID - is SmallSolarPanel -> SmallSolarPanel.BLOCK_ID - else -> null - } - - if (baseItemId != null) { - val itemBuilder = NexoItems.itemFromId(baseItemId) - if (itemBuilder != null) { - val dropLocation = event.block.location.add(0.5, 0.5, 0.5) - event.block.world.dropItemNaturally(dropLocation, itemBuilder.build()) - event.isDropItems = false // prevent any default drops - } else { - plugin.logger.warning("Could not find Nexo item for $baseItemId") - } - } - } - } - - @EventHandler - fun onPlayerInteract(event: PlayerInteractEvent) { - if (event.action != Action.RIGHT_CLICK_BLOCK) return - if (event.player.isSneaking) return // allow placing blocks against power blocks - val block = event.clickedBlock ?: return - val powerBlock = registry.getPowerBlock(block.location) ?: return - - PowerBlockDialog.showPowerDialog(event.player, powerBlock) - event.isCancelled = true - } - - private fun getPlayerFacing(event: BlockPlaceEvent): BlockFace { - // Use the face the player clicked on — the cable faces away from the surface - // e.g., clicking the top of a block places a cable facing UP - val against = event.blockAgainst.location - val placed = event.block.location - val dx = placed.blockX - against.blockX - val dy = placed.blockY - against.blockY - val dz = placed.blockZ - against.blockZ - - return when { - dy > 0 -> BlockFace.UP - dy < 0 -> BlockFace.DOWN - dx > 0 -> BlockFace.EAST - dx < 0 -> BlockFace.WEST - dz > 0 -> BlockFace.SOUTH - dz < 0 -> BlockFace.NORTH - else -> event.player.facing - } - } -} diff --git a/src/test/kotlin/com/coderjoe/atlas/fluid/FluidBlockListenerTest.kt b/src/test/kotlin/com/coderjoe/atlas/fluid/FluidBlockListenerTest.kt index 8318f9c..785c848 100644 --- a/src/test/kotlin/com/coderjoe/atlas/fluid/FluidBlockListenerTest.kt +++ b/src/test/kotlin/com/coderjoe/atlas/fluid/FluidBlockListenerTest.kt @@ -1,8 +1,9 @@ package com.coderjoe.atlas.fluid import com.coderjoe.atlas.TestHelper +import com.coderjoe.atlas.core.AtlasBlockListener +import com.coderjoe.atlas.core.BlockSystem import com.coderjoe.atlas.fluid.block.FluidPump -import com.nexomc.nexo.api.NexoBlocks import io.mockk.* import org.bukkit.block.Block import org.bukkit.block.BlockFace @@ -17,13 +18,20 @@ import org.junit.jupiter.api.Assertions.* class FluidBlockListenerTest { private lateinit var registry: FluidBlockRegistry - private lateinit var listener: FluidBlockListener + private lateinit var listener: AtlasBlockListener @BeforeEach fun setup() { TestHelper.setup() registry = FluidBlockRegistry(TestHelper.mockPlugin) - listener = FluidBlockListener(TestHelper.mockPlugin, registry) + val system = BlockSystem( + name = "fluid", + registry = registry, + factory = FluidBlockFactory, + descriptors = emptyMap(), + showDialog = { _, _ -> } + ) + listener = AtlasBlockListener(TestHelper.mockPlugin, listOf(system)) } @AfterEach @@ -72,8 +80,6 @@ class FluidBlockListenerTest { val event = mockk(relaxed = true) every { event.block } returns block - // NexoItems.itemFromId() will throw NoClassDefFoundError in test env - // but the block should still be unregistered before that call try { listener.onBlockBreak(event) } catch (_: NoClassDefFoundError) {} @@ -131,9 +137,6 @@ class FluidBlockListenerTest { @Test fun `getPlayerFacing returns UP when dy positive`() { - val method = FluidBlockListener::class.java.getDeclaredMethod("getPlayerFacing", BlockPlaceEvent::class.java) - method.isAccessible = true - val placed = mockk(relaxed = true) val against = mockk(relaxed = true) every { placed.location } returns TestHelper.createLocation(0.0, 65.0, 0.0) @@ -143,14 +146,11 @@ class FluidBlockListenerTest { every { event.block } returns placed every { event.blockAgainst } returns against - assertEquals(BlockFace.UP, method.invoke(listener, event) as BlockFace) + assertEquals(BlockFace.UP, AtlasBlockListener.getPlayerFacing(event)) } @Test fun `getPlayerFacing returns EAST when dx positive`() { - val method = FluidBlockListener::class.java.getDeclaredMethod("getPlayerFacing", BlockPlaceEvent::class.java) - method.isAccessible = true - val placed = mockk(relaxed = true) val against = mockk(relaxed = true) every { placed.location } returns TestHelper.createLocation(1.0, 64.0, 0.0) @@ -160,14 +160,11 @@ class FluidBlockListenerTest { every { event.block } returns placed every { event.blockAgainst } returns against - assertEquals(BlockFace.EAST, method.invoke(listener, event) as BlockFace) + assertEquals(BlockFace.EAST, AtlasBlockListener.getPlayerFacing(event)) } @Test fun `getPlayerFacing returns DOWN when dy negative`() { - val method = FluidBlockListener::class.java.getDeclaredMethod("getPlayerFacing", BlockPlaceEvent::class.java) - method.isAccessible = true - val placed = mockk(relaxed = true) val against = mockk(relaxed = true) every { placed.location } returns TestHelper.createLocation(0.0, 63.0, 0.0) @@ -177,14 +174,11 @@ class FluidBlockListenerTest { every { event.block } returns placed every { event.blockAgainst } returns against - assertEquals(BlockFace.DOWN, method.invoke(listener, event) as BlockFace) + assertEquals(BlockFace.DOWN, AtlasBlockListener.getPlayerFacing(event)) } @Test fun `getPlayerFacing returns WEST when dx negative`() { - val method = FluidBlockListener::class.java.getDeclaredMethod("getPlayerFacing", BlockPlaceEvent::class.java) - method.isAccessible = true - val placed = mockk(relaxed = true) val against = mockk(relaxed = true) every { placed.location } returns TestHelper.createLocation(-1.0, 64.0, 0.0) @@ -194,14 +188,11 @@ class FluidBlockListenerTest { every { event.block } returns placed every { event.blockAgainst } returns against - assertEquals(BlockFace.WEST, method.invoke(listener, event) as BlockFace) + assertEquals(BlockFace.WEST, AtlasBlockListener.getPlayerFacing(event)) } @Test fun `getPlayerFacing returns SOUTH when dz positive`() { - val method = FluidBlockListener::class.java.getDeclaredMethod("getPlayerFacing", BlockPlaceEvent::class.java) - method.isAccessible = true - val placed = mockk(relaxed = true) val against = mockk(relaxed = true) every { placed.location } returns TestHelper.createLocation(0.0, 64.0, 1.0) @@ -211,14 +202,11 @@ class FluidBlockListenerTest { every { event.block } returns placed every { event.blockAgainst } returns against - assertEquals(BlockFace.SOUTH, method.invoke(listener, event) as BlockFace) + assertEquals(BlockFace.SOUTH, AtlasBlockListener.getPlayerFacing(event)) } @Test fun `getPlayerFacing returns NORTH when dz negative`() { - val method = FluidBlockListener::class.java.getDeclaredMethod("getPlayerFacing", BlockPlaceEvent::class.java) - method.isAccessible = true - val placed = mockk(relaxed = true) val against = mockk(relaxed = true) every { placed.location } returns TestHelper.createLocation(0.0, 64.0, -1.0) @@ -228,6 +216,6 @@ class FluidBlockListenerTest { every { event.block } returns placed every { event.blockAgainst } returns against - assertEquals(BlockFace.NORTH, method.invoke(listener, event) as BlockFace) + assertEquals(BlockFace.NORTH, AtlasBlockListener.getPlayerFacing(event)) } } diff --git a/src/test/kotlin/com/coderjoe/atlas/power/PowerBlockListenerTest.kt b/src/test/kotlin/com/coderjoe/atlas/power/PowerBlockListenerTest.kt index 8d5baf7..0710a41 100644 --- a/src/test/kotlin/com/coderjoe/atlas/power/PowerBlockListenerTest.kt +++ b/src/test/kotlin/com/coderjoe/atlas/power/PowerBlockListenerTest.kt @@ -1,10 +1,10 @@ package com.coderjoe.atlas.power import com.coderjoe.atlas.TestHelper +import com.coderjoe.atlas.core.AtlasBlockListener +import com.coderjoe.atlas.core.BlockSystem import com.coderjoe.atlas.power.block.SmallSolarPanel -import com.nexomc.nexo.api.NexoBlocks import io.mockk.* -import org.bukkit.Location import org.bukkit.block.Block import org.bukkit.block.BlockFace import org.bukkit.entity.Player @@ -12,21 +12,26 @@ import org.bukkit.event.block.Action import org.bukkit.event.block.BlockBreakEvent import org.bukkit.event.block.BlockPlaceEvent import org.bukkit.event.player.PlayerInteractEvent -import org.bukkit.inventory.EquipmentSlot -import org.bukkit.inventory.ItemStack import org.junit.jupiter.api.* import org.junit.jupiter.api.Assertions.* class PowerBlockListenerTest { private lateinit var registry: PowerBlockRegistry - private lateinit var listener: PowerBlockListener + private lateinit var listener: AtlasBlockListener @BeforeEach fun setup() { TestHelper.setup() registry = PowerBlockRegistry(TestHelper.mockPlugin) - listener = PowerBlockListener(TestHelper.mockPlugin, registry) + val system = BlockSystem( + name = "power", + registry = registry, + factory = PowerBlockFactory, + descriptors = emptyMap(), + showDialog = { _, _ -> } + ) + listener = AtlasBlockListener(TestHelper.mockPlugin, listOf(system)) } @AfterEach @@ -46,7 +51,6 @@ class PowerBlockListenerTest { every { event.block } returns block listener.onBlockPlace(event) - // No block should be registered assertNull(registry.getPowerBlock(loc)) } @@ -62,7 +66,6 @@ class PowerBlockListenerTest { every { event.block } returns block listener.onBlockBreak(event) - // Should not crash or unregister } @Test @@ -77,8 +80,6 @@ class PowerBlockListenerTest { val event = mockk(relaxed = true) every { event.block } returns block - // NexoItems.itemFromId() will throw NoClassDefFoundError in test env - // but the block should still be unregistered before that call try { listener.onBlockBreak(event) } catch (_: NoClassDefFoundError) {} @@ -136,9 +137,6 @@ class PowerBlockListenerTest { @Test fun `getPlayerFacing returns UP when dy positive`() { - val method = PowerBlockListener::class.java.getDeclaredMethod("getPlayerFacing", BlockPlaceEvent::class.java) - method.isAccessible = true - val placed = mockk(relaxed = true) val against = mockk(relaxed = true) every { placed.location } returns TestHelper.createLocation(0.0, 65.0, 0.0) @@ -148,15 +146,11 @@ class PowerBlockListenerTest { every { event.block } returns placed every { event.blockAgainst } returns against - val result = method.invoke(listener, event) as BlockFace - assertEquals(BlockFace.UP, result) + assertEquals(BlockFace.UP, AtlasBlockListener.getPlayerFacing(event)) } @Test fun `getPlayerFacing returns DOWN when dy negative`() { - val method = PowerBlockListener::class.java.getDeclaredMethod("getPlayerFacing", BlockPlaceEvent::class.java) - method.isAccessible = true - val placed = mockk(relaxed = true) val against = mockk(relaxed = true) every { placed.location } returns TestHelper.createLocation(0.0, 63.0, 0.0) @@ -166,15 +160,11 @@ class PowerBlockListenerTest { every { event.block } returns placed every { event.blockAgainst } returns against - val result = method.invoke(listener, event) as BlockFace - assertEquals(BlockFace.DOWN, result) + assertEquals(BlockFace.DOWN, AtlasBlockListener.getPlayerFacing(event)) } @Test fun `getPlayerFacing returns EAST when dx positive`() { - val method = PowerBlockListener::class.java.getDeclaredMethod("getPlayerFacing", BlockPlaceEvent::class.java) - method.isAccessible = true - val placed = mockk(relaxed = true) val against = mockk(relaxed = true) every { placed.location } returns TestHelper.createLocation(1.0, 64.0, 0.0) @@ -184,15 +174,11 @@ class PowerBlockListenerTest { every { event.block } returns placed every { event.blockAgainst } returns against - val result = method.invoke(listener, event) as BlockFace - assertEquals(BlockFace.EAST, result) + assertEquals(BlockFace.EAST, AtlasBlockListener.getPlayerFacing(event)) } @Test fun `getPlayerFacing returns WEST when dx negative`() { - val method = PowerBlockListener::class.java.getDeclaredMethod("getPlayerFacing", BlockPlaceEvent::class.java) - method.isAccessible = true - val placed = mockk(relaxed = true) val against = mockk(relaxed = true) every { placed.location } returns TestHelper.createLocation(-1.0, 64.0, 0.0) @@ -202,15 +188,11 @@ class PowerBlockListenerTest { every { event.block } returns placed every { event.blockAgainst } returns against - val result = method.invoke(listener, event) as BlockFace - assertEquals(BlockFace.WEST, result) + assertEquals(BlockFace.WEST, AtlasBlockListener.getPlayerFacing(event)) } @Test fun `getPlayerFacing returns SOUTH when dz positive`() { - val method = PowerBlockListener::class.java.getDeclaredMethod("getPlayerFacing", BlockPlaceEvent::class.java) - method.isAccessible = true - val placed = mockk(relaxed = true) val against = mockk(relaxed = true) every { placed.location } returns TestHelper.createLocation(0.0, 64.0, 1.0) @@ -220,15 +202,11 @@ class PowerBlockListenerTest { every { event.block } returns placed every { event.blockAgainst } returns against - val result = method.invoke(listener, event) as BlockFace - assertEquals(BlockFace.SOUTH, result) + assertEquals(BlockFace.SOUTH, AtlasBlockListener.getPlayerFacing(event)) } @Test fun `getPlayerFacing returns NORTH when dz negative`() { - val method = PowerBlockListener::class.java.getDeclaredMethod("getPlayerFacing", BlockPlaceEvent::class.java) - method.isAccessible = true - val placed = mockk(relaxed = true) val against = mockk(relaxed = true) every { placed.location } returns TestHelper.createLocation(0.0, 64.0, -1.0) @@ -238,7 +216,6 @@ class PowerBlockListenerTest { every { event.block } returns placed every { event.blockAgainst } returns against - val result = method.invoke(listener, event) as BlockFace - assertEquals(BlockFace.NORTH, result) + assertEquals(BlockFace.NORTH, AtlasBlockListener.getPlayerFacing(event)) } } From ec4169d090821965573ed8a0ad747f5b328eac6f Mon Sep 17 00:00:00 2001 From: CoderJoe Date: Fri, 6 Mar 2026 00:08:12 -0600 Subject: [PATCH 5/7] Extract shared dialog refresh loop into AtlasBlockDialog Move activeDialogs management, proximity/online/registry checks, and refresh task scheduling into core AtlasBlockDialog. PowerBlockDialog and FluidBlockDialog become thin rendering wrappers that delegate to AtlasBlockDialog.showDialog with domain-specific render lambdas. --- src/main/kotlin/com/coderjoe/atlas/Atlas.kt | 4 +- .../coderjoe/atlas/core/AtlasBlockDialog.kt | 52 ++++++++++++++++ .../coderjoe/atlas/fluid/FluidBlockDialog.kt | 48 ++++----------- .../coderjoe/atlas/power/PowerBlockDialog.kt | 61 ++++++------------- 4 files changed, 83 insertions(+), 82 deletions(-) create mode 100644 src/main/kotlin/com/coderjoe/atlas/core/AtlasBlockDialog.kt diff --git a/src/main/kotlin/com/coderjoe/atlas/Atlas.kt b/src/main/kotlin/com/coderjoe/atlas/Atlas.kt index 3ae7be2..dd5feb8 100644 --- a/src/main/kotlin/com/coderjoe/atlas/Atlas.kt +++ b/src/main/kotlin/com/coderjoe/atlas/Atlas.kt @@ -53,7 +53,7 @@ class Atlas : JavaPlugin() { factory = PowerBlockFactory, descriptors = powerDescriptors(), showDialog = { player, block -> - PowerBlockDialog.showPowerDialog(player, block as com.coderjoe.atlas.power.PowerBlock) + PowerBlockDialog.showPowerDialog(player, block as com.coderjoe.atlas.power.PowerBlock, powerBlockRegistry) } ) @@ -63,7 +63,7 @@ class Atlas : JavaPlugin() { factory = FluidBlockFactory, descriptors = fluidDescriptors(), showDialog = { player, block -> - FluidBlockDialog.showFluidDialog(player, block as com.coderjoe.atlas.fluid.FluidBlock) + FluidBlockDialog.showFluidDialog(player, block as com.coderjoe.atlas.fluid.FluidBlock, fluidBlockRegistry) } ) diff --git a/src/main/kotlin/com/coderjoe/atlas/core/AtlasBlockDialog.kt b/src/main/kotlin/com/coderjoe/atlas/core/AtlasBlockDialog.kt new file mode 100644 index 0000000..c4d3ece --- /dev/null +++ b/src/main/kotlin/com/coderjoe/atlas/core/AtlasBlockDialog.kt @@ -0,0 +1,52 @@ +package com.coderjoe.atlas.core + +import org.bukkit.entity.Player +import org.bukkit.plugin.java.JavaPlugin +import org.bukkit.scheduler.BukkitTask +import java.util.UUID +import java.util.concurrent.ConcurrentHashMap + +object AtlasBlockDialog { + private lateinit var plugin: JavaPlugin + private val activeDialogs = ConcurrentHashMap() + + fun init(plugin: JavaPlugin) { + this.plugin = plugin + } + + fun cleanup() { + activeDialogs.values.forEach { it.cancel() } + activeDialogs.clear() + } + + fun showDialog( + player: Player, + block: AtlasBlock, + registry: BlockRegistry<*>, + renderDialog: (Player, AtlasBlock, onClose: (Player) -> Unit) -> Unit + ) { + activeDialogs.remove(player.uniqueId)?.cancel() + + val onClose: (Player) -> Unit = { p -> activeDialogs.remove(p.uniqueId)?.cancel() } + + renderDialog(player, block, onClose) + + val task = plugin.server.scheduler.runTaskTimer(plugin, Runnable { + if (!player.isOnline) { + activeDialogs.remove(player.uniqueId)?.cancel() + return@Runnable + } + if (player.location.distance(block.location) > 10) { + activeDialogs.remove(player.uniqueId)?.cancel() + return@Runnable + } + if (registry.getBlock(block.location) == null) { + activeDialogs.remove(player.uniqueId)?.cancel() + return@Runnable + } + renderDialog(player, block, onClose) + }, 10L, 10L) + + activeDialogs[player.uniqueId] = task + } +} diff --git a/src/main/kotlin/com/coderjoe/atlas/fluid/FluidBlockDialog.kt b/src/main/kotlin/com/coderjoe/atlas/fluid/FluidBlockDialog.kt index bf03f36..75b5eb1 100644 --- a/src/main/kotlin/com/coderjoe/atlas/fluid/FluidBlockDialog.kt +++ b/src/main/kotlin/com/coderjoe/atlas/fluid/FluidBlockDialog.kt @@ -1,5 +1,7 @@ package com.coderjoe.atlas.fluid +import com.coderjoe.atlas.core.AtlasBlockDialog +import com.coderjoe.atlas.core.BlockRegistry import com.coderjoe.atlas.fluid.block.FluidContainer import com.coderjoe.atlas.fluid.block.FluidPipe import com.coderjoe.atlas.fluid.block.FluidPump @@ -16,45 +18,24 @@ import net.kyori.adventure.text.format.NamedTextColor import net.kyori.adventure.text.format.TextDecoration import org.bukkit.entity.Player import org.bukkit.plugin.java.JavaPlugin -import org.bukkit.scheduler.BukkitTask -import java.util.UUID -import java.util.concurrent.ConcurrentHashMap object FluidBlockDialog { - private lateinit var plugin: JavaPlugin - private val activeDialogs = ConcurrentHashMap() - fun init(plugin: JavaPlugin) { - this.plugin = plugin + AtlasBlockDialog.init(plugin) } - fun showFluidDialog(player: Player, fluidBlock: FluidBlock) { - activeDialogs.remove(player.uniqueId)?.cancel() - - sendDialog(player, fluidBlock) - - val task = plugin.server.scheduler.runTaskTimer(plugin, Runnable { - if (!player.isOnline) { - activeDialogs.remove(player.uniqueId)?.cancel() - return@Runnable - } - if (player.location.distance(fluidBlock.location) > 10) { - activeDialogs.remove(player.uniqueId)?.cancel() - return@Runnable - } - val registry = FluidBlockRegistry.instance - if (registry == null || registry.getFluidBlock(fluidBlock.location) == null) { - activeDialogs.remove(player.uniqueId)?.cancel() - return@Runnable - } - sendDialog(player, fluidBlock) - }, 10L, 10L) + fun showFluidDialog(player: Player, fluidBlock: FluidBlock, registry: BlockRegistry<*>) { + AtlasBlockDialog.showDialog(player, fluidBlock, registry) { p, block, onClose -> + sendDialog(p, block as FluidBlock, onClose) + } + } - activeDialogs[player.uniqueId] = task + fun cleanup() { + AtlasBlockDialog.cleanup() } - private fun sendDialog(player: Player, fluidBlock: FluidBlock) { + private fun sendDialog(player: Player, fluidBlock: FluidBlock, onClose: (Player) -> Unit) { val title = Component.text(getBlockDisplayName(fluidBlock)) val bodyText = buildFluidInfo(fluidBlock) val body = DialogBody.plainMessage(bodyText) @@ -62,7 +43,7 @@ object FluidBlockDialog { val closeAction = DialogAction.customClick( DialogActionCallback { _, audience -> val p = audience as? Player ?: return@DialogActionCallback - activeDialogs.remove(p.uniqueId)?.cancel() + onClose(p) }, ClickCallback.Options.builder().build() ) @@ -86,11 +67,6 @@ object FluidBlockDialog { player.showDialog(dialog) } - fun cleanup() { - activeDialogs.values.forEach { it.cancel() } - activeDialogs.clear() - } - private fun getBlockDisplayName(fluidBlock: FluidBlock): String = when (fluidBlock) { is FluidPump -> "Fluid Pump" is FluidPipe -> "Fluid Pipe (${fluidBlock.facing.name.lowercase().replaceFirstChar { it.uppercase() }})" diff --git a/src/main/kotlin/com/coderjoe/atlas/power/PowerBlockDialog.kt b/src/main/kotlin/com/coderjoe/atlas/power/PowerBlockDialog.kt index 50e30fe..0a87402 100644 --- a/src/main/kotlin/com/coderjoe/atlas/power/PowerBlockDialog.kt +++ b/src/main/kotlin/com/coderjoe/atlas/power/PowerBlockDialog.kt @@ -1,5 +1,7 @@ package com.coderjoe.atlas.power +import com.coderjoe.atlas.core.AtlasBlockDialog +import com.coderjoe.atlas.core.BlockRegistry import com.coderjoe.atlas.power.block.PowerCable import com.coderjoe.atlas.power.block.SmallBattery import com.coderjoe.atlas.power.block.SmallDrill @@ -17,56 +19,32 @@ import net.kyori.adventure.text.format.NamedTextColor import net.kyori.adventure.text.format.TextDecoration import org.bukkit.entity.Player import org.bukkit.plugin.java.JavaPlugin -import org.bukkit.scheduler.BukkitTask -import java.util.UUID -import java.util.concurrent.ConcurrentHashMap object PowerBlockDialog { - private lateinit var plugin: JavaPlugin - private val activeDialogs = ConcurrentHashMap() - fun init(plugin: JavaPlugin) { - this.plugin = plugin + AtlasBlockDialog.init(plugin) } - fun showPowerDialog(player: Player, powerBlock: PowerBlock) { - // Cancel any existing refresh task for this player - activeDialogs.remove(player.uniqueId)?.cancel() - - // Send initial dialog - sendDialog(player, powerBlock) - - // Start refresh task (every 10 ticks / 0.5s) - val task = plugin.server.scheduler.runTaskTimer(plugin, Runnable { - if (!player.isOnline) { - activeDialogs.remove(player.uniqueId)?.cancel() - return@Runnable - } - if (player.location.distance(powerBlock.location) > 10) { - activeDialogs.remove(player.uniqueId)?.cancel() - return@Runnable - } - val registry = PowerBlockRegistry.instance - if (registry == null || registry.getPowerBlock(powerBlock.location) == null) { - activeDialogs.remove(player.uniqueId)?.cancel() - return@Runnable - } - sendDialog(player, powerBlock) - }, 10L, 10L) + fun showPowerDialog(player: Player, powerBlock: PowerBlock, registry: BlockRegistry<*>) { + AtlasBlockDialog.showDialog(player, powerBlock, registry) { p, block, onClose -> + sendDialog(p, block as PowerBlock, onClose) + } + } - activeDialogs[player.uniqueId] = task + fun cleanup() { + AtlasBlockDialog.cleanup() } - private fun sendDialog(player: Player, powerBlock: PowerBlock) { + private fun sendDialog(player: Player, powerBlock: PowerBlock, onClose: (Player) -> Unit) { if (powerBlock is SmallDrill) { - sendDrillDialog(player, powerBlock) + sendDrillDialog(player, powerBlock, onClose) } else { - sendDefaultDialog(player, powerBlock) + sendDefaultDialog(player, powerBlock, onClose) } } - private fun sendDefaultDialog(player: Player, powerBlock: PowerBlock) { + private fun sendDefaultDialog(player: Player, powerBlock: PowerBlock, onClose: (Player) -> Unit) { val title = Component.text(getBlockDisplayName(powerBlock)) val bodyText = buildPowerInfo(powerBlock) val body = DialogBody.plainMessage(bodyText) @@ -74,7 +52,7 @@ object PowerBlockDialog { val closeAction = DialogAction.customClick( DialogActionCallback { _, audience -> val p = audience as? Player ?: return@DialogActionCallback - activeDialogs.remove(p.uniqueId)?.cancel() + onClose(p) }, ClickCallback.Options.builder().build() ) @@ -98,7 +76,7 @@ object PowerBlockDialog { player.showDialog(dialog) } - private fun sendDrillDialog(player: Player, drill: SmallDrill) { + private fun sendDrillDialog(player: Player, drill: SmallDrill, onClose: (Player) -> Unit) { val title = Component.text("Small Drill") val bodyText = buildPowerInfo(drill) val body = DialogBody.plainMessage(bodyText) @@ -117,7 +95,7 @@ object PowerBlockDialog { val closeAction = DialogAction.customClick( DialogActionCallback { _, audience -> val p = audience as? Player ?: return@DialogActionCallback - activeDialogs.remove(p.uniqueId)?.cancel() + onClose(p) }, ClickCallback.Options.builder().build() ) @@ -140,11 +118,6 @@ object PowerBlockDialog { player.showDialog(dialog) } - fun cleanup() { - activeDialogs.values.forEach { it.cancel() } - activeDialogs.clear() - } - private fun getBlockDisplayName(powerBlock: PowerBlock): String = when (powerBlock) { is SmallSolarPanel -> "Small Solar Panel" is SmallBattery -> "Small Battery" From 8023f8d54be12c9e66044bb77dd9969a906f1982 Mon Sep 17 00:00:00 2001 From: CoderJoe Date: Fri, 6 Mar 2026 00:10:23 -0600 Subject: [PATCH 6/7] Auto-discover textures by scanning JAR instead of maintaining hardcoded list MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the 90-entry hardcoded texture list in NexoIntegration with classpath scanning via discoverResources(). Supports both JAR (production) and file (test/dev) protocols. Adding new textures now requires zero code changes — just drop the .png file into the resources directory. --- .../com/coderjoe/atlas/NexoIntegration.kt | 127 +++++------------- 1 file changed, 34 insertions(+), 93 deletions(-) diff --git a/src/main/kotlin/com/coderjoe/atlas/NexoIntegration.kt b/src/main/kotlin/com/coderjoe/atlas/NexoIntegration.kt index 77cab81..0cbede7 100644 --- a/src/main/kotlin/com/coderjoe/atlas/NexoIntegration.kt +++ b/src/main/kotlin/com/coderjoe/atlas/NexoIntegration.kt @@ -1,11 +1,9 @@ package com.coderjoe.atlas -import com.nexomc.nexo.api.NexoBlocks -import com.nexomc.nexo.api.NexoItems -import org.bukkit.Location -import org.bukkit.block.Block import org.bukkit.plugin.java.JavaPlugin import java.io.File +import java.net.URI +import java.util.jar.JarFile class NexoIntegration(private val plugin: JavaPlugin) { private val nexoFolder: File @@ -41,99 +39,42 @@ class NexoIntegration(private val plugin: JavaPlugin) { texturesFolder.mkdirs() } - // Copy block textures - val textures = listOf( - "small_solar_panel", - "small_solar_panel_full", - "small_solar_panel_side", - "small_solar_panel_bottom", - "power_cable_front_powered", - "power_cable_back_powered", - "power_cable_side_up_powered", - "power_cable_side_down_powered", - "power_cable_side_left_powered", - "power_cable_side_right_powered", - "power_cable_cap_up_powered", - "power_cable_cap_down_powered", - "power_cable_cap_left_powered", - "power_cable_cap_right_powered", - "small_drill", - "small_drill_front", - "small_drill_arrow_up", - "small_drill_arrow_down", - "small_drill_arrow_left", - "small_drill_arrow_right", - "small_battery", - "small_battery_low", - "small_battery_medium", - "small_battery_full", - "small_battery_side", - "small_battery_bottom", - "fluid_pump_top", - "fluid_pump_side", - "fluid_pump_bottom", - "fluid_pump_top_active", - "fluid_pump_side_active", - "fluid_pump_bottom_active", - "fluid_pipe_front", - "fluid_pipe_back", - "fluid_pipe_side_up", - "fluid_pipe_side_down", - "fluid_pipe_side_left", - "fluid_pipe_side_right", - "fluid_pipe_front_filled", - "fluid_pipe_back_filled", - "fluid_pipe_side_filled_up", - "fluid_pipe_side_filled_down", - "fluid_pipe_side_filled_left", - "fluid_pipe_side_filled_right", - "fluid_pump_top_active_lava", - "fluid_pump_side_active_lava", - "fluid_pump_bottom_active_lava", - "fluid_pipe_front_filled_lava", - "fluid_pipe_back_filled_lava", - "fluid_pipe_side_filled_lava_up", - "fluid_pipe_side_filled_lava_down", - "fluid_pipe_side_filled_lava_left", - "fluid_pipe_side_filled_lava_right", - "fluid_container_front", - "fluid_container_back", - "fluid_container_side", - "fluid_container_top", - "fluid_container_front_water_low", - "fluid_container_back_water_low", - "fluid_container_side_water_low", - "fluid_container_top_water_low", - "fluid_container_front_water_medium", - "fluid_container_back_water_medium", - "fluid_container_side_water_medium", - "fluid_container_top_water_medium", - "fluid_container_front_water_full", - "fluid_container_back_water_full", - "fluid_container_side_water_full", - "fluid_container_top_water_full", - "fluid_container_front_lava_low", - "fluid_container_back_lava_low", - "fluid_container_side_lava_low", - "fluid_container_top_lava_low", - "fluid_container_front_lava_medium", - "fluid_container_back_lava_medium", - "fluid_container_side_lava_medium", - "fluid_container_top_lava_medium", - "fluid_container_front_lava_full", - "fluid_container_back_lava_full", - "fluid_container_side_lava_full", - "fluid_container_top_lava_full" - ) - for (textureName in textures) { - val textureFile = File(texturesFolder, "$textureName.png") - plugin.saveResource("nexo/pack/assets/atlas/textures/block/$textureName.png", true) - val sourceFile = File(plugin.dataFolder, "nexo/pack/assets/atlas/textures/block/$textureName.png") + val prefix = "nexo/pack/assets/atlas/textures/block/" + val texturePaths = discoverResources(prefix, ".png") + + for (resourcePath in texturePaths) { + val fileName = resourcePath.substringAfterLast("/") + val textureFile = File(texturesFolder, fileName) + plugin.saveResource(resourcePath, true) + val sourceFile = File(plugin.dataFolder, resourcePath) if (sourceFile.exists()) { sourceFile.copyTo(textureFile, overwrite = true) sourceFile.delete() - plugin.logger.info("Copied $textureName texture to Nexo") + plugin.logger.info("Copied ${fileName.removeSuffix(".png")} texture to Nexo") + } + } + } + + private fun discoverResources(prefix: String, suffix: String): List { + val url = javaClass.classLoader.getResource(prefix) ?: return emptyList() + + return when (url.protocol) { + "jar" -> { + val jarPath = url.toURI().schemeSpecificPart.substringBefore("!") + JarFile(File(URI(jarPath))).use { jar -> + jar.entries().asSequence() + .filter { it.name.startsWith(prefix) && it.name.endsWith(suffix) && !it.isDirectory } + .map { it.name } + .toList() + } + } + "file" -> { + File(url.toURI()).listFiles() + ?.filter { it.name.endsWith(suffix) } + ?.map { prefix + it.name } + ?: emptyList() } + else -> emptyList() } } From 65f63f6eb38af5d8a3427175af464b1928624758 Mon Sep 17 00:00:00 2001 From: CoderJoe Date: Fri, 6 Mar 2026 00:17:42 -0600 Subject: [PATCH 7/7] Replace initializer classes with descriptor-driven block registration Add BlockFactory.registerFromDescriptors() to register all block IDs from BlockDescriptor metadata. Delete PowerBlockInitializer and FluidBlockInitializer. Atlas.kt now derives registrations from the same descriptor lists used by BlockSystem. Update all tests to use TestHelper.initPowerFactory() / initFluidFactory() helpers. --- src/main/kotlin/com/coderjoe/atlas/Atlas.kt | 10 ++-- .../com/coderjoe/atlas/core/BlockFactory.kt | 9 ++++ .../atlas/fluid/FluidBlockInitializer.kt | 52 ------------------- .../atlas/power/PowerBlockInitializer.kt | 45 ---------------- .../com/coderjoe/atlas/AtlasPluginTest.kt | 10 ++-- .../kotlin/com/coderjoe/atlas/TestHelper.kt | 21 ++++++++ .../atlas/fluid/FluidBlockInitializerTest.kt | 16 +++--- .../atlas/fluid/FluidBlockPersistenceTest.kt | 2 +- .../atlas/power/PowerBlockInitializerTest.kt | 20 +++---- .../atlas/power/PowerBlockPersistenceTest.kt | 2 +- 10 files changed, 58 insertions(+), 129 deletions(-) delete mode 100644 src/main/kotlin/com/coderjoe/atlas/fluid/FluidBlockInitializer.kt delete mode 100644 src/main/kotlin/com/coderjoe/atlas/power/PowerBlockInitializer.kt diff --git a/src/main/kotlin/com/coderjoe/atlas/Atlas.kt b/src/main/kotlin/com/coderjoe/atlas/Atlas.kt index dd5feb8..5cbaa7a 100644 --- a/src/main/kotlin/com/coderjoe/atlas/Atlas.kt +++ b/src/main/kotlin/com/coderjoe/atlas/Atlas.kt @@ -4,12 +4,10 @@ import com.coderjoe.atlas.core.AtlasBlockListener import com.coderjoe.atlas.core.BlockSystem import com.coderjoe.atlas.fluid.FluidBlockDialog import com.coderjoe.atlas.fluid.FluidBlockFactory -import com.coderjoe.atlas.fluid.FluidBlockInitializer import com.coderjoe.atlas.fluid.FluidBlockPersistence import com.coderjoe.atlas.fluid.FluidBlockRegistry import com.coderjoe.atlas.power.PowerBlockDialog import com.coderjoe.atlas.power.PowerBlockFactory -import com.coderjoe.atlas.power.PowerBlockInitializer import com.coderjoe.atlas.power.PowerBlockPersistence import com.coderjoe.atlas.power.PowerBlockRegistry import org.bukkit.plugin.java.JavaPlugin @@ -107,21 +105,21 @@ class Atlas : JavaPlugin() { } fun initPowerSystem() { - PowerBlockInitializer.initialize(this) + PowerBlockFactory.registerFromDescriptors(powerDescriptors().values) powerBlockRegistry = PowerBlockRegistry(this) powerBlockPersistence = PowerBlockPersistence(this) powerBlockPersistence.load(powerBlockRegistry) - logger.info("Power system initialized") + logger.info("Power system initialized with ${PowerBlockFactory.getRegisteredBlockIds().size} block types") } fun initFluidSystem() { - FluidBlockInitializer.initialize(this) + FluidBlockFactory.registerFromDescriptors(fluidDescriptors().values) fluidBlockRegistry = FluidBlockRegistry(this) fluidBlockPersistence = FluidBlockPersistence(this) fluidBlockPersistence.load(fluidBlockRegistry) - logger.info("Fluid system initialized") + logger.info("Fluid system initialized with ${FluidBlockFactory.getRegisteredBlockIds().size} block types") } private fun powerDescriptors(): Map { diff --git a/src/main/kotlin/com/coderjoe/atlas/core/BlockFactory.kt b/src/main/kotlin/com/coderjoe/atlas/core/BlockFactory.kt index cc0c2d3..a53e351 100644 --- a/src/main/kotlin/com/coderjoe/atlas/core/BlockFactory.kt +++ b/src/main/kotlin/com/coderjoe/atlas/core/BlockFactory.kt @@ -22,6 +22,15 @@ open class BlockFactory { return blockConstructors.keys } + @Suppress("UNCHECKED_CAST") + fun registerFromDescriptors(descriptors: Collection) { + for (desc in descriptors) { + for (id in desc.allRegistrableIds) { + register(id, desc.constructor as (Location, BlockFace) -> T) + } + } + } + fun clear() { blockConstructors.clear() } diff --git a/src/main/kotlin/com/coderjoe/atlas/fluid/FluidBlockInitializer.kt b/src/main/kotlin/com/coderjoe/atlas/fluid/FluidBlockInitializer.kt deleted file mode 100644 index 9a26598..0000000 --- a/src/main/kotlin/com/coderjoe/atlas/fluid/FluidBlockInitializer.kt +++ /dev/null @@ -1,52 +0,0 @@ -package com.coderjoe.atlas.fluid - -import com.coderjoe.atlas.fluid.block.FluidContainer -import com.coderjoe.atlas.fluid.block.FluidPipe -import com.coderjoe.atlas.fluid.block.FluidPump -import org.bukkit.plugin.java.JavaPlugin - -object FluidBlockInitializer { - - fun initialize(plugin: JavaPlugin) { - plugin.logger.info("FluidBlockInitializer starting...") - - plugin.logger.info("Registering FluidPump...") - FluidBlockFactory.register(FluidPump.BLOCK_ID) { location, _ -> - FluidPump(location) - } - FluidBlockFactory.register(FluidPump.BLOCK_ID_ACTIVE) { location, _ -> - FluidPump(location) - } - FluidBlockFactory.register(FluidPump.BLOCK_ID_ACTIVE_LAVA) { location, _ -> - FluidPump(location) - } - - plugin.logger.info("Registering FluidPipe variants...") - for ((face, variantId) in FluidPipe.DIRECTIONAL_IDS) { - FluidBlockFactory.register(variantId) { location, facing -> - FluidPipe(location, facing) - } - } - for ((face, variantId) in FluidPipe.WATER_FILLED_IDS) { - FluidBlockFactory.register(variantId) { location, facing -> - FluidPipe(location, facing) - } - } - for ((face, variantId) in FluidPipe.LAVA_FILLED_IDS) { - FluidBlockFactory.register(variantId) { location, facing -> - FluidPipe(location, facing) - } - } - - plugin.logger.info("Registering FluidContainer variants...") - for (variantId in FluidContainer.ALL_VARIANT_IDS) { - FluidBlockFactory.register(variantId) { location, facing -> - FluidContainer(location, facing) - } - } - - val registeredBlocks = FluidBlockFactory.getRegisteredBlockIds() - plugin.logger.info("Initialized ${registeredBlocks.size} fluid block type(s): ${registeredBlocks.joinToString(", ")}") - plugin.logger.info("FluidBlockInitializer complete") - } -} diff --git a/src/main/kotlin/com/coderjoe/atlas/power/PowerBlockInitializer.kt b/src/main/kotlin/com/coderjoe/atlas/power/PowerBlockInitializer.kt deleted file mode 100644 index 4b03abc..0000000 --- a/src/main/kotlin/com/coderjoe/atlas/power/PowerBlockInitializer.kt +++ /dev/null @@ -1,45 +0,0 @@ -package com.coderjoe.atlas.power - -import com.coderjoe.atlas.power.block.PowerCable -import com.coderjoe.atlas.power.block.SmallBattery -import com.coderjoe.atlas.power.block.SmallDrill -import com.coderjoe.atlas.power.block.SmallSolarPanel -import org.bukkit.plugin.java.JavaPlugin - -object PowerBlockInitializer { - - fun initialize(plugin: JavaPlugin) { - plugin.logger.info("PowerBlockInitializer starting...") - - plugin.logger.info("Registering SmallSolarPanel...") - PowerBlockFactory.register("small_solar_panel") { location, _ -> - SmallSolarPanel(location) - } - - plugin.logger.info("Registering SmallDrill variants...") - for (variantId in SmallDrill.ALL_DIRECTIONAL_IDS) { - PowerBlockFactory.register(variantId) { location, facing -> - SmallDrill(location, facing) - } - } - - plugin.logger.info("Registering SmallBattery variants...") - for (variantId in SmallBattery.ALL_VARIANT_IDS) { - PowerBlockFactory.register(variantId) { location, facing -> - SmallBattery(location, facing) - } - } - - plugin.logger.info("Registering PowerCable variants...") - // Register all 6 directional variants - for ((face, variantId) in PowerCable.DIRECTIONAL_IDS) { - PowerBlockFactory.register(variantId) { location, facing -> - PowerCable(location, facing) - } - } - - val registeredBlocks = PowerBlockFactory.getRegisteredBlockIds() - plugin.logger.info("Initialized ${registeredBlocks.size} power block type(s): ${registeredBlocks.joinToString(", ")}") - plugin.logger.info("PowerBlockInitializer complete") - } -} diff --git a/src/test/kotlin/com/coderjoe/atlas/AtlasPluginTest.kt b/src/test/kotlin/com/coderjoe/atlas/AtlasPluginTest.kt index bd5466e..5f0d697 100644 --- a/src/test/kotlin/com/coderjoe/atlas/AtlasPluginTest.kt +++ b/src/test/kotlin/com/coderjoe/atlas/AtlasPluginTest.kt @@ -2,11 +2,9 @@ package com.coderjoe.atlas import com.coderjoe.atlas.fluid.FluidBlockDialog import com.coderjoe.atlas.fluid.FluidBlockFactory -import com.coderjoe.atlas.fluid.FluidBlockInitializer import com.coderjoe.atlas.fluid.FluidBlockRegistry import com.coderjoe.atlas.power.PowerBlockDialog import com.coderjoe.atlas.power.PowerBlockFactory -import com.coderjoe.atlas.power.PowerBlockInitializer import com.coderjoe.atlas.power.PowerBlockRegistry import org.junit.jupiter.api.* import org.junit.jupiter.api.Assertions.* @@ -27,13 +25,13 @@ class AtlasPluginTest { @Test fun `power system initializes with 17 block types`() { - PowerBlockInitializer.initialize(TestHelper.mockPlugin) + TestHelper.initPowerFactory() assertEquals(17, PowerBlockFactory.getRegisteredBlockIds().size) } @Test fun `fluid system initializes with 63 block types`() { - FluidBlockInitializer.initialize(TestHelper.mockPlugin) + TestHelper.initFluidFactory() assertEquals(63, FluidBlockFactory.getRegisteredBlockIds().size) } @@ -61,7 +59,7 @@ class AtlasPluginTest { @Test fun `stopAll clears power blocks`() { - PowerBlockInitializer.initialize(TestHelper.mockPlugin) + TestHelper.initPowerFactory() val registry = PowerBlockRegistry(TestHelper.mockPlugin) registry.stopAll() assertEquals(0, registry.getAllPowerBlocks().size) @@ -69,7 +67,7 @@ class AtlasPluginTest { @Test fun `stopAll clears fluid blocks`() { - FluidBlockInitializer.initialize(TestHelper.mockPlugin) + TestHelper.initFluidFactory() val registry = FluidBlockRegistry(TestHelper.mockPlugin) registry.stopAll() assertEquals(0, registry.getAllFluidBlocksWithIds().size) diff --git a/src/test/kotlin/com/coderjoe/atlas/TestHelper.kt b/src/test/kotlin/com/coderjoe/atlas/TestHelper.kt index 0669d0f..922b542 100644 --- a/src/test/kotlin/com/coderjoe/atlas/TestHelper.kt +++ b/src/test/kotlin/com/coderjoe/atlas/TestHelper.kt @@ -5,9 +5,16 @@ import com.coderjoe.atlas.core.BlockRegistry import com.coderjoe.atlas.fluid.FluidBlock import com.coderjoe.atlas.fluid.FluidBlockFactory import com.coderjoe.atlas.fluid.FluidBlockRegistry +import com.coderjoe.atlas.fluid.block.FluidContainer +import com.coderjoe.atlas.fluid.block.FluidPipe +import com.coderjoe.atlas.fluid.block.FluidPump import com.coderjoe.atlas.power.PowerBlock import com.coderjoe.atlas.power.PowerBlockFactory import com.coderjoe.atlas.power.PowerBlockRegistry +import com.coderjoe.atlas.power.block.PowerCable +import com.coderjoe.atlas.power.block.SmallBattery +import com.coderjoe.atlas.power.block.SmallDrill +import com.coderjoe.atlas.power.block.SmallSolarPanel import io.mockk.* import org.bukkit.Location import org.bukkit.Server @@ -126,6 +133,20 @@ object TestHelper { } catch (_: Exception) {} } + fun initPowerFactory() { + PowerBlockFactory.registerFromDescriptors(listOf( + SmallSolarPanel.descriptor, SmallDrill.descriptor, + SmallBattery.descriptor, PowerCable.descriptor + )) + } + + fun initFluidFactory() { + FluidBlockFactory.registerFromDescriptors(listOf( + FluidPump.descriptor, FluidPipe.descriptor, + FluidContainer.descriptor + )) + } + private fun clearFactories() { PowerBlockFactory.clear() FluidBlockFactory.clear() diff --git a/src/test/kotlin/com/coderjoe/atlas/fluid/FluidBlockInitializerTest.kt b/src/test/kotlin/com/coderjoe/atlas/fluid/FluidBlockInitializerTest.kt index b3dd804..1eda9f4 100644 --- a/src/test/kotlin/com/coderjoe/atlas/fluid/FluidBlockInitializerTest.kt +++ b/src/test/kotlin/com/coderjoe/atlas/fluid/FluidBlockInitializerTest.kt @@ -7,7 +7,7 @@ import org.bukkit.block.BlockFace import org.junit.jupiter.api.* import org.junit.jupiter.api.Assertions.* -class FluidBlockInitializerTest { +class FluidBlockRegistrationTest { @BeforeEach fun setup() { @@ -21,7 +21,7 @@ class FluidBlockInitializerTest { @Test fun `initialize registers all expected IDs`() { - FluidBlockInitializer.initialize(TestHelper.mockPlugin) + TestHelper.initFluidFactory() val ids = FluidBlockFactory.getRegisteredBlockIds() // 3 pump + 6 directional pipe + 6 water-filled + 6 lava-filled + 42 container = 63 @@ -30,7 +30,7 @@ class FluidBlockInitializerTest { @Test fun `pump IDs are registered`() { - FluidBlockInitializer.initialize(TestHelper.mockPlugin) + TestHelper.initFluidFactory() assertTrue(FluidBlockFactory.isRegistered(FluidPump.BLOCK_ID)) assertTrue(FluidBlockFactory.isRegistered(FluidPump.BLOCK_ID_ACTIVE)) assertTrue(FluidBlockFactory.isRegistered(FluidPump.BLOCK_ID_ACTIVE_LAVA)) @@ -38,7 +38,7 @@ class FluidBlockInitializerTest { @Test fun `all pipe directional IDs are registered`() { - FluidBlockInitializer.initialize(TestHelper.mockPlugin) + TestHelper.initFluidFactory() for (id in FluidPipe.DIRECTIONAL_IDS.values) { assertTrue(FluidBlockFactory.isRegistered(id), "Missing: $id") } @@ -46,7 +46,7 @@ class FluidBlockInitializerTest { @Test fun `all pipe water-filled IDs are registered`() { - FluidBlockInitializer.initialize(TestHelper.mockPlugin) + TestHelper.initFluidFactory() for (id in FluidPipe.WATER_FILLED_IDS.values) { assertTrue(FluidBlockFactory.isRegistered(id), "Missing: $id") } @@ -54,7 +54,7 @@ class FluidBlockInitializerTest { @Test fun `all pipe lava-filled IDs are registered`() { - FluidBlockInitializer.initialize(TestHelper.mockPlugin) + TestHelper.initFluidFactory() for (id in FluidPipe.LAVA_FILLED_IDS.values) { assertTrue(FluidBlockFactory.isRegistered(id), "Missing: $id") } @@ -62,14 +62,14 @@ class FluidBlockInitializerTest { @Test fun `pump ID creates FluidPump`() { - FluidBlockInitializer.initialize(TestHelper.mockPlugin) + TestHelper.initFluidFactory() val block = FluidBlockFactory.createFluidBlock("fluid_pump", TestHelper.createLocation()) assertTrue(block is FluidPump) } @Test fun `pipe ID creates FluidPipe`() { - FluidBlockInitializer.initialize(TestHelper.mockPlugin) + TestHelper.initFluidFactory() val block = FluidBlockFactory.createFluidBlock("fluid_pipe_north", TestHelper.createLocation(), BlockFace.NORTH) assertTrue(block is FluidPipe) } diff --git a/src/test/kotlin/com/coderjoe/atlas/fluid/FluidBlockPersistenceTest.kt b/src/test/kotlin/com/coderjoe/atlas/fluid/FluidBlockPersistenceTest.kt index 10638bf..6cf6a8e 100644 --- a/src/test/kotlin/com/coderjoe/atlas/fluid/FluidBlockPersistenceTest.kt +++ b/src/test/kotlin/com/coderjoe/atlas/fluid/FluidBlockPersistenceTest.kt @@ -18,7 +18,7 @@ class FluidBlockPersistenceTest { TestHelper.setup() registry = FluidBlockRegistry(TestHelper.mockPlugin) persistence = FluidBlockPersistence(TestHelper.mockPlugin) - FluidBlockInitializer.initialize(TestHelper.mockPlugin) + TestHelper.initFluidFactory() } @AfterEach diff --git a/src/test/kotlin/com/coderjoe/atlas/power/PowerBlockInitializerTest.kt b/src/test/kotlin/com/coderjoe/atlas/power/PowerBlockInitializerTest.kt index 8cb4eae..bf677a2 100644 --- a/src/test/kotlin/com/coderjoe/atlas/power/PowerBlockInitializerTest.kt +++ b/src/test/kotlin/com/coderjoe/atlas/power/PowerBlockInitializerTest.kt @@ -9,7 +9,7 @@ import org.bukkit.block.BlockFace import org.junit.jupiter.api.* import org.junit.jupiter.api.Assertions.* -class PowerBlockInitializerTest { +class PowerBlockRegistrationTest { @BeforeEach fun setup() { @@ -23,7 +23,7 @@ class PowerBlockInitializerTest { @Test fun `initialize registers all expected IDs`() { - PowerBlockInitializer.initialize(TestHelper.mockPlugin) + TestHelper.initPowerFactory() val ids = PowerBlockFactory.getRegisteredBlockIds() // 1 solar + 6 drill + 4 battery + 6 cable = 17 @@ -32,13 +32,13 @@ class PowerBlockInitializerTest { @Test fun `solar panel ID is registered`() { - PowerBlockInitializer.initialize(TestHelper.mockPlugin) + TestHelper.initPowerFactory() assertTrue(PowerBlockFactory.isRegistered("small_solar_panel")) } @Test fun `all drill directional IDs are registered`() { - PowerBlockInitializer.initialize(TestHelper.mockPlugin) + TestHelper.initPowerFactory() for (id in SmallDrill.ALL_DIRECTIONAL_IDS) { assertTrue(PowerBlockFactory.isRegistered(id), "Missing drill ID: $id") } @@ -46,7 +46,7 @@ class PowerBlockInitializerTest { @Test fun `all battery variant IDs are registered`() { - PowerBlockInitializer.initialize(TestHelper.mockPlugin) + TestHelper.initPowerFactory() for (id in SmallBattery.ALL_VARIANT_IDS) { assertTrue(PowerBlockFactory.isRegistered(id), "Missing battery ID: $id") } @@ -54,7 +54,7 @@ class PowerBlockInitializerTest { @Test fun `all cable directional IDs are registered`() { - PowerBlockInitializer.initialize(TestHelper.mockPlugin) + TestHelper.initPowerFactory() for (id in PowerCable.DIRECTIONAL_IDS.values) { assertTrue(PowerBlockFactory.isRegistered(id), "Missing cable ID: $id") } @@ -62,28 +62,28 @@ class PowerBlockInitializerTest { @Test fun `solar panel ID creates SmallSolarPanel`() { - PowerBlockInitializer.initialize(TestHelper.mockPlugin) + TestHelper.initPowerFactory() val block = PowerBlockFactory.createPowerBlock("small_solar_panel", TestHelper.createLocation()) assertTrue(block is SmallSolarPanel) } @Test fun `drill ID creates SmallDrill`() { - PowerBlockInitializer.initialize(TestHelper.mockPlugin) + TestHelper.initPowerFactory() val block = PowerBlockFactory.createPowerBlock("small_drill_north", TestHelper.createLocation(), BlockFace.NORTH) assertTrue(block is SmallDrill) } @Test fun `battery ID creates SmallBattery`() { - PowerBlockInitializer.initialize(TestHelper.mockPlugin) + TestHelper.initPowerFactory() val block = PowerBlockFactory.createPowerBlock("small_battery", TestHelper.createLocation(), BlockFace.DOWN) assertTrue(block is SmallBattery) } @Test fun `cable ID creates PowerCable`() { - PowerBlockInitializer.initialize(TestHelper.mockPlugin) + TestHelper.initPowerFactory() val block = PowerBlockFactory.createPowerBlock("power_cable_north", TestHelper.createLocation(), BlockFace.NORTH) assertTrue(block is PowerCable) } diff --git a/src/test/kotlin/com/coderjoe/atlas/power/PowerBlockPersistenceTest.kt b/src/test/kotlin/com/coderjoe/atlas/power/PowerBlockPersistenceTest.kt index d91141e..c6d7363 100644 --- a/src/test/kotlin/com/coderjoe/atlas/power/PowerBlockPersistenceTest.kt +++ b/src/test/kotlin/com/coderjoe/atlas/power/PowerBlockPersistenceTest.kt @@ -22,7 +22,7 @@ class PowerBlockPersistenceTest { persistence = PowerBlockPersistence(TestHelper.mockPlugin) // Initialize factory so load() can create blocks - PowerBlockInitializer.initialize(TestHelper.mockPlugin) + TestHelper.initPowerFactory() } @AfterEach