diff --git a/.github/workflows/generate-artifacts.yml b/.github/workflows/generate-artifacts.yml index 93414fc17..e07b09ee4 100644 --- a/.github/workflows/generate-artifacts.yml +++ b/.github/workflows/generate-artifacts.yml @@ -9,10 +9,10 @@ jobs: strategy: fail-fast: false matrix: - platform: [Bukkit, Paper] + platform: [Bukkit, Paper, Minestom] runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - uses: gradle/wrapper-validation-action@v1 - uses: actions/setup-java@v2 with: @@ -31,7 +31,7 @@ jobs: arguments: inventory-framework-platform-${{ steps.vars.outputs.lowercase }}:shadowJar gradle-version: wrapper - name: Upload artifacts - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: inventory-framework-platform-${{ steps.vars.outputs.lowercase }}-${{ github.sha }}.jar path: inventory-framework-platform-${{ steps.vars.outputs.lowercase }}/build/libs/*.jar diff --git a/.gitignore b/.gitignore index 3cec2558e..80982cfbb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .gradle/ .idea/ +.kotlin/ build/ libs/ \ No newline at end of file diff --git a/examples/minestom/build.gradle b/examples/minestom/build.gradle new file mode 100644 index 000000000..d57be9ec0 --- /dev/null +++ b/examples/minestom/build.gradle @@ -0,0 +1,30 @@ + +plugins { + alias(libs.plugins.shadowjar) + alias(libs.plugins.kotlin) + id("application") +} + +apply from: '../../library.gradle' + +dependencies { + implementation projects.inventoryFrameworkPlatformMinestom + implementation libs.minestom +} + +shadowJar { + archiveBaseName.set('inventory-framework-example') + archiveAppendix.set('minestome') +} + +java { + targetCompatibility = JavaVersion.VERSION_21 + toolchain { + languageVersion.set(JavaLanguageVersion.of(21)) + } +} + +application { + applicationDefaultJvmArgs = ["-Dme.devnatan.inventoryframework.debug=true"] + mainClass = 'me.devnatan.inventoryframework.runtime.SampleServerKt' +} \ No newline at end of file diff --git a/examples/minestom/src/main/kotlin/me/devnatan/inventoryframework/runtime/ExampleUtil.kt b/examples/minestom/src/main/kotlin/me/devnatan/inventoryframework/runtime/ExampleUtil.kt new file mode 100644 index 000000000..0638cf19e --- /dev/null +++ b/examples/minestom/src/main/kotlin/me/devnatan/inventoryframework/runtime/ExampleUtil.kt @@ -0,0 +1,28 @@ +package me.devnatan.inventoryframework.runtime + +import net.kyori.adventure.text.Component +import net.minestom.server.item.ItemStack +import net.minestom.server.item.Material +import java.util.* + + +object ExampleUtil { + @JvmStatic + fun getRandomItems(amount: Int): List { + val materials = Material.values().toTypedArray() + val random = Random() + + val result = ArrayList() + + for (i in 0 until amount) { + result.add(ItemStack.of(materials[random.nextInt(10, 100)])) + } + + return result + } + + @JvmStatic + fun displayItem(material: Material, displayName: String): ItemStack { + return ItemStack.of(material).withCustomName(Component.text(displayName)) + } +} diff --git a/examples/minestom/src/main/kotlin/me/devnatan/inventoryframework/runtime/SampleServer.kt b/examples/minestom/src/main/kotlin/me/devnatan/inventoryframework/runtime/SampleServer.kt new file mode 100644 index 000000000..5a7b25898 --- /dev/null +++ b/examples/minestom/src/main/kotlin/me/devnatan/inventoryframework/runtime/SampleServer.kt @@ -0,0 +1,70 @@ +package me.devnatan.inventoryframework.runtime + +import me.devnatan.inventoryframework.ViewFrame +import me.devnatan.inventoryframework.runtime.command.GamemodeCommand +import me.devnatan.inventoryframework.runtime.command.IFExampleCommand +import me.devnatan.inventoryframework.runtime.view.Failing +import me.devnatan.inventoryframework.runtime.view.ScheduledView +import me.devnatan.inventoryframework.runtime.view.SimplePagination +import net.minestom.server.MinecraftServer +import net.minestom.server.coordinate.Pos +import net.minestom.server.entity.PlayerSkin +import net.minestom.server.event.Event +import net.minestom.server.event.EventFilter +import net.minestom.server.event.EventNode +import net.minestom.server.event.player.AsyncPlayerConfigurationEvent +import net.minestom.server.event.player.PlayerSkinInitEvent +import net.minestom.server.event.player.PlayerSpawnEvent +import net.minestom.server.instance.LightingChunk +import net.minestom.server.instance.block.Block +import net.minestom.server.inventory.TransactionOption +import net.minestom.server.item.ItemStack +import net.minestom.server.item.Material + +class SampleServer { + + init { + val server = MinecraftServer.init() + val instanceManager = MinecraftServer.getInstanceManager() + + // Create word filled with quartz blocks up to height 50 + val instance = instanceManager.createInstanceContainer() + instance.setGenerator { + it.modifier().fillHeight(it.absoluteStart().blockY(), 50, Block.QUARTZ_BLOCK) + } + instance.setChunkSupplier(::LightingChunk) + + val handler = MinecraftServer.getGlobalEventHandler() + + handler.addListener(AsyncPlayerConfigurationEvent::class.java) { event -> + event.spawningInstance = instance + event.player.respawnPoint = Pos(0.0, 53.0, 0.0) + } + handler.addListener(PlayerSkinInitEvent::class.java) { event -> + event.skin = PlayerSkin.fromUsername(event.player.username) + } + handler.addListener(PlayerSpawnEvent::class.java) { event -> + event.player.inventory.addItemStacks(ExampleUtil.getRandomItems(20), TransactionOption.ALL) + event.player.inventory.addItemStack(ItemStack.of(Material.OAK_PLANKS, 64), TransactionOption.ALL) + } + + val viewFrame = ViewFrame.create(handler) + .with( + Failing(), + SimplePagination(), + ScheduledView()) + .register() + + MinecraftServer.getCommandManager().register( + IFExampleCommand(viewFrame), + GamemodeCommand(viewFrame), + ) + + + server.start("0.0.0.0", 25565) + } +} + +fun main() { + SampleServer() +} diff --git a/examples/minestom/src/main/kotlin/me/devnatan/inventoryframework/runtime/command/GamemodeCommand.kt b/examples/minestom/src/main/kotlin/me/devnatan/inventoryframework/runtime/command/GamemodeCommand.kt new file mode 100644 index 000000000..679e37bac --- /dev/null +++ b/examples/minestom/src/main/kotlin/me/devnatan/inventoryframework/runtime/command/GamemodeCommand.kt @@ -0,0 +1,38 @@ +package me.devnatan.inventoryframework.runtime.command + +import me.devnatan.inventoryframework.ViewFrame +import net.minestom.server.command.CommandSender +import net.minestom.server.command.builder.Command +import net.minestom.server.command.builder.CommandContext +import net.minestom.server.command.builder.arguments.Argument +import net.minestom.server.command.builder.arguments.ArgumentType +import net.minestom.server.command.builder.suggestion.SuggestionEntry +import net.minestom.server.entity.GameMode +import net.minestom.server.entity.Player +import java.util.* + +class GamemodeCommand(private val viewFrame: ViewFrame) : Command("gamemode") { + + private val gamemodeArg: Argument = ArgumentType.Enum("gameMode", GameMode::class.java) + .setSuggestionCallback { _, _, suggestion -> + for (gameMode in GameMode.entries) { + suggestion.addEntry(SuggestionEntry(gameMode.name.lowercase())) + } + } + + init { + setDefaultExecutor(::onCommand) + addSyntax(::onCommand, gamemodeArg) + } + + + private fun onCommand(sender: CommandSender, ctx: CommandContext) { + if (sender !is Player) { + sender.sendMessage("This command can only be executed by players.") + return + } + + val gameMode: GameMode = ctx.get(gamemodeArg) + sender.gameMode = gameMode + } +} \ No newline at end of file diff --git a/examples/minestom/src/main/kotlin/me/devnatan/inventoryframework/runtime/command/IFExampleCommand.kt b/examples/minestom/src/main/kotlin/me/devnatan/inventoryframework/runtime/command/IFExampleCommand.kt new file mode 100644 index 000000000..bd4910155 --- /dev/null +++ b/examples/minestom/src/main/kotlin/me/devnatan/inventoryframework/runtime/command/IFExampleCommand.kt @@ -0,0 +1,54 @@ +package me.devnatan.inventoryframework.runtime.command + +import me.devnatan.inventoryframework.ViewFrame +import me.devnatan.inventoryframework.runtime.view.Failing +import me.devnatan.inventoryframework.runtime.view.ScheduledView +import me.devnatan.inventoryframework.runtime.view.SimplePagination +import net.minestom.server.command.CommandSender +import net.minestom.server.command.builder.Command +import net.minestom.server.command.builder.CommandContext +import net.minestom.server.command.builder.arguments.Argument +import net.minestom.server.command.builder.arguments.ArgumentType +import net.minestom.server.command.builder.suggestion.SuggestionEntry +import net.minestom.server.entity.Player +import java.util.* + +class IFExampleCommand(private val viewFrame: ViewFrame) : Command("ifexample") { + + private val availableViews = mapOf( + "failing" to Failing::class.java, + "simple-pagination" to SimplePagination::class.java, + "scheduled" to ScheduledView::class.java + ); + + private val arg: Argument = + ArgumentType.String("view").setSuggestionCallback { _, _, suggestion -> + availableViews.keys.forEach { + suggestion.addEntry(SuggestionEntry(it)) + } + } + + init { + addSyntax({ sender, ctx -> onCommand(sender, ctx) }, arg) + } + + private fun onCommand(sender: CommandSender, ctx: CommandContext) { + if (sender !is Player) { + sender.sendMessage("This command can only be executed by players.") + return + } + + val view = availableViews[ctx.get(arg)] + if (view != null) { + sender.sendMessage("Opened view: ${ctx.get(arg)}") + try { + viewFrame.open(view, sender) + } catch (e: Exception) { + e.printStackTrace() + } + } else { + sender.sendMessage("Unknown view: ${ctx.get(arg)}") + sender.sendMessage("Available views: ${availableViews.keys.joinToString(", ")}") + } + } +} \ No newline at end of file diff --git a/examples/minestom/src/main/kotlin/me/devnatan/inventoryframework/runtime/view/Failing.kt b/examples/minestom/src/main/kotlin/me/devnatan/inventoryframework/runtime/view/Failing.kt new file mode 100644 index 000000000..26607c207 --- /dev/null +++ b/examples/minestom/src/main/kotlin/me/devnatan/inventoryframework/runtime/view/Failing.kt @@ -0,0 +1,44 @@ +package me.devnatan.inventoryframework.runtime.view + +import me.devnatan.inventoryframework.View +import me.devnatan.inventoryframework.ViewConfigBuilder +import me.devnatan.inventoryframework.context.RenderContext +import me.devnatan.inventoryframework.context.SlotClickContext +import me.devnatan.inventoryframework.context.SlotRenderContext +import me.devnatan.inventoryframework.runtime.ExampleUtil.displayItem +import me.devnatan.inventoryframework.state.MutableState +import net.minestom.server.item.Material + +class Failing : View() { + var state: MutableState = mutableState(0) + + override fun onInit(config: ViewConfigBuilder) { + config.size(1) + config.cancelOnClick() + config.title("Failing Inventory") + config.layout(" R C ") + } + + override fun onFirstRender(render: RenderContext) { + render.layoutSlot('R') + .onRender { ctx: SlotRenderContext -> + if (state[ctx] == 0) { + ctx.item = displayItem( + Material.DIAMOND, + "Click me to fail" + ) + } else { + throw IllegalStateException("This item cannot be rendered") + } + } + .onClick { ctx: SlotClickContext -> + state[1] = ctx + ctx.update() + } + + render.layoutSlot('C', displayItem(Material.STONE, "Click me and I will fail")) + .onClick { _ -> + throw IllegalStateException("This is a failing inventory") + } + } +} diff --git a/examples/minestom/src/main/kotlin/me/devnatan/inventoryframework/runtime/view/ScheduledView.kt b/examples/minestom/src/main/kotlin/me/devnatan/inventoryframework/runtime/view/ScheduledView.kt new file mode 100644 index 000000000..b248969c9 --- /dev/null +++ b/examples/minestom/src/main/kotlin/me/devnatan/inventoryframework/runtime/view/ScheduledView.kt @@ -0,0 +1,38 @@ +package me.devnatan.inventoryframework.runtime.view + +import me.devnatan.inventoryframework.View +import me.devnatan.inventoryframework.ViewConfigBuilder +import me.devnatan.inventoryframework.context.Context +import me.devnatan.inventoryframework.context.RenderContext +import me.devnatan.inventoryframework.context.SlotClickContext +import me.devnatan.inventoryframework.runtime.ExampleUtil +import net.minestom.server.item.Material +import java.time.Duration + +class ScheduledView : View() { + + val counter = mutableState(0) + + override fun onInit(config: ViewConfigBuilder) { + config.cancelOnClick() + config.size(3) + config.title("Simple Pagination") + config.layout( + " ", + " C ", + "B ") + config.scheduleUpdate(20) + } + + override fun onFirstRender(render: RenderContext) { + render.layoutSlot('C') + .onRender { + it.item = ExampleUtil.displayItem(Material.STONE, counter.increment(it).toString()) + } + + render.layoutSlot('B', ExampleUtil.displayItem(Material.PAPER, "Back")) + .displayIf(Context::canBack) + .onClick(SlotClickContext::back) + } + +} diff --git a/examples/minestom/src/main/kotlin/me/devnatan/inventoryframework/runtime/view/SimplePagination.kt b/examples/minestom/src/main/kotlin/me/devnatan/inventoryframework/runtime/view/SimplePagination.kt new file mode 100644 index 000000000..658b88da2 --- /dev/null +++ b/examples/minestom/src/main/kotlin/me/devnatan/inventoryframework/runtime/view/SimplePagination.kt @@ -0,0 +1,50 @@ +package me.devnatan.inventoryframework.runtime.view + +import me.devnatan.inventoryframework.View +import me.devnatan.inventoryframework.ViewConfigBuilder +import me.devnatan.inventoryframework.component.MinestomIemComponentBuilder +import me.devnatan.inventoryframework.component.Pagination +import me.devnatan.inventoryframework.component.PaginationValueConsumer +import me.devnatan.inventoryframework.context.Context +import me.devnatan.inventoryframework.context.RenderContext +import me.devnatan.inventoryframework.context.SlotClickContext +import me.devnatan.inventoryframework.runtime.ExampleUtil.displayItem +import me.devnatan.inventoryframework.runtime.ExampleUtil.getRandomItems +import me.devnatan.inventoryframework.state.State +import net.minestom.server.item.ItemStack +import net.minestom.server.item.Material +import java.util.function.BooleanSupplier +import java.util.function.Supplier + +class SimplePagination : View() { + private val state: State = lazyPaginationState( + {_ -> getRandomItems(123).toMutableList() }, + { _: Context, builder: MinestomIemComponentBuilder, index: Int, value: ItemStack -> + builder.withItem(value) + builder.onClick { ctx: SlotClickContext -> + ctx.player.sendMessage( + "You clicked on item $index" + ) + } + }) + + override fun onInit(config: ViewConfigBuilder) { + config.cancelOnClick() + config.size(3) + config.title("Simple Pagination") + config.layout("OOOOOOOOO", "OOOOOOOOO", " P N ") + } + + override fun onFirstRender(render: RenderContext) { + val previousItem = displayItem(Material.ARROW, "Previous") + val nextItem = displayItem(Material.ARROW, "Next") + render.layoutSlot('P', previousItem) + .displayIf({ctx -> state[ctx].canBack() }) + .updateOnStateChange(state) + .onClick { ctx: SlotClickContext -> state[ctx].back() } + render.layoutSlot('N', nextItem) + .displayIf({ctx -> state[ctx].canAdvance()}) + .updateOnStateChange(state) + .onClick { ctx: SlotClickContext -> state[ctx].advance() } + } +} diff --git a/example/.gitignore b/examples/paper/.gitignore similarity index 100% rename from example/.gitignore rename to examples/paper/.gitignore diff --git a/example/build.gradle b/examples/paper/build.gradle similarity index 92% rename from example/build.gradle rename to examples/paper/build.gradle index b3a45b89f..0fc30682e 100644 --- a/example/build.gradle +++ b/examples/paper/build.gradle @@ -5,7 +5,7 @@ plugins { alias(libs.plugins.run.paper) } -apply from: '../library.gradle' +apply from: '../../library.gradle' dependencies { implementation projects.inventoryFrameworkPlatformBukkit @@ -25,6 +25,7 @@ shadowJar { } runServer { + jvmArgs("-Dme.devnatan.inventoryframework.debug=true") minecraftVersion("1.21.3") } diff --git a/example/src/main/java/me/devnatan/inventoryframework/runtime/ExampleUtil.java b/examples/paper/src/main/java/me/devnatan/inventoryframework/runtime/ExampleUtil.java similarity index 100% rename from example/src/main/java/me/devnatan/inventoryframework/runtime/ExampleUtil.java rename to examples/paper/src/main/java/me/devnatan/inventoryframework/runtime/ExampleUtil.java diff --git a/example/src/main/java/me/devnatan/inventoryframework/runtime/SamplePlugin.java b/examples/paper/src/main/java/me/devnatan/inventoryframework/runtime/SamplePlugin.java similarity index 100% rename from example/src/main/java/me/devnatan/inventoryframework/runtime/SamplePlugin.java rename to examples/paper/src/main/java/me/devnatan/inventoryframework/runtime/SamplePlugin.java diff --git a/example/src/main/java/me/devnatan/inventoryframework/runtime/commands/IFExampleCommandExecutor.java b/examples/paper/src/main/java/me/devnatan/inventoryframework/runtime/commands/IFExampleCommandExecutor.java similarity index 100% rename from example/src/main/java/me/devnatan/inventoryframework/runtime/commands/IFExampleCommandExecutor.java rename to examples/paper/src/main/java/me/devnatan/inventoryframework/runtime/commands/IFExampleCommandExecutor.java diff --git a/example/src/main/java/me/devnatan/inventoryframework/runtime/view/AnvilInputSample.java b/examples/paper/src/main/java/me/devnatan/inventoryframework/runtime/view/AnvilInputSample.java similarity index 100% rename from example/src/main/java/me/devnatan/inventoryframework/runtime/view/AnvilInputSample.java rename to examples/paper/src/main/java/me/devnatan/inventoryframework/runtime/view/AnvilInputSample.java diff --git a/example/src/main/java/me/devnatan/inventoryframework/runtime/view/Failing.java b/examples/paper/src/main/java/me/devnatan/inventoryframework/runtime/view/Failing.java similarity index 100% rename from example/src/main/java/me/devnatan/inventoryframework/runtime/view/Failing.java rename to examples/paper/src/main/java/me/devnatan/inventoryframework/runtime/view/Failing.java diff --git a/example/src/main/java/me/devnatan/inventoryframework/runtime/view/SimplePagination.java b/examples/paper/src/main/java/me/devnatan/inventoryframework/runtime/view/SimplePagination.java similarity index 100% rename from example/src/main/java/me/devnatan/inventoryframework/runtime/view/SimplePagination.java rename to examples/paper/src/main/java/me/devnatan/inventoryframework/runtime/view/SimplePagination.java diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a7f08771d..c5f2366f8 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -5,10 +5,12 @@ paperSpigot = "1.20.1-R0.1-SNAPSHOT" junit = "5.10.1" mockito = "4.11.0" adventure-api = "4.14.0" -kotlin = "2.0.21" +kotlin = "2.1.0" plugin-shadowjar = "8.1.1" plugin-spotless = "6.25.0" plugin-bukkit = "0.6.0" +minestom = "87f6524aeb" + [libraries.spigot] module = "org.spigotmc:spigot-api" @@ -42,6 +44,10 @@ version.ref = "mockito" module = "net.kyori:adventure-api" version.ref = "adventure-api" +[libraries.minestom] +module = "net.minestom:minestom-snapshots" +version.ref = "minestom" + [plugins] shadowjar = { id = "com.github.johnrengelman.shadow", version.ref = "plugin-shadowjar" } spotless = { id = "com.diffplug.spotless", version.ref = "plugin-spotless" } diff --git a/inventory-framework-core/src/main/java/me/devnatan/inventoryframework/component/DefaultComponentBuilder.java b/inventory-framework-core/src/main/java/me/devnatan/inventoryframework/component/DefaultComponentBuilder.java index e25ddb843..818ba2c84 100644 --- a/inventory-framework-core/src/main/java/me/devnatan/inventoryframework/component/DefaultComponentBuilder.java +++ b/inventory-framework-core/src/main/java/me/devnatan/inventoryframework/component/DefaultComponentBuilder.java @@ -13,7 +13,7 @@ import org.jetbrains.annotations.NotNull; @SuppressWarnings("unchecked") -abstract class DefaultComponentBuilder, C extends IFContext> +public abstract class DefaultComponentBuilder, C extends IFContext> implements ComponentBuilder { protected Ref reference; diff --git a/inventory-framework-platform-minestom/build.gradle b/inventory-framework-platform-minestom/build.gradle new file mode 100644 index 000000000..4602460bd --- /dev/null +++ b/inventory-framework-platform-minestom/build.gradle @@ -0,0 +1,34 @@ +import org.apache.tools.ant.filters.ReplaceTokens + +plugins { + alias(libs.plugins.shadowjar) + alias(libs.plugins.kotlin) +} + +apply from: '../library.gradle' +apply from: '../publish.gradle' + +dependencies { + api projects.inventoryFrameworkPlatform + compileOnly libs.minestom + testCompileOnly libs.minestom + testImplementation projects.inventoryFrameworkApi + testImplementation projects.inventoryFrameworkTest +} + +shadowJar { + archiveBaseName.set('inventory-framework') + archiveAppendix.set('bukkit') + + dependencies { + include(project(":inventory-framework-platform")) + } +} + + +java { + targetCompatibility = JavaVersion.VERSION_21 + toolchain { + languageVersion.set(JavaLanguageVersion.of(21)) + } +} \ No newline at end of file diff --git a/inventory-framework-platform-minestom/src/main/kotlin/me/devnatan/inventoryframework/IFInventoryListener.kt b/inventory-framework-platform-minestom/src/main/kotlin/me/devnatan/inventoryframework/IFInventoryListener.kt new file mode 100644 index 000000000..dc135b0e3 --- /dev/null +++ b/inventory-framework-platform-minestom/src/main/kotlin/me/devnatan/inventoryframework/IFInventoryListener.kt @@ -0,0 +1,91 @@ +package me.devnatan.inventoryframework + +import me.devnatan.inventoryframework.context.IFCloseContext +import me.devnatan.inventoryframework.context.IFContext +import me.devnatan.inventoryframework.context.IFRenderContext +import me.devnatan.inventoryframework.context.IFSlotClickContext +import me.devnatan.inventoryframework.pipeline.StandardPipelinePhases +import net.minestom.server.entity.Player +import net.minestom.server.event.EventFilter +import net.minestom.server.event.EventNode +import net.minestom.server.event.inventory.InventoryCloseEvent +import net.minestom.server.event.inventory.InventoryPreClickEvent +import net.minestom.server.event.item.PickupItemEvent +import net.minestom.server.event.trait.EntityEvent +import net.minestom.server.inventory.PlayerInventory +import net.minestom.server.inventory.click.ClickType +import kotlin.jvm.optionals.getOrNull + +internal class IFInventoryListener( + private val viewFrame: ViewFrame, + handler: EventNode +) { + + init { + val node = EventNode.type("IF", EventFilter.ENTITY) + { _, e -> e is Player && viewFrame.getViewer(e) != null } + .setPriority(10) + .addListener(PickupItemEvent::class.java, this::onItemPickup) + .addListener(InventoryPreClickEvent::class.java, this::onInventoryClick) + .addListener(InventoryCloseEvent::class.java, this::onInventoryClose) + handler.addChild(node) + } + + fun onInventoryClick(event: InventoryPreClickEvent) { + if (event.isCancelled) return + + val player = event.player + val viewer = viewFrame.getViewer(player) ?: return + + if (event.clickType == ClickType.DROP) { + val context: IFContext = viewer.activeContext + if (!context.config.isOptionSet(ViewConfig.CANCEL_ON_DROP)) return + + event.isCancelled = context.config.getOptionValue(ViewConfig.CANCEL_ON_DROP) + return + } + if (event.clickType == ClickType.LEFT_DRAGGING || event.clickType == ClickType.RIGHT_DRAGGING) { + val context: IFContext = viewer.activeContext + if (!context.config.isOptionSet(ViewConfig.CANCEL_ON_DRAG)) return + + event.isCancelled = context.config.getOptionValue(ViewConfig.CANCEL_ON_DRAG) + return + } + + val context: IFRenderContext = viewer.activeContext + val clickedComponent = context.getComponentsAt(event.slot).stream() + .filter { obj: me.devnatan.inventoryframework.component.Component -> obj.isVisible } + .findFirst() + .getOrNull() + val clickedContainer = if (event.inventory is PlayerInventory) + viewer.selfContainer + else + context.getContainer() + + val root: RootView = context.getRoot() + val clickContext: IFSlotClickContext = root.elementFactory + .createSlotClickContext(event.slot, viewer, clickedContainer, clickedComponent, event, false) + + root.pipeline.execute(StandardPipelinePhases.CLICK, clickContext) + } + + fun onInventoryClose(event: InventoryCloseEvent) { + val player: Player = event.player + val viewer = viewFrame.getViewer(player) ?: return + + val context: IFRenderContext = viewer.activeContext + val root: RootView = context.getRoot() + val closeContext: IFCloseContext = root.elementFactory.createCloseContext(viewer, context) + + root.pipeline.execute(StandardPipelinePhases.CLOSE, closeContext) + } + + fun onItemPickup(event: PickupItemEvent) { + val viewer = viewFrame.getViewer(event.entity as Player) ?: return + + val context: IFContext = viewer.activeContext + if (!context.getConfig().isOptionSet(ViewConfig.CANCEL_ON_PICKUP)) return + + event.isCancelled = context.getConfig().getOptionValue(ViewConfig.CANCEL_ON_PICKUP) + } +} diff --git a/inventory-framework-platform-minestom/src/main/kotlin/me/devnatan/inventoryframework/MinestomViewContainer.kt b/inventory-framework-platform-minestom/src/main/kotlin/me/devnatan/inventoryframework/MinestomViewContainer.kt new file mode 100644 index 000000000..d6612b9dd --- /dev/null +++ b/inventory-framework-platform-minestom/src/main/kotlin/me/devnatan/inventoryframework/MinestomViewContainer.kt @@ -0,0 +1,158 @@ +package me.devnatan.inventoryframework + +import net.kyori.adventure.text.Component +import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer +import net.minestom.server.entity.Player +import net.minestom.server.inventory.Inventory +import net.minestom.server.inventory.InventoryType +import net.minestom.server.inventory.PlayerInventory +import net.minestom.server.item.ItemStack +import java.util.* + +class MinestomViewContainer( + private val inventory: Inventory, shared: Boolean, private val type: ViewType, + private val proxied: Boolean +) : + ViewContainer { + val isShared: Boolean = shared + + fun getInventory(): Inventory { + return inventory + } + + override fun isProxied(): Boolean { + return proxied + } + + override fun getTitle(): String { + val diffTitle: Boolean = inventory.viewers.stream() + .map { player -> + (player.openInventory as? Inventory)?.title + ?.let { PlainTextComponentSerializer.plainText().serialize(it) } ?: "" + } + .distinct() + .findAny() + .isPresent + + check(!(diffTitle && isShared)) { "Cannot get unique title of shared inventory" } + val openInventory = inventory.viewers.first().openInventory + return (openInventory as? Inventory)?.title + ?.let { PlainTextComponentSerializer.plainText().serialize(it) } ?: "" + } + + override fun getTitle(viewer: Viewer): String { + return ((viewer as MinestomViewer).player.openInventory as? Inventory)?.title + ?.let { PlainTextComponentSerializer.plainText().serialize(it) } ?: "" + } + + override fun getType(): ViewType { + return type + } + + override fun getRowsCount(): Int { + return size / columnsCount + } + + override fun getColumnsCount(): Int { + return type.columns + } + + override fun renderItem(slot: Int, item: Any) { + requireSupportedItem(item) + inventory.setItemStack(slot, item as ItemStack) + } + + override fun removeItem(slot: Int) { + inventory.setItemStack(slot, ItemStack.AIR) + } + + override fun matchesItem(slot: Int, item: Any?, exactly: Boolean): Boolean { + requireSupportedItem(item) + val target: ItemStack = inventory.getItemStack(slot) ?: return item == null + if (item is ItemStack) return if (exactly) target == item else target.isSimilar(item as ItemStack) + + return false + } + + override fun isSupportedItem(item: Any?): Boolean { + return item == null || item is ItemStack + } + + private fun requireSupportedItem(item: Any?) { + if (isSupportedItem(item)) return + + throw IllegalStateException( + "Unsupported item type: " + item!!.javaClass.name + ) + } + + override fun hasItem(slot: Int): Boolean { + return !inventory.getItemStack(slot).isAir + } + + override fun getSize(): Int { + return inventory.size + } + + override fun getSlotsCount(): Int { + return size - 1 + } + + override fun getFirstSlot(): Int { + return 0 + } + + override fun getLastSlot(): Int { + val resultSlots = getType().resultSlots + var lastSlot = slotsCount + if (resultSlots != null) { + for (resultSlot in resultSlots) { + if (resultSlot == lastSlot) lastSlot-- + } + } + + return lastSlot + } + + override fun changeTitle(title: String?, target: Viewer) { + changeTitle(title?.let { Component.text(it) } ?: Component.empty(), (target as MinestomViewer).player) + } + + fun changeTitle(title: Component, target: Player) { + val open: Inventory = target.openInventory as? Inventory ?: return + if (inventory.inventoryType == InventoryType.CRAFTING || inventory.inventoryType == InventoryType.CRAFTER_3X3) return + open.setTitle(title) + } + + override fun isEntityContainer(): Boolean { + return inventory is PlayerInventory + } + + override fun open(viewer: Viewer) { + viewer.open(this) + } + + override fun close() { + inventory.viewers.forEach(Player::closeInventory) + } + + override fun close(viewer: Viewer) { + viewer.close() + } + + override fun equals(o: Any?): Boolean { + if (this === o) return true + if (o == null || javaClass != o.javaClass) return false + val that = o as MinestomViewContainer + return isShared == that.isShared && inventory == that.inventory + && getType() == that.getType() + } + + override fun hashCode(): Int { + return Objects.hash(inventory, isShared, getType()) + } + + override fun toString(): String { + return "BukkitViewContainer{" + "inventory=" + inventory + ", shared=" + isShared + ", type=" + type + '}' + } +} diff --git a/inventory-framework-platform-minestom/src/main/kotlin/me/devnatan/inventoryframework/MinestomViewer.kt b/inventory-framework-platform-minestom/src/main/kotlin/me/devnatan/inventoryframework/MinestomViewer.kt new file mode 100644 index 000000000..bd7d47133 --- /dev/null +++ b/inventory-framework-platform-minestom/src/main/kotlin/me/devnatan/inventoryframework/MinestomViewer.kt @@ -0,0 +1,100 @@ +package me.devnatan.inventoryframework + +import me.devnatan.inventoryframework.context.IFRenderContext +import net.minestom.server.entity.Player +import net.minestom.server.inventory.Inventory +import java.util.* + +class MinestomViewer(val player: Player, private var activeContext: IFRenderContext?) : Viewer { + private var selfContainer: ViewContainer? = null + private val previousContexts: Deque = LinkedList() + private var lastInteractionInMillis: Long = 0 + private var transitioning = false + + + override fun getActiveContext(): IFRenderContext { + return activeContext!! + } + + override fun setActiveContext(context: IFRenderContext) { + this.activeContext = context + } + + override fun getId(): String { + return player.uuid.toString() + } + + override fun open(container: ViewContainer) { + player.openInventory((container as MinestomViewContainer).getInventory()) + } + + override fun close() { + player.closeInventory() + } + + override fun getSelfContainer(): ViewContainer { + if (selfContainer == null) selfContainer = MinestomViewContainer( + player.openInventory as Inventory, getActiveContext().isShared(), ViewType.PLAYER, false + ) + return selfContainer!! + } + + override fun getLastInteractionInMillis(): Long { + return lastInteractionInMillis + } + + override fun setLastInteractionInMillis(lastInteractionInMillis: Long) { + this.lastInteractionInMillis = lastInteractionInMillis + } + + override fun isBlockedByInteractionDelay(): Boolean { + val configuredDelay: Long = activeContext?.getConfig()?.interactionDelayInMillis ?: return false + if (configuredDelay <= 0 || getLastInteractionInMillis() <= 0) return false + + return getLastInteractionInMillis() + configuredDelay >= System.currentTimeMillis() + } + + override fun isTransitioning(): Boolean { + return transitioning + } + + override fun setTransitioning(transitioning: Boolean) { + this.transitioning = transitioning + } + + override fun getPreviousContext(): IFRenderContext? { + return previousContexts.peekLast() + } + + override fun setPreviousContext(previousContext: IFRenderContext) { + previousContexts.add(previousContext) + } + + override fun unsetPreviousContext() { + previousContexts.pollLast() + } + + override fun getPlatformInstance(): Any { + return player + } + + override fun equals(o: Any?): Boolean { + if (this === o) return true + if (o == null || javaClass != o.javaClass) return false + val that = o as MinestomViewer + return player == that.player + } + + override fun hashCode(): Int { + return player.hashCode() + } + + override fun toString(): String { + return ("BukkitViewer{" + + "player=" + player + + ", selfContainer=" + selfContainer + + ", lastInteractionInMillis=" + lastInteractionInMillis + + ", isTransitioning=" + transitioning + + "}") + } +} diff --git a/inventory-framework-platform-minestom/src/main/kotlin/me/devnatan/inventoryframework/View.kt b/inventory-framework-platform-minestom/src/main/kotlin/me/devnatan/inventoryframework/View.kt new file mode 100644 index 000000000..f9fea6122 --- /dev/null +++ b/inventory-framework-platform-minestom/src/main/kotlin/me/devnatan/inventoryframework/View.kt @@ -0,0 +1,28 @@ +package me.devnatan.inventoryframework + +import me.devnatan.inventoryframework.component.MinestomIemComponentBuilder +import me.devnatan.inventoryframework.context.* +import me.devnatan.inventoryframework.pipeline.* +import net.minestom.server.MinecraftServer +import net.minestom.server.entity.Player +import org.jetbrains.annotations.ApiStatus.OverrideOnly + +/** + * Bukkit platform [PlatformView] implementation. + */ +@OverrideOnly +open class View : + PlatformView() { + + public override fun registerPlatformInterceptors() { + val pipeline: Pipeline = pipeline + pipeline.intercept(StandardPipelinePhases.CLICK, ItemClickInterceptor()) + pipeline.intercept(StandardPipelinePhases.CLICK, GlobalClickInterceptor()) + pipeline.intercept(StandardPipelinePhases.CLICK, ItemCloseOnClickInterceptor()) + pipeline.intercept(StandardPipelinePhases.CLOSE, CancelledCloseInterceptor()) + } + + override fun nextTick(task: Runnable) { + MinecraftServer.getSchedulerManager().scheduleNextTick(task) + } +} diff --git a/inventory-framework-platform-minestom/src/main/kotlin/me/devnatan/inventoryframework/ViewFrame.kt b/inventory-framework-platform-minestom/src/main/kotlin/me/devnatan/inventoryframework/ViewFrame.kt new file mode 100644 index 000000000..13848e3ca --- /dev/null +++ b/inventory-framework-platform-minestom/src/main/kotlin/me/devnatan/inventoryframework/ViewFrame.kt @@ -0,0 +1,295 @@ +package me.devnatan.inventoryframework + +import me.devnatan.inventoryframework.context.EndlessContextInfo +import me.devnatan.inventoryframework.feature.DefaultFeatureInstaller +import me.devnatan.inventoryframework.feature.Feature +import me.devnatan.inventoryframework.feature.FeatureInstaller +import me.devnatan.inventoryframework.internal.MinestomElementFactory +import me.devnatan.inventoryframework.internal.PlatformUtils +import net.minestom.server.entity.Player +import net.minestom.server.event.EventNode +import net.minestom.server.event.trait.EntityEvent +import org.jetbrains.annotations.ApiStatus +import org.jetbrains.annotations.ApiStatus.Experimental +import java.util.function.UnaryOperator + +class ViewFrame private constructor(private val parentNode: EventNode) : IFViewFrame() { + + private val featureInstaller: FeatureInstaller = DefaultFeatureInstaller( + this + ) + + + // region Opening + /** + * Opens a view to a player. + * + * @param viewClass The target view to be opened. + * @param player The player that the view will be open to. + * @return The id of the newly created [IFContext]. + */ + fun open(viewClass: Class, player: Player): String { + return open(viewClass, player, null) + } + + /** + * Opens a view to a player with initial data. + * + * @param viewClass The target view to be opened. + * @param player The player that the view will be open to. + * @param initialData The initial data. + * @return The id of the newly created [IFContext]. + */ + fun open(viewClass: Class, player: Player, initialData: Any?): String { + return open(viewClass, listOf(player), initialData) + } + + /** + * Opens a view to more than one player. + * + * + * These players will see the same inventory and share the same context. + * + * + * *** This API is experimental and is not subject to the general compatibility guarantees + * such API may be changed or may be removed completely in any further release. *** + * + * @param viewClass The target view to be opened. + * @param players The players that the view will be open to. + * @return The id of the newly created [IFContext]. + */ + @Experimental + fun open(viewClass: Class, players: Collection): String { + return open(viewClass, players, null) + } + + /** + * Opens a view to more than one player with initial data. + * + * + * These players will see the same inventory and share the same context. + * + * + * *** This API is experimental and is not subject to the general compatibility guarantees + * such API may be changed or may be removed completely in any further release. *** + * + * @param viewClass The target view to be opened. + * @param players The players that the view will be open to. + * @param initialData The initial data. + * @return The id of the newly created [IFContext]. + */ + @Experimental + fun open( + viewClass: Class, + players: Collection, + initialData: Any? + ): String { + return internalOpen(viewClass, players, initialData) + } + + /** + * Opens an already active context to a player. + * + * + * *** This API is experimental and is not subject to the general compatibility guarantees + * such API may be changed or may be removed completely in any further release. *** + * + * @param contextId The id of the context. + * @param player Who the context will be open to. + */ + @Experimental + fun openActive( + viewClass: Class, contextId: String, player: Player + ) { + openActive(viewClass, contextId, player, null) + } + + /** + * Opens an already active context to a player. + * + * + * *** This API is experimental and is not subject to the general compatibility guarantees + * such API may be changed or may be removed completely in any further release. *** + * + * @param contextId The id of the context. + * @param player Who the context will be open to. + * @param initialData Initial data to pass to [PlatformView.onViewerAdded]. + */ + @Experimental + fun openActive( + viewClass: Class, + contextId: String, + player: Player, + initialData: Any? + ) { + internalOpenActiveContext(viewClass, contextId, player, initialData) + } + + /** + * Opens an already active context to a player. + * + * + * *** This API is experimental and is not subject to the general compatibility guarantees + * such API may be changed or may be removed completely in any further release. *** + * + * @param endlessContextInfo The id of the context. + * @param player Who the context will be open to. + */ + @Experimental + fun openEndless(endlessContextInfo: EndlessContextInfo, player: Player) { + openEndless(endlessContextInfo, player, null) + } + + /** + * Opens an already active context to a player. + * + * + * *** This API is experimental and is not subject to the general compatibility guarantees + * such API may be changed or may be removed completely in any further release. *** + * + * @param endlessContextInfo The id of the context. + * @param player Who the context will be open to. + * @param initialData Initial data to pass to [PlatformView.onViewerAdded]. + */ + @Experimental + fun openEndless( + endlessContextInfo: EndlessContextInfo, player: Player, initialData: Any? + ) { + openActive( + endlessContextInfo.view.javaClass as Class, + endlessContextInfo.contextId, + player, + initialData + ) + } + + // endregion + override fun register(): ViewFrame { + check(!isRegistered) { "This view frame is already registered" } + + isRegistered = true + PlatformUtils.setFactory(MinestomElementFactory()) + pipeline.execute(FRAME_REGISTERED, this) + initializeViews() + IFInventoryListener(this, parentNode) + return this + } + + override fun unregister() { + if (!isRegistered) return + + // Locks new operations while unregistering + isRegistered = false + + val iterator: MutableIterator = registeredViews.values.iterator() + while (iterator.hasNext()) { + val view = iterator.next() + try { + view.closeForEveryone() + } catch (ignored: RuntimeException) { + } + iterator.remove() + } + pipeline.execute(FRAME_UNREGISTERED, this) + } + + private fun initializeViews() { + for ((_, view) in getRegisteredViews()) { + try { + view.internalInitialization(this) + view.isInitialized = true + } catch (exception: RuntimeException) { + view.isInitialized = false + LOGGER.severe( + String.format( + "An error occurred while enabling view %s: %s", + view.javaClass.name, exception + ) + ) + exception.printStackTrace() + } + } + } + + // endregion + /** + * *** This is an internal inventory-framework API that should not be used from outside of + * this library. No compatibility guarantees are provided. *** + */ + @ApiStatus.Internal + fun getViewer(player: Player): Viewer? { + return viewerById[player.uuid.toString()] + } + + /** + * Installs a feature. + * + * @param feature The feature to be installed. + * @param configure The feature configuration. + * @param The feature configuration type. + * @param The feature value instance type. + * @return An instance of the installed feature. + */ + fun install( + feature: Feature, configure: UnaryOperator + ): ViewFrame { + featureInstaller.install(feature, configure) + IFDebug.debug("Feature %s installed", feature.name()) + return this + } + + /** + * Installs a feature with no specific configuration. + * + * @param feature The feature to be installed. + * @return This view frame. + */ + fun install(feature: Feature<*, *, ViewFrame>): ViewFrame { + install(feature, UnaryOperator.identity()) + return this + } + + /** + * Disables bStats metrics tracking. + * + * + * InventoryFramework use bStats metrics to obtain some information from servers that use it as + * a library, such as: number of players, version, software, etc. + * + * + * **No sensitive information is tracked.** + * + * @return This view frame. + */ + fun disableMetrics(): ViewFrame { + System.setProperty(BSTATS_SYSTEM_PROP, java.lang.Boolean.FALSE.toString()) + return this + } + + companion object { + private const val BSTATS_SYSTEM_PROP = "inventory-framework.enable-bstats" + private const val BSTATS_PROJECT_ID = 15518 + private const val PLUGIN_FQN = "me.devnatan.inventoryframework.runtime.InventoryFramework" + + private const val RELOCATION_MESSAGE = + ("Inventory Framework is running as a shaded non-relocated library. It's extremely recommended that " + + "you relocate the library package. Learn more about on docs: " + + "https://github.com/DevNatan/inventory-framework/wiki/Installation#preventing-library-conflicts") + + init { + PlatformUtils.setFactory(MinestomElementFactory()) + } + + /** + * Creates a new ViewFrame. + * + * @param owner The plugin that owns this view frame. + * @return A new ViewFrame instance. + */ + fun create(parentNode: EventNode): ViewFrame { + return ViewFrame(parentNode) + } + + private val LOGGER = java.util.logging.Logger.getLogger("IF") + } +} diff --git a/inventory-framework-platform-minestom/src/main/kotlin/me/devnatan/inventoryframework/component/MinestomIemComponentBuilder.kt b/inventory-framework-platform-minestom/src/main/kotlin/me/devnatan/inventoryframework/component/MinestomIemComponentBuilder.kt new file mode 100644 index 000000000..7171737c5 --- /dev/null +++ b/inventory-framework-platform-minestom/src/main/kotlin/me/devnatan/inventoryframework/component/MinestomIemComponentBuilder.kt @@ -0,0 +1,226 @@ +package me.devnatan.inventoryframework.component + +import me.devnatan.inventoryframework.Ref +import me.devnatan.inventoryframework.ViewContainer +import me.devnatan.inventoryframework.VirtualView +import me.devnatan.inventoryframework.context.Context +import me.devnatan.inventoryframework.context.IFRenderContext +import me.devnatan.inventoryframework.context.IFSlotClickContext +import me.devnatan.inventoryframework.context.IFSlotContext +import me.devnatan.inventoryframework.context.IFSlotRenderContext +import me.devnatan.inventoryframework.context.SlotClickContext +import me.devnatan.inventoryframework.context.SlotContext +import me.devnatan.inventoryframework.context.SlotRenderContext +import me.devnatan.inventoryframework.state.State +import me.devnatan.inventoryframework.utils.SlotConverter +import net.minestom.server.item.ItemStack +import java.util.function.Consumer +import java.util.function.Predicate +import java.util.function.Supplier + +class MinestomIemComponentBuilder private constructor( + private val root: VirtualView, + slot: Int, + item: ItemStack?, + renderHandler: Consumer?, + clickHandler: Consumer?, + updateHandler: Consumer?, + reference: Ref?, + data: Map, + cancelOnClick: Boolean, + closeOnClick: Boolean, + updateOnClick: Boolean, + watchingStates: Set>, + isManagedExternally: Boolean, + displayCondition: Predicate? +) : DefaultComponentBuilder( + reference, + data, + cancelOnClick, + closeOnClick, + updateOnClick, + watchingStates, + isManagedExternally, + displayCondition + ), ItemComponentBuilder, + ComponentFactory { + private var slot: Int + private var item: ItemStack? + private var renderHandler: Consumer? + private var clickHandler: Consumer? + private var updateHandler: Consumer? + + constructor(root: VirtualView) : this( + root, + -1, + null, + null, + null, + null, + null, + HashMap(), + false, + false, + false, + LinkedHashSet>(), + false, + null + ) + + init { + this.slot = slot + this.item = item + this.renderHandler = renderHandler + this.clickHandler = clickHandler + this.updateHandler = updateHandler + } + + override fun toString(): String { + return ("BukkitItemComponentBuilder{" + + "slot=" + slot + + ", item=" + item + + ", renderHandler=" + renderHandler + + ", clickHandler=" + clickHandler + + ", updateHandler=" + updateHandler + + "} " + super.toString()) + } + + override fun isContainedWithin(position: Int): Boolean { + return position == slot + } + + /** + * {@inheritDoc} + */ + override fun withSlot(slot: Int): MinestomIemComponentBuilder { + this.slot = slot + return this + } + + override fun withSlot(row: Int, column: Int): MinestomIemComponentBuilder { + val container: ViewContainer = (root as IFRenderContext).getContainer() + return withSlot(SlotConverter.convertSlot(row, column, container.getRowsCount(), container.getColumnsCount())) + } + + /** + * Defines the item that will be used as fallback for rendering in the slot where this item is + * positioned. The fallback item is always static. + * + * @param item The new fallback item stack. + * @return This item builder. + */ + fun withItem(item: ItemStack?): MinestomIemComponentBuilder { + this.item = item + return this + } + + /** + * Called when the item is rendered. + * + * + * This handler is called every time the item or the view that owns it is updated. + * + * @param renderHandler The render handler. + * @return This item builder. + */ + @Suppress("UNCHECKED_CAST") + fun onRender(renderHandler: Consumer?): MinestomIemComponentBuilder { + this.renderHandler = renderHandler as? Consumer + return this + } + + /** + * Dynamic rendering of a specific item. + * + * + * This handler is called every time the item or the view that owns it is updated. + * + * @param renderFactory The render handler. + * @return This item builder. + */ + fun renderWith(renderFactory: Supplier): MinestomIemComponentBuilder { + return onRender { render: SlotRenderContext -> + render.item = renderFactory.get() + } + } + + /** + * Called when a player clicks on the item. + * + * + * This handler works on any container that the actor has access to and only works if the + * interaction has not been cancelled. + * + * @param clickHandler The click handler. + * @return This item builder. + */ + @Suppress("UNCHECKED_CAST") + fun onClick(clickHandler: Consumer?): MinestomIemComponentBuilder { + this.clickHandler = clickHandler as? Consumer + return this + } + + /** + * Called when a player clicks on the item. + * + * + * This handler works on any container that the actor has access to and only works if the + * interaction has not been cancelled. + * + * @param clickHandler The click handler. + * @return This item builder. + */ + fun onClick(clickHandler: Runnable?): MinestomIemComponentBuilder { + return onClick(if (clickHandler == null) null else Consumer { `$`: SlotClickContext? -> clickHandler.run() }) + } + + /** + * Called when the item is updated. + * + * @param updateHandler The update handler. + * @return This item builder. + */ + @Suppress("UNCHECKED_CAST") + fun onUpdate(updateHandler: Consumer?): MinestomIemComponentBuilder { + this.updateHandler = updateHandler as? Consumer + return this + } + + override fun create(): Component { + return ItemComponent( + root, + slot, + item, + cancelOnClick, + closeOnClick, + displayCondition, + renderHandler, + updateHandler, + clickHandler, + watchingStates, + isManagedExternally, + updateOnClick, + false, + reference + ) + } + + override fun copy(): MinestomIemComponentBuilder { + return MinestomIemComponentBuilder( + root, + slot, + item, + renderHandler, + clickHandler, + updateHandler, + reference, + data, + cancelOnClick, + closeOnClick, + updateOnClick, + watchingStates, + isManagedExternally, + displayCondition + ) + } +} diff --git a/inventory-framework-platform-minestom/src/main/kotlin/me/devnatan/inventoryframework/context/CloseContext.kt b/inventory-framework-platform-minestom/src/main/kotlin/me/devnatan/inventoryframework/context/CloseContext.kt new file mode 100644 index 000000000..e199be339 --- /dev/null +++ b/inventory-framework-platform-minestom/src/main/kotlin/me/devnatan/inventoryframework/context/CloseContext.kt @@ -0,0 +1,112 @@ +package me.devnatan.inventoryframework.context + +import me.devnatan.inventoryframework.MinestomViewer +import me.devnatan.inventoryframework.View +import me.devnatan.inventoryframework.ViewConfig +import me.devnatan.inventoryframework.ViewContainer +import me.devnatan.inventoryframework.Viewer +import me.devnatan.inventoryframework.state.State +import me.devnatan.inventoryframework.state.StateValue +import me.devnatan.inventoryframework.state.StateWatcher +import net.kyori.adventure.text.Component +import net.minestom.server.entity.Player +import org.jetbrains.annotations.ApiStatus +import org.jetbrains.annotations.UnmodifiableView +import java.util.* + +class CloseContext @ApiStatus.Internal constructor(subject: Viewer, private val parent: IFRenderContext) : + PlatformConfinedContext(), + IFCloseContext, Context { + private val subject: Viewer = subject + override val player: Player = (subject as MinestomViewer).player + + private var cancelled = false + + override val allPlayers: List + get() = getParent().allPlayers + + override fun updateTitleForPlayer(title: Component, player: Player) { + getParent().updateTitleForPlayer(title, player) + } + + override fun resetTitleForPlayer(player: Player) { + getParent().resetTitleForPlayer(player) + } + + override fun isCancelled(): Boolean { + return cancelled + } + + override fun setCancelled(cancelled: Boolean) { + this.cancelled = cancelled + } + + override fun getViewer(): Viewer { + return subject + } + + override fun getParent(): RenderContext { + return parent as RenderContext + } + + override fun getId(): UUID { + return getParent().id + } + + override fun getConfig(): ViewConfig { + return getParent().config + } + + override fun getContainer(): ViewContainer { + return getParent().container + } + + override fun getRoot(): View { + return getParent().getRoot() + } + + override fun getInitialData(): Any { + return getParent().initialData + } + + override fun setInitialData(initialData: Any) { + getParent().initialData = initialData + } + + override fun getStateValues(): @UnmodifiableView MutableMap? { + return getParent().stateValues + } + + override fun initializeState(id: Long, value: StateValue) { + getParent().initializeState(id, value) + } + + override fun watchState(id: Long, listener: StateWatcher) { + getParent().watchState(id, listener) + } + + override fun getRawStateValue(state: State<*>?): Any { + return getParent().getRawStateValue(state) + } + + override fun getInternalStateValue(state: State<*>): StateValue { + return getParent().getInternalStateValue(state) + } + + override fun getUninitializedStateValue(stateId: Long): StateValue { + return getParent().getUninitializedStateValue(stateId) + } + + override fun updateState(id: Long, value: Any) { + getParent().updateState(id, value) + } + + override fun toString(): String { + return ("CloseContext{" + "subject=" + + subject + ", player=" + + player + ", parent=" + + parent + ", cancelled=" + + cancelled + "} " + + super.toString()) + } +} diff --git a/inventory-framework-platform-minestom/src/main/kotlin/me/devnatan/inventoryframework/context/Context.kt b/inventory-framework-platform-minestom/src/main/kotlin/me/devnatan/inventoryframework/context/Context.kt new file mode 100644 index 000000000..4f7f6802b --- /dev/null +++ b/inventory-framework-platform-minestom/src/main/kotlin/me/devnatan/inventoryframework/context/Context.kt @@ -0,0 +1,56 @@ +package me.devnatan.inventoryframework.context + +import net.kyori.adventure.text.Component +import net.minestom.server.entity.Player +import org.jetbrains.annotations.ApiStatus.Experimental + +interface Context : IFConfinedContext { + /** + * The player for the current interaction context. + * + * + * Contexts can be shared and contain multiple viewers, this method will + * always return the player for the current event. + * + * @return A player in this interaction context. + */ + val player: Player + + @get:Experimental + val allPlayers: List + + /** + * Updates the container title for a specific player. + * + * + * This should not be used before the container is opened, if you need to set the __initial + * title__ use [IFOpenContext.modifyConfig] on open handler instead. + * + * + * This method is version dependant, so it may be that your server version is not yet + * supported, if you try to use this method and fail (can fail silently), report it to the + * library developers to add support to your version. + * + * + * *** This API is experimental and is not subject to the general compatibility guarantees + * such API may be changed or may be removed completely in any further release. *** + * + * @param title The new container title. + * @param player The player to update the title. + */ + @Experimental + fun updateTitleForPlayer(title: Component, player: Player) + + /** + * Resets the container title only for the player current scope of execution to the initially + * defined title. Must be used after [.updateTitleForPlayer] to take effect. + * + * + * *** This API is experimental and is not subject to the general compatibility guarantees + * such API may be changed or may be removed completely in any further release. *** + * + * @param player The player to reset the title. + */ + @Experimental + fun resetTitleForPlayer(player: Player) +} diff --git a/inventory-framework-platform-minestom/src/main/kotlin/me/devnatan/inventoryframework/context/OpenContext.kt b/inventory-framework-platform-minestom/src/main/kotlin/me/devnatan/inventoryframework/context/OpenContext.kt new file mode 100644 index 000000000..1881f96cc --- /dev/null +++ b/inventory-framework-platform-minestom/src/main/kotlin/me/devnatan/inventoryframework/context/OpenContext.kt @@ -0,0 +1,141 @@ +package me.devnatan.inventoryframework.context + +import me.devnatan.inventoryframework.* +import net.kyori.adventure.text.Component +import net.minestom.server.entity.Player +import org.jetbrains.annotations.ApiStatus +import java.util.* +import java.util.concurrent.CompletableFuture + +/** + * Creates a new open context instance. + * + * + * *** This is an internal inventory-framework API that should not be used from outside of + * this library. No compatibility guarantees are provided. *** + * + * @param root Root view that will be owner of the upcoming render context. + * @param subject The viewer that is opening the view. + * @param viewers Who'll be the viewers of this context, if this parameter is provided it + * means that this context is a shared context. + * Must be provided even in non-shared context cases. + * @param initialData Initial data provided by the user. + */ +class OpenContext @ApiStatus.Internal constructor( + private val root: View, + private val subject: Viewer?, + private val viewers: Map, + private var initialData: Any? +) : PlatformConfinedContext(), IFOpenContext, Context { + private var container: ViewContainer? = null + + // --- Inherited --- + private val id: UUID = UUID.randomUUID() + + // --- User Provided --- + private var waitTask: CompletableFuture? = null + private var inheritedConfigBuilder: ViewConfigBuilder? = null + + // --- Properties --- + /** + * The player that's currently opening the view. + * + * @return The player that is opening the view. + * @throws UnsupportedOperationInSharedContextException If this context [is shared][.isShared]. + */ + override val player: Player + get() { + tryThrowDoNotWorkWithSharedContext("getAllPlayers()") + return field + } + private var cancelled = false + + init { + this.initialData = initialData + this.player = (subject as MinestomViewer).player + } + + override val allPlayers: List + get() = getViewers().stream() + .map { viewer -> (viewer as MinestomViewer).player } + .toList(); + override fun updateTitleForPlayer(title: Component, player: Player) { + tryThrowDoNotWorkWithSharedContext() + modifyConfig().title(title) + } + + override fun resetTitleForPlayer(player: Player) { + tryThrowDoNotWorkWithSharedContext() + if (modifiedConfig == null) return + + modifyConfig().title(null) + } + + override fun isCancelled(): Boolean { + return cancelled + } + + override fun setCancelled(cancelled: Boolean) { + this.cancelled = cancelled + } + + override fun getAsyncOpenJob(): CompletableFuture? { + return waitTask + } + + override fun getRoot(): View { + return root + } + + override fun getIndexedViewers(): Map { + return viewers + } + + override fun getId(): UUID { + return id + } + + override fun getInitialData(): Any? { + return initialData + } + + override fun setInitialData(initialData: Any?) { + this.initialData = initialData + } + + override fun waitUntil(task: CompletableFuture) { + this.waitTask = task + } + + override fun getConfig(): ViewConfig { + return if (inheritedConfigBuilder == null) + getRoot().config + else + Objects.requireNonNull(modifiedConfig, "Modified config cannot be null") + } + + override fun getModifiedConfig(): ViewConfig? { + if (inheritedConfigBuilder == null) return null + + return inheritedConfigBuilder!!.build().merge(getRoot().config) + } + + override fun modifyConfig(): ViewConfigBuilder { + if (inheritedConfigBuilder == null) inheritedConfigBuilder = ViewConfigBuilder() + + return inheritedConfigBuilder!! + } + + override fun getViewer(): Viewer? { + tryThrowDoNotWorkWithSharedContext("getViewers()") + return subject + } + + override fun getContainer(): ViewContainer? { + return container + } + + override fun setContainer(container: ViewContainer) { + this.container = container + } +} diff --git a/inventory-framework-platform-minestom/src/main/kotlin/me/devnatan/inventoryframework/context/RenderContext.kt b/inventory-framework-platform-minestom/src/main/kotlin/me/devnatan/inventoryframework/context/RenderContext.kt new file mode 100644 index 000000000..c3d0f8ace --- /dev/null +++ b/inventory-framework-platform-minestom/src/main/kotlin/me/devnatan/inventoryframework/context/RenderContext.kt @@ -0,0 +1,149 @@ +package me.devnatan.inventoryframework.context + +import me.devnatan.inventoryframework.MinestomViewContainer +import me.devnatan.inventoryframework.MinestomViewer +import me.devnatan.inventoryframework.View +import me.devnatan.inventoryframework.ViewConfig +import me.devnatan.inventoryframework.ViewContainer +import me.devnatan.inventoryframework.Viewer +import me.devnatan.inventoryframework.component.MinestomIemComponentBuilder +import net.kyori.adventure.text.Component +import net.minestom.server.entity.Player +import net.minestom.server.item.ItemStack +import org.jetbrains.annotations.ApiStatus +import java.util.* + +class RenderContext @ApiStatus.Internal constructor( + id: UUID, + root: View, + config: ViewConfig, + container: ViewContainer, + viewers: Map, + subject: Viewer, + initialData: Any? +) : + PlatformRenderContext( + id, + root, + config, + container, + viewers, + subject, + initialData + ), + Context { + override val player: Player = (subject as MinestomViewer).player + get() { + tryThrowDoNotWorkWithSharedContext("getAllPlayers") + return field + } + + override fun getRoot(): View { + return root as View + } + + override val allPlayers: List + get() = viewers.stream() + .map { viewer -> (viewer as MinestomViewer).player }.toList() + + + override fun updateTitleForPlayer(title: Component, player: Player) { + (container as MinestomViewContainer).changeTitle(title, player) + } + + override fun resetTitleForPlayer(player: Player) { + (container as MinestomViewContainer).changeTitle(Component.empty(), player) + } + + /** + * Adds an item to a specific slot in the context container. + * + * @param slot The slot in which the item will be positioned. + * @return An item builder to configure the item. + */ + fun slot(slot: Int, item: ItemStack): MinestomIemComponentBuilder { + return slot(slot).withItem(item) + } + + /** + * Adds an item at the specific column and ROW (X, Y) in that context's container. + * + * @param row The row (Y) in which the item will be positioned. + * @param column The column (X) in which the item will be positioned. + * @return An item builder to configure the item. + */ + fun slot(row: Int, column: Int, item: ItemStack?): MinestomIemComponentBuilder { + return slot(row, column).withItem(item) + } + + /** + * Sets an item in the first slot of this context's container. + * + * @param item The item that'll be set. + * @return An item builder to configure the item. + */ + fun firstSlot(item: ItemStack?): MinestomIemComponentBuilder { + return firstSlot().withItem(item) + } + + /** + * Sets an item in the last slot of this context's container. + * + * @param item The item that'll be set. + * @return An item builder to configure the item. + */ + fun lastSlot(item: ItemStack?): MinestomIemComponentBuilder { + return lastSlot().withItem(item) + } + + /** + * Adds an item in the next available slot of this context's container. + * + * @param item The item that'll be added. + * @return An item builder to configure the item. + */ + fun availableSlot(item: ItemStack?): MinestomIemComponentBuilder { + return availableSlot().withItem(item) + } + + /** + * Defines the item that will represent a character provided in the context layout. + * + * @param character The layout character target. + * @param item The item that'll represent the layout character. + * @return An item builder to configure the item. + */ + fun layoutSlot(character: Char, item: ItemStack?): MinestomIemComponentBuilder { + return layoutSlot(character).withItem(item) + } + + /** + * + * *** This API is experimental and is not subject to the general compatibility guarantees + * such API may be changed or may be removed completely in any further release. *** + */ + @ApiStatus.Experimental + fun resultSlot(item: ItemStack?): MinestomIemComponentBuilder { + return resultSlot().withItem(item) + } + + override fun createBuilder(): MinestomIemComponentBuilder { + return MinestomIemComponentBuilder(this) + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || javaClass != other.javaClass) return false + if (!super.equals(other)) return false + val that = other as RenderContext + return player== that.player + } + + override fun hashCode(): Int { + return Objects.hash(super.hashCode(), player) + } + + override fun toString(): String { + return "RenderContext{" + "player=" + player + "} " + super.toString() + } +} diff --git a/inventory-framework-platform-minestom/src/main/kotlin/me/devnatan/inventoryframework/context/SlotClickContext.kt b/inventory-framework-platform-minestom/src/main/kotlin/me/devnatan/inventoryframework/context/SlotClickContext.kt new file mode 100644 index 000000000..ede4d669b --- /dev/null +++ b/inventory-framework-platform-minestom/src/main/kotlin/me/devnatan/inventoryframework/context/SlotClickContext.kt @@ -0,0 +1,122 @@ +package me.devnatan.inventoryframework.context + +import me.devnatan.inventoryframework.RootView +import me.devnatan.inventoryframework.ViewContainer +import me.devnatan.inventoryframework.Viewer +import me.devnatan.inventoryframework.component.Component +import net.minestom.server.entity.Player +import net.minestom.server.event.inventory.InventoryPreClickEvent +import net.minestom.server.inventory.PlayerInventory +import net.minestom.server.inventory.click.ClickType +import net.minestom.server.item.ItemStack +import org.jetbrains.annotations.ApiStatus + +class SlotClickContext @ApiStatus.Internal constructor( + slot: Int, + parent: IFRenderContext, + private val whoClicked: Viewer, + private val clickedContainer: ViewContainer, + private val clickedComponent: Component?, + val clickOrigin: InventoryPreClickEvent, + private val combined: Boolean +): SlotContext(slot, parent), IFSlotClickContext { + private var cancelled = false + + override val player: Player + /** + * The player who clicked on the slot. + */ + get() = clickOrigin.player + + override val item: ItemStack + /** + * The item that was clicked. + */ + get() = clickOrigin.cursorItem + + override fun getComponent(): Component? { + return clickedComponent + } + + override fun getClickedContainer(): ViewContainer { + return clickedContainer + } + + override fun isCancelled(): Boolean { + return cancelled + } + + override fun setCancelled(cancelled: Boolean) { + this.cancelled = cancelled + clickOrigin.isCancelled = cancelled + } + + override fun getPlatformEvent(): Any { + return clickOrigin + } + + override fun getClickedSlot(): Int { + return clickOrigin.slot + } + + override fun isLeftClick(): Boolean { + return clickOrigin.clickType == ClickType.LEFT_CLICK + } + + override fun isRightClick(): Boolean { + return clickOrigin.clickType == ClickType.RIGHT_CLICK + } + + override fun isMiddleClick(): Boolean { + return false + } + + override fun isShiftClick(): Boolean { + val clickType = clickOrigin.clickType + return clickType == ClickType.SHIFT_CLICK + } + + override fun isKeyboardClick(): Boolean { + return clickOrigin.clickType == ClickType.CHANGE_HELD + } + + override fun isOutsideClick(): Boolean { + return clickOrigin.slot < 0; + } + + override fun getClickIdentifier(): String { + return clickOrigin.clickType.name + } + + override fun isOnEntityContainer(): Boolean { + return clickOrigin.inventory is PlayerInventory + } + + override fun getViewer(): Viewer { + return whoClicked + } + + override fun closeForPlayer() { + parent.closeForPlayer() + } + + override fun openForPlayer(other: Class) { + parent.openForPlayer(other) + } + + override fun openForPlayer(other: Class, initialData: Any) { + parent.openForPlayer(other, initialData) + } + + override fun updateTitleForPlayer(title: String) { + parent.updateTitleForPlayer(title) + } + + override fun resetTitleForPlayer() { + parent.resetTitleForPlayer() + } + + override fun isCombined(): Boolean { + return combined + } +} diff --git a/inventory-framework-platform-minestom/src/main/kotlin/me/devnatan/inventoryframework/context/SlotContext.kt b/inventory-framework-platform-minestom/src/main/kotlin/me/devnatan/inventoryframework/context/SlotContext.kt new file mode 100644 index 000000000..8e3633b92 --- /dev/null +++ b/inventory-framework-platform-minestom/src/main/kotlin/me/devnatan/inventoryframework/context/SlotContext.kt @@ -0,0 +1,173 @@ +package me.devnatan.inventoryframework.context + +import me.devnatan.inventoryframework.View +import me.devnatan.inventoryframework.ViewConfig +import me.devnatan.inventoryframework.ViewContainer +import me.devnatan.inventoryframework.Viewer +import me.devnatan.inventoryframework.component.Component +import me.devnatan.inventoryframework.state.State +import me.devnatan.inventoryframework.state.StateValue +import me.devnatan.inventoryframework.state.StateWatcher +import net.minestom.server.entity.Player +import net.minestom.server.item.ItemStack +import org.jetbrains.annotations.ApiStatus +import org.jetbrains.annotations.UnmodifiableView +import java.util.* + +abstract class SlotContext @ApiStatus.Internal protected constructor( + private var slot: Int, + private val parent: IFRenderContext +) : PlatformContext(), + IFSlotContext, Context { + abstract val item: ItemStack + + override fun getParent(): RenderContext { + return parent as RenderContext + } + + override fun getSlot(): Int { + return slot + } + + override fun setSlot(slot: Int) { + this.slot = slot + } + + override fun getIndexedViewers(): Map { + return getParent().indexedViewers + } + + override fun getTitle(): String { + return getParent().title + } + + override fun getComponents(): @UnmodifiableView MutableList { + return getParent().components + } + + override fun getInternalComponents(): List { + return getParent().internalComponents + } + + override fun getComponentsAt(position: Int): List { + return getParent().getComponentsAt(position) + } + + override fun addComponent(component: Component) { + getParent().addComponent(component) + } + + override fun removeComponent(component: Component) { + getParent().removeComponent(component) + } + + override fun renderComponent(component: Component) { + getParent().renderComponent(component) + } + + override fun updateComponent(component: Component, force: Boolean) { + getParent().updateComponent(component, force) + } + + override fun performClickInComponent( + component: Component, + viewer: Viewer, + clickedContainer: ViewContainer, + platformEvent: Any, + clickedSlot: Int, + combined: Boolean + ) { + getParent().performClickInComponent(component, viewer, clickedContainer, platformEvent, clickedSlot, combined) + } + + override fun update() { + getParent().update() + } + + override fun getRawStateValue(state: State<*>?): Any { + return getParent().getRawStateValue(state) + } + + override fun getInternalStateValue(state: State<*>): StateValue { + return getParent().getInternalStateValue(state) + } + + override fun getUninitializedStateValue(stateId: Long): StateValue { + return getParent().getUninitializedStateValue(stateId) + } + + override fun initializeState(id: Long, value: StateValue) { + getParent().initializeState(id, value) + } + + override fun updateState(id: Long, value: Any) { + getParent().updateState(id, value) + } + + override fun watchState(id: Long, listener: StateWatcher) { + getParent().watchState(id, listener) + } + + override fun getId(): UUID { + return getParent().id + } + + override fun getConfig(): ViewConfig { + return getParent().config + } + + override fun getContainer(): ViewContainer { + return getParent().container + } + + override fun getRoot(): View { + return getParent().getRoot() + } + + override fun getInitialData(): Any { + return getParent().initialData + } + + override fun setInitialData(initialData: Any) { + getParent().initialData = initialData + } + + override val allPlayers: List + get() = getParent().allPlayers + + override fun updateTitleForPlayer(title: net.kyori.adventure.text.Component, player: Player) { + getParent().updateTitleForPlayer(title, player) + } + + override fun resetTitleForPlayer(player: Player) { + getParent().resetTitleForPlayer(player) + } + + override fun isActive(): Boolean { + return getParent().isActive + } + + override fun setActive(active: Boolean) { + getParent().isActive = active + } + + override fun isEndless(): Boolean { + return getParent().isEndless + } + + override fun setEndless(endless: Boolean) { + getParent().isEndless = endless + } + + override fun back() { + getParent().back() + } + + override fun back(initialData: Any) { + getParent().back(initialData) + } + + override fun canBack(): Boolean { + return getParent().canBack() + } +} diff --git a/inventory-framework-platform-minestom/src/main/kotlin/me/devnatan/inventoryframework/context/SlotRenderContext.kt b/inventory-framework-platform-minestom/src/main/kotlin/me/devnatan/inventoryframework/context/SlotRenderContext.kt new file mode 100644 index 000000000..d79060df1 --- /dev/null +++ b/inventory-framework-platform-minestom/src/main/kotlin/me/devnatan/inventoryframework/context/SlotRenderContext.kt @@ -0,0 +1,79 @@ +package me.devnatan.inventoryframework.context + +import me.devnatan.inventoryframework.MinestomViewer +import me.devnatan.inventoryframework.RootView +import me.devnatan.inventoryframework.Viewer +import net.minestom.server.entity.Player +import net.minestom.server.item.ItemStack +import org.jetbrains.annotations.ApiStatus +import org.jetbrains.annotations.UnknownNullability + +class SlotRenderContext @ApiStatus.Internal constructor(slot: Int, parent: IFRenderContext, private val viewer: Viewer?) : + SlotContext(slot, parent), IFSlotRenderContext { + override val player: Player = (viewer as MinestomViewer).player + + override var item: ItemStack = ItemStack.AIR + private var cancelled = false + private var changed = false + private var forceUpdate = false + + override fun getResult(): ItemStack { + return item + } + + override fun isCancelled(): Boolean { + return cancelled + } + + override fun setCancelled(cancelled: Boolean) { + this.cancelled = cancelled + } + + override fun clear() { + item = ItemStack.AIR + } + + override fun hasChanged(): Boolean { + return changed + } + + override fun setChanged(changed: Boolean) { + this.changed = changed + } + + override fun isForceUpdate(): Boolean { + return forceUpdate + } + + override fun setForceUpdate(forceUpdate: Boolean) { + this.forceUpdate = forceUpdate + } + + override fun isOnEntityContainer(): Boolean { + return container.isEntityContainer + } + + override fun getViewer(): Viewer? { + return viewer + } + + override fun closeForPlayer() { + parent.closeForPlayer() + } + + override fun openForPlayer(other: Class) { + parent.openForPlayer(other) + } + + override fun openForPlayer(other: Class, initialData: Any) { + parent.openForPlayer(other, initialData) + } + + override fun updateTitleForPlayer(title: String) { + parent.updateTitleForPlayer(title) + } + + override fun resetTitleForPlayer() { + parent.resetTitleForPlayer() + } +} diff --git a/inventory-framework-platform-minestom/src/main/kotlin/me/devnatan/inventoryframework/internal/MinestomElementFactory.kt b/inventory-framework-platform-minestom/src/main/kotlin/me/devnatan/inventoryframework/internal/MinestomElementFactory.kt new file mode 100644 index 000000000..03e9d2c3b --- /dev/null +++ b/inventory-framework-platform-minestom/src/main/kotlin/me/devnatan/inventoryframework/internal/MinestomElementFactory.kt @@ -0,0 +1,155 @@ +package me.devnatan.inventoryframework.internal + +import me.devnatan.inventoryframework.* +import me.devnatan.inventoryframework.component.MinestomIemComponentBuilder +import me.devnatan.inventoryframework.component.Component +import me.devnatan.inventoryframework.component.ComponentBuilder +import me.devnatan.inventoryframework.context.* +import me.devnatan.inventoryframework.logging.Logger +import me.devnatan.inventoryframework.logging.NoopLogger +import net.minestom.server.entity.Player +import net.minestom.server.event.inventory.InventoryPreClickEvent +import net.minestom.server.inventory.Inventory +import net.minestom.server.inventory.InventoryType +import java.util.* +import java.util.function.Function +import java.util.stream.Collectors + +class MinestomElementFactory : ElementFactory() { + private var worksInCurrentPlatform: Boolean? = null + + override fun createUninitializedRoot(): RootView { + return View() + } + + // TODO Test it + override fun createContainer(context: IFContext): ViewContainer { + val config: ViewConfig = context.getConfig() + val finalType: ViewType = config.type ?: defaultType + + val size: Int = finalType.normalize(config.getSize()) + require(!(size != 0 && !finalType.isExtendable())) { + String.format( + ("Only \"%s\" type can have a custom size," + + " \"%s\" always have a size of %d. Remove the parameter that specifies the size" + + " of the container on %s or just set the type explicitly."), + ViewType.CHEST.getIdentifier(), + finalType.getIdentifier(), + finalType.getMaxSize(), + context.getRoot().javaClass.getName() + ) + } + + + val type = when (finalType) { + ViewType.CHEST -> { + when (size / finalType.columns) { + 1 -> InventoryType.CHEST_1_ROW + 2 -> InventoryType.CHEST_2_ROW + 3 -> InventoryType.CHEST_3_ROW + 4 -> InventoryType.CHEST_4_ROW + 5 -> InventoryType.CHEST_5_ROW + 6 -> InventoryType.CHEST_6_ROW + else -> InventoryType.CHEST_6_ROW + } + } + ViewType.BEACON -> InventoryType.BEACON + ViewType.HOPPER -> InventoryType.HOPPER + ViewType.SMOKER -> InventoryType.SMOKER + ViewType.BLAST_FURNACE -> InventoryType.BLAST_FURNACE + ViewType.FURNACE -> InventoryType.FURNACE + ViewType.ANVIL -> InventoryType.ANVIL + ViewType.CRAFTING_TABLE -> InventoryType.CRAFTING + ViewType.DROPPER, ViewType.DROPPER -> InventoryType.WINDOW_3X3 + ViewType.BREWING_STAND -> InventoryType.BREWING_STAND + ViewType.SHULKER_BOX -> InventoryType.SHULKER_BOX + else -> error("Unsupported type: $finalType") + } + + val inventory = Inventory(type, net.kyori.adventure.text.Component.empty()) + return MinestomViewContainer(inventory, false, finalType, false) + } + + override fun createViewer(entity: Any, context: IFRenderContext?): Viewer { + require(entity is Player) { "createViewer(...) first parameter must be a Player" } + + return MinestomViewer(entity, context) + } + + override fun createOpenContext( + root: RootView, subject: Viewer?, viewers: List, initialData: Any? + ): IFOpenContext { + return OpenContext( + root as View, + subject, + viewers.stream().collect( + Collectors.toMap( + Function { obj: Viewer -> obj.getId() }, Function.identity() + ) + ), + initialData + ) + } + + override fun createRenderContext( + id: UUID, + root: RootView, + config: ViewConfig, + container: ViewContainer, + viewers: Map, + subject: Viewer, + initialData: Any? + ): IFRenderContext { + return RenderContext(id, root as View, config, container, viewers, subject, initialData) + } + + override fun createSlotClickContext( + slotClicked: Int, + whoClicked: Viewer, + interactionContainer: ViewContainer, + componentClicked: Component?, + origin: Any, + combined: Boolean + ): IFSlotClickContext { + val context: IFRenderContext = whoClicked.getActiveContext() + return SlotClickContext( + slotClicked, + context, + whoClicked, + interactionContainer, + componentClicked, + origin as InventoryPreClickEvent, + combined + ) + } + + override fun createSlotRenderContext( + slot: Int, parent: IFRenderContext, viewer: Viewer? + ): IFSlotRenderContext { + return SlotRenderContext(slot, parent, viewer) + } + + override fun createCloseContext(viewer: Viewer, parent: IFRenderContext): IFCloseContext { + return CloseContext(viewer, parent) + } + + override fun createComponentBuilder(root: VirtualView): ComponentBuilder<*, Context> { + return MinestomIemComponentBuilder(root) + } + + override fun worksInCurrentPlatform(): Boolean { + return true + } + + override fun getLogger(): Logger { + return NoopLogger() + } + + override fun scheduleJobInterval(root: RootView, intervalInTicks: Long, execution: Runnable): Job { + return MinestomTaskJobImpl(intervalInTicks.toInt(), execution) + } + + companion object { + private val defaultType: ViewType = ViewType.CHEST + } +} diff --git a/inventory-framework-platform-minestom/src/main/kotlin/me/devnatan/inventoryframework/internal/MinestomTaskJobImpl.kt b/inventory-framework-platform-minestom/src/main/kotlin/me/devnatan/inventoryframework/internal/MinestomTaskJobImpl.kt new file mode 100644 index 000000000..7afaa6859 --- /dev/null +++ b/inventory-framework-platform-minestom/src/main/kotlin/me/devnatan/inventoryframework/internal/MinestomTaskJobImpl.kt @@ -0,0 +1,31 @@ +package me.devnatan.inventoryframework.internal + +import net.minestom.server.MinecraftServer +import net.minestom.server.timer.Task +import net.minestom.server.timer.TaskSchedule + +internal class MinestomTaskJobImpl(private val intervalInTicks: Int, + private val execution: Runnable +) : Job { + private var task: Task? = null + + override fun isStarted(): Boolean { + return task != null + } + + override fun start() { + if (isStarted) return + val schedule = TaskSchedule.tick(intervalInTicks) + task = MinecraftServer.getSchedulerManager().scheduleTask( this::loop, schedule, schedule) + } + + override fun cancel() { + if (!isStarted) return + task?.cancel() + task = null + } + + override fun loop() { + execution.run() + } +} diff --git a/inventory-framework-platform-minestom/src/main/kotlin/me/devnatan/inventoryframework/pipeline/CancelledCloseInterceptor.kt b/inventory-framework-platform-minestom/src/main/kotlin/me/devnatan/inventoryframework/pipeline/CancelledCloseInterceptor.kt new file mode 100644 index 000000000..dd0ce026f --- /dev/null +++ b/inventory-framework-platform-minestom/src/main/kotlin/me/devnatan/inventoryframework/pipeline/CancelledCloseInterceptor.kt @@ -0,0 +1,15 @@ +package me.devnatan.inventoryframework.pipeline + +import me.devnatan.inventoryframework.VirtualView +import me.devnatan.inventoryframework.context.CloseContext + + +class CancelledCloseInterceptor : PipelineInterceptor { + override fun intercept(pipeline: PipelineContext, subject: VirtualView) { + if (subject !is CloseContext) return + + if (!subject.isCancelled) return + + subject.root.nextTick { subject.viewer.open(subject.container) } + } +} diff --git a/inventory-framework-platform-minestom/src/main/kotlin/me/devnatan/inventoryframework/pipeline/GlobalClickInterceptor.kt b/inventory-framework-platform-minestom/src/main/kotlin/me/devnatan/inventoryframework/pipeline/GlobalClickInterceptor.kt new file mode 100644 index 000000000..1de0d014c --- /dev/null +++ b/inventory-framework-platform-minestom/src/main/kotlin/me/devnatan/inventoryframework/pipeline/GlobalClickInterceptor.kt @@ -0,0 +1,23 @@ +package me.devnatan.inventoryframework.pipeline + +import me.devnatan.inventoryframework.ViewConfig +import me.devnatan.inventoryframework.VirtualView +import me.devnatan.inventoryframework.context.SlotClickContext +import net.minestom.server.event.inventory.InventoryPreClickEvent + +/** + * Intercepted when a player clicks on the view container. + * If the click is canceled, this interceptor ends the pipeline immediately. + */ +class GlobalClickInterceptor : PipelineInterceptor { + override fun intercept(pipeline: PipelineContext, subject: VirtualView) { + if (subject !is SlotClickContext) return + + val event: InventoryPreClickEvent = subject.clickOrigin + + // inherit cancellation so we can un-cancel it + subject.isCancelled = + event.isCancelled || subject.config.isOptionSet(ViewConfig.CANCEL_ON_CLICK, true) + subject.root.onClick(subject) + } +} diff --git a/inventory-framework-platform-minestom/src/main/kotlin/me/devnatan/inventoryframework/pipeline/ItemClickInterceptor.kt b/inventory-framework-platform-minestom/src/main/kotlin/me/devnatan/inventoryframework/pipeline/ItemClickInterceptor.kt new file mode 100644 index 000000000..ac93429e7 --- /dev/null +++ b/inventory-framework-platform-minestom/src/main/kotlin/me/devnatan/inventoryframework/pipeline/ItemClickInterceptor.kt @@ -0,0 +1,27 @@ +package me.devnatan.inventoryframework.pipeline + +import me.devnatan.inventoryframework.VirtualView +import me.devnatan.inventoryframework.component.ItemComponent +import me.devnatan.inventoryframework.context.SlotClickContext +import net.minestom.server.event.inventory.InventoryPreClickEvent + +/** + * Intercepted when a player clicks on an item the view container. + */ +class ItemClickInterceptor : PipelineInterceptor { + override fun intercept(pipeline: PipelineContext, subject: VirtualView) { + if (subject !is SlotClickContext) return + + val event: InventoryPreClickEvent = subject.clickOrigin + event.inventory ?: return + + val component = subject.component ?: return + + if (component is ItemComponent) { + val item: ItemComponent = component + + // inherit cancellation so we can un-cancel it + subject.isCancelled = item.isCancelOnClick + } + } +} diff --git a/inventory-framework-platform-minestom/src/main/kotlin/me/devnatan/inventoryframework/pipeline/ItemCloseOnClickInterceptor.kt b/inventory-framework-platform-minestom/src/main/kotlin/me/devnatan/inventoryframework/pipeline/ItemCloseOnClickInterceptor.kt new file mode 100644 index 000000000..d1d7b3463 --- /dev/null +++ b/inventory-framework-platform-minestom/src/main/kotlin/me/devnatan/inventoryframework/pipeline/ItemCloseOnClickInterceptor.kt @@ -0,0 +1,28 @@ +package me.devnatan.inventoryframework.pipeline + +import me.devnatan.inventoryframework.VirtualView +import me.devnatan.inventoryframework.component.ItemComponent +import me.devnatan.inventoryframework.context.SlotClickContext +import net.minestom.server.event.inventory.InventoryPreClickEvent + +/** + * Intercepted when a player clicks on an item the view container. Checks if the container should be + * closed when the item is clicked. + */ +class ItemCloseOnClickInterceptor : PipelineInterceptor { + override fun intercept(pipeline: PipelineContext, subject: VirtualView) { + if (subject !is SlotClickContext) return + + val event: InventoryPreClickEvent = subject.clickOrigin + event.inventory ?: return + + val component = subject.component + if (component !is ItemComponent || !component.isVisible) return + + val item: ItemComponent = component as ItemComponent + if (item.isCloseOnClick) { + subject.closeForPlayer() + pipeline.finish() + } + } +} diff --git a/inventory-framework-platform/src/main/java/me/devnatan/inventoryframework/IFViewFrame.java b/inventory-framework-platform/src/main/java/me/devnatan/inventoryframework/IFViewFrame.java index e93e47e9e..9808ffc15 100644 --- a/inventory-framework-platform/src/main/java/me/devnatan/inventoryframework/IFViewFrame.java +++ b/inventory-framework-platform/src/main/java/me/devnatan/inventoryframework/IFViewFrame.java @@ -17,7 +17,7 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.UnmodifiableView; -abstract class IFViewFrame, V extends PlatformView> { +public abstract class IFViewFrame, V extends PlatformView> { /** * Called when a {@link IFViewFrame} is registered. diff --git a/inventory-framework-platform/src/main/java/me/devnatan/inventoryframework/PlatformView.java b/inventory-framework-platform/src/main/java/me/devnatan/inventoryframework/PlatformView.java index f18c65033..a716987e4 100644 --- a/inventory-framework-platform/src/main/java/me/devnatan/inventoryframework/PlatformView.java +++ b/inventory-framework-platform/src/main/java/me/devnatan/inventoryframework/PlatformView.java @@ -65,6 +65,10 @@ public abstract class PlatformView< private final StateAccess stateAccess = new StateAccessImpl<>(this, getElementFactory(), stateRegistry); + protected PlatformView() { + super(); + } + // region Open & Close /** * Closes all contexts that are currently active in this view. diff --git a/inventory-framework-platform/src/main/java/me/devnatan/inventoryframework/context/PlatformConfinedContext.java b/inventory-framework-platform/src/main/java/me/devnatan/inventoryframework/context/PlatformConfinedContext.java index 09d0b0347..84fe802bc 100644 --- a/inventory-framework-platform/src/main/java/me/devnatan/inventoryframework/context/PlatformConfinedContext.java +++ b/inventory-framework-platform/src/main/java/me/devnatan/inventoryframework/context/PlatformConfinedContext.java @@ -9,7 +9,9 @@ import me.devnatan.inventoryframework.Viewer; import org.jetbrains.annotations.NotNull; -abstract class PlatformConfinedContext extends PlatformContext implements IFConfinedContext { +public abstract class PlatformConfinedContext extends PlatformContext implements IFConfinedContext { + + protected PlatformConfinedContext() {} @Override public abstract Viewer getViewer(); diff --git a/settings.gradle b/settings.gradle index 71e5d0554..6b175daaf 100644 --- a/settings.gradle +++ b/settings.gradle @@ -5,6 +5,7 @@ buildscript { maven { url 'https://hub.spigotmc.org/nexus/content/repositories/snapshots/' } maven { url 'https://oss.sonatype.org/content/repositories/snapshots' } maven { url 'https://repo.papermc.io/repository/maven-public/' } + maven { url 'https://jitpack.io' } } } } @@ -22,6 +23,10 @@ include 'inventory-framework-test', 'inventory-framework-platform', 'inventory-framework-platform-paper', 'inventory-framework-platform-bukkit', + 'inventory-framework-platform-minestom', 'inventory-framework-anvil-input', - 'example' + 'example-paper', + 'example-minestom' +project(':example-paper').projectDir = new File('examples/paper') +project(':example-minestom').projectDir = new File('examples/minestom')