diff --git a/src/main/kotlin/org/xodium/vanillaplus/VanillaPlus.kt b/src/main/kotlin/org/xodium/vanillaplus/VanillaPlus.kt
index 6daf35ac..185d0adb 100644
--- a/src/main/kotlin/org/xodium/vanillaplus/VanillaPlus.kt
+++ b/src/main/kotlin/org/xodium/vanillaplus/VanillaPlus.kt
@@ -56,6 +56,7 @@ internal class VanillaPlus : JavaPlugin() {
logger.info(
listOf(
+ BookshelfModule,
BooksModule,
ChatModule,
DimensionsModule,
diff --git a/src/main/kotlin/org/xodium/vanillaplus/data/ConfigData.kt b/src/main/kotlin/org/xodium/vanillaplus/data/ConfigData.kt
index fe4af80e..01392379 100644
--- a/src/main/kotlin/org/xodium/vanillaplus/data/ConfigData.kt
+++ b/src/main/kotlin/org/xodium/vanillaplus/data/ConfigData.kt
@@ -23,6 +23,7 @@ import kotlin.time.measureTime
/** Configuration data for the plugin. */
@Serializable
internal data class ConfigData(
+ var bookshelfModule: BookshelfModule.Config = BookshelfModule.Config(),
var booksModule: BooksModule.Config = BooksModule.Config(),
var chatModule: ChatModule.Config = ChatModule.Config(),
var dimensionsModule: DimensionsModule.Config = DimensionsModule.Config(),
diff --git a/src/main/kotlin/org/xodium/vanillaplus/modules/BookshelfModule.kt b/src/main/kotlin/org/xodium/vanillaplus/modules/BookshelfModule.kt
new file mode 100644
index 00000000..b3254877
--- /dev/null
+++ b/src/main/kotlin/org/xodium/vanillaplus/modules/BookshelfModule.kt
@@ -0,0 +1,134 @@
+package org.xodium.vanillaplus.modules
+
+import kotlinx.serialization.Serializable
+import net.kyori.adventure.text.Component
+import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder
+import org.bukkit.block.ChiseledBookshelf
+import org.bukkit.entity.Player
+import org.bukkit.event.EventHandler
+import org.bukkit.event.player.PlayerInteractEvent
+import org.bukkit.inventory.meta.BookMeta
+import org.bukkit.inventory.meta.EnchantmentStorageMeta
+import org.xodium.vanillaplus.interfaces.ModuleInterface
+import org.xodium.vanillaplus.utils.Utils.MM
+
+/** Represents a module handling bookshelf mechanics within the system. */
+internal object BookshelfModule : ModuleInterface {
+ @EventHandler
+ fun on(event: PlayerInteractEvent) {
+ val player = event.player
+
+ if (!player.isSneaking) return
+
+ val bookshelf = event.clickedBlock?.state as? ChiseledBookshelf ?: return
+
+ event.isCancelled = true
+
+ handleBookshelfInteraction(player, bookshelf)
+ }
+
+ /**
+ * Handles the interaction logic for a chiseled bookshelf.
+ * @param player The player interacting with the bookshelf.
+ * @param bookshelf The clicked chiseled bookshelf.
+ */
+ private fun handleBookshelfInteraction(
+ player: Player,
+ bookshelf: ChiseledBookshelf,
+ ) {
+ val inventory = bookshelf.inventory
+
+ player.sendMessage(MM.deserialize(config.bookshelfModule.header))
+
+ val slots = 0 until inventory.size
+
+ if (!slots.any { inventory.getItem(it) != null }) return
+
+ for (i in slots) {
+ val item = inventory.getItem(i) ?: continue
+ val slotNumber = i + 1
+ val slotPrefix = createSlotPrefix(slotNumber)
+
+ when (val itemMeta = item.itemMeta) {
+ is BookMeta -> player.sendMessage(renderBook(slotPrefix, itemMeta))
+ is EnchantmentStorageMeta -> player.sendMessage(renderEnchantedBook(slotPrefix, itemMeta))
+ else -> player.sendMessage(renderGenericItem(slotPrefix, item.type.key.key))
+ }
+ }
+
+ player.sendMessage(MM.deserialize(config.bookshelfModule.footer))
+ }
+
+ /**
+ * Renders a written or writable book message.
+ * @param slotPrefix The formatted slot prefix component.
+ * @param meta The book meta.
+ * @return The rendered message component.
+ */
+ private fun renderBook(
+ slotPrefix: Component,
+ meta: BookMeta,
+ ): Component {
+ val title = meta.title
+ val author = meta.author
+
+ var message =
+ slotPrefix.append(
+ MM.deserialize(
+ " ",
+ ),
+ )
+
+ if (title != null) message = message.append(MM.deserialize(" $title"))
+ if (author != null) message = message.append(MM.deserialize(" by $author"))
+
+ return message
+ }
+
+ /**
+ * Renders an enchanted book message.
+ * @param slotPrefix The formatted slot prefix component.
+ * @param meta The enchantment storage meta.
+ * @return The rendered message component.
+ */
+ private fun renderEnchantedBook(
+ slotPrefix: Component,
+ meta: EnchantmentStorageMeta,
+ ): Component {
+ val enchantmentList = meta.storedEnchants.entries.joinToString(", ") { "${it.key.key.key} ${it.value}" }
+
+ return slotPrefix.append(
+ MM.deserialize(
+ " $enchantmentList",
+ ),
+ )
+ }
+
+ /**
+ * Renders a generic item message.
+ * @param slotPrefix The formatted slot prefix component.
+ * @param itemKey The namespaced item key.
+ * @return The rendered message component.
+ */
+ private fun renderGenericItem(
+ slotPrefix: Component,
+ itemKey: String,
+ ): Component = slotPrefix.append(MM.deserialize(" "))
+
+ private fun createSlotPrefix(slotNumber: Int): Component =
+ MM.deserialize(
+ config.bookshelfModule.slotPrefix,
+ Placeholder.component("slot", MM.deserialize(slotNumber.toString())),
+ )
+
+ /** Represents the config of the module. */
+ @Serializable
+ data class Config(
+ var enabled: Boolean = true,
+ var header: String =
+ "\n───────────────────────────────────",
+ var footer: String =
+ "───────────────────────────────────\n",
+ var slotPrefix: String = ".",
+ )
+}