From 9655d10025faaab011ee552c48496780adcd8414 Mon Sep 17 00:00:00 2001 From: Denis Istratov Date: Thu, 28 Mar 2024 10:12:38 +0300 Subject: [PATCH] 1.0.0.3 - new modules Added utility, menu, thirdparty and exposed modules A few little fixes --- README.md | 96 ++++- build.gradle.kts | 9 +- .../carcadex/kotlinbukkitkit/commands/Test.kt | 12 + exposed/.gitignore | 42 +++ exposed/build.gradle.kts | 121 ++++++ .../kotlinbukkitkit/exposed/DatabaseHelper.kt | 64 ++++ .../kotlinbukkitkit/exposed/DatabaseType.kt | 232 ++++++++++++ .../kotlinbukkitkit/exposed/delegate/Block.kt | 162 ++++++++ .../exposed/delegate/BlockPos.kt | 148 ++++++++ .../exposed/delegate/Cached.kt | 35 ++ .../kotlinbukkitkit/exposed/delegate/Chunk.kt | 152 ++++++++ .../exposed/delegate/ChunkPos.kt | 137 +++++++ .../exposed/delegate/ExposedDelegate.kt | 18 + .../kotlinbukkitkit/exposed/delegate/Item.kt | 82 ++++ .../exposed/delegate/Location.kt | 190 ++++++++++ .../exposed/delegate/LocationPos.kt | 177 +++++++++ .../exposed/delegate/OfflinePlayer.kt | 47 +++ .../kotlinbukkitkit/exposed/delegate/World.kt | 52 +++ gradle.properties | 2 +- menu/.gitignore | 42 +++ menu/build.gradle.kts | 117 ++++++ .../carcadex/kotlinbukkitkit/menu/Menu.kt | 51 +++ .../kotlinbukkitkit/menu/MenuController.kt | 105 ++++++ .../kotlinbukkitkit/menu/MenuEventHandler.kt | 14 + .../kotlinbukkitkit/menu/MenuEvents.kt | 121 ++++++ .../carcadex/kotlinbukkitkit/menu/Utils.kt | 29 ++ .../kotlinbukkitkit/menu/dsl/Extensions.kt | 8 + .../kotlinbukkitkit/menu/dsl/MenuDSL.kt | 46 +++ .../kotlinbukkitkit/menu/dsl/MenuDSLImpl.kt | 199 ++++++++++ .../menu/dsl/MenuEventHandlerDSL.kt | 54 +++ .../menu/dsl/pagination/MenuPagination.kt | 134 +++++++ .../menu/dsl/pagination/MenuPaginationImpl.kt | 265 +++++++++++++ .../dsl/pagination/PaginationEventHandler.kt | 23 ++ .../dsl/pagination/slot/PaginationSlotDSL.kt | 35 ++ .../pagination/slot/PaginationSlotDSLImpl.kt | 94 +++++ .../slot/PaginationSlotEventHandler.kt | 54 +++ .../kotlinbukkitkit/menu/dsl/slot/SlotDSL.kt | 27 ++ .../menu/dsl/slot/SlotDSLImpl.kt | 22 ++ .../menu/dsl/slot/SlotEventHandlerDSL.kt | 53 +++ .../kotlinbukkitkit/menu/slot/Slot.kt | 33 ++ .../menu/slot/SlotEventHandler.kt | 14 + .../kotlinbukkitkit/menu/slot/SlotEvents.kt | 85 +++++ .../messages/model/Message.java | 4 +- settings.gradle.kts | 4 + thirdparty/.gitignore | 42 +++ thirdparty/build.gradle.kts | 116 ++++++ .../bungeecord/BungeeCordController.kt | 55 +++ .../bungeecord/BungeeCordUtils.kt | 23 ++ .../bungeecord/ExBungeeCord.kt | 58 +++ utility/.gitignore | 42 +++ utility/build.gradle.kts | 116 ++++++ .../utility/collections/ExpirationList.kt | 352 ++++++++++++++++++ .../utility/collections/ExpirationMap.kt | 175 +++++++++ .../collections/ObservableCollections.kt | 238 ++++++++++++ .../collections/OnlinePlayerCollections.kt | 270 ++++++++++++++ .../utility/extensions/Collection.kt | 28 ++ .../utility/extensions/Duration.kt | 19 + .../utility/extensions/PositionRange.kt | 122 ++++++ .../utility/extensions/Random.kt | 19 + .../utility/extensions/String.kt | 34 ++ .../kotlinbukkitkit/utility/types/Position.kt | 87 +++++ .../utility/types/TimeFormat.kt | 114 ++++++ .../utility/utils/ComparatorUtils.kt | 25 ++ 63 files changed, 5334 insertions(+), 12 deletions(-) create mode 100644 commands/src/main/kotlin/tech/carcadex/kotlinbukkitkit/commands/Test.kt create mode 100644 exposed/.gitignore create mode 100644 exposed/build.gradle.kts create mode 100644 exposed/src/main/kotlin/tech/carcadex/kotlinbukkitkit/exposed/DatabaseHelper.kt create mode 100644 exposed/src/main/kotlin/tech/carcadex/kotlinbukkitkit/exposed/DatabaseType.kt create mode 100644 exposed/src/main/kotlin/tech/carcadex/kotlinbukkitkit/exposed/delegate/Block.kt create mode 100644 exposed/src/main/kotlin/tech/carcadex/kotlinbukkitkit/exposed/delegate/BlockPos.kt create mode 100644 exposed/src/main/kotlin/tech/carcadex/kotlinbukkitkit/exposed/delegate/Cached.kt create mode 100644 exposed/src/main/kotlin/tech/carcadex/kotlinbukkitkit/exposed/delegate/Chunk.kt create mode 100644 exposed/src/main/kotlin/tech/carcadex/kotlinbukkitkit/exposed/delegate/ChunkPos.kt create mode 100644 exposed/src/main/kotlin/tech/carcadex/kotlinbukkitkit/exposed/delegate/ExposedDelegate.kt create mode 100644 exposed/src/main/kotlin/tech/carcadex/kotlinbukkitkit/exposed/delegate/Item.kt create mode 100644 exposed/src/main/kotlin/tech/carcadex/kotlinbukkitkit/exposed/delegate/Location.kt create mode 100644 exposed/src/main/kotlin/tech/carcadex/kotlinbukkitkit/exposed/delegate/LocationPos.kt create mode 100644 exposed/src/main/kotlin/tech/carcadex/kotlinbukkitkit/exposed/delegate/OfflinePlayer.kt create mode 100644 exposed/src/main/kotlin/tech/carcadex/kotlinbukkitkit/exposed/delegate/World.kt create mode 100644 menu/.gitignore create mode 100644 menu/build.gradle.kts create mode 100644 menu/src/main/kotlin/tech/carcadex/kotlinbukkitkit/menu/Menu.kt create mode 100644 menu/src/main/kotlin/tech/carcadex/kotlinbukkitkit/menu/MenuController.kt create mode 100644 menu/src/main/kotlin/tech/carcadex/kotlinbukkitkit/menu/MenuEventHandler.kt create mode 100644 menu/src/main/kotlin/tech/carcadex/kotlinbukkitkit/menu/MenuEvents.kt create mode 100644 menu/src/main/kotlin/tech/carcadex/kotlinbukkitkit/menu/Utils.kt create mode 100644 menu/src/main/kotlin/tech/carcadex/kotlinbukkitkit/menu/dsl/Extensions.kt create mode 100644 menu/src/main/kotlin/tech/carcadex/kotlinbukkitkit/menu/dsl/MenuDSL.kt create mode 100644 menu/src/main/kotlin/tech/carcadex/kotlinbukkitkit/menu/dsl/MenuDSLImpl.kt create mode 100644 menu/src/main/kotlin/tech/carcadex/kotlinbukkitkit/menu/dsl/MenuEventHandlerDSL.kt create mode 100644 menu/src/main/kotlin/tech/carcadex/kotlinbukkitkit/menu/dsl/pagination/MenuPagination.kt create mode 100644 menu/src/main/kotlin/tech/carcadex/kotlinbukkitkit/menu/dsl/pagination/MenuPaginationImpl.kt create mode 100644 menu/src/main/kotlin/tech/carcadex/kotlinbukkitkit/menu/dsl/pagination/PaginationEventHandler.kt create mode 100644 menu/src/main/kotlin/tech/carcadex/kotlinbukkitkit/menu/dsl/pagination/slot/PaginationSlotDSL.kt create mode 100644 menu/src/main/kotlin/tech/carcadex/kotlinbukkitkit/menu/dsl/pagination/slot/PaginationSlotDSLImpl.kt create mode 100644 menu/src/main/kotlin/tech/carcadex/kotlinbukkitkit/menu/dsl/pagination/slot/PaginationSlotEventHandler.kt create mode 100644 menu/src/main/kotlin/tech/carcadex/kotlinbukkitkit/menu/dsl/slot/SlotDSL.kt create mode 100644 menu/src/main/kotlin/tech/carcadex/kotlinbukkitkit/menu/dsl/slot/SlotDSLImpl.kt create mode 100644 menu/src/main/kotlin/tech/carcadex/kotlinbukkitkit/menu/dsl/slot/SlotEventHandlerDSL.kt create mode 100644 menu/src/main/kotlin/tech/carcadex/kotlinbukkitkit/menu/slot/Slot.kt create mode 100644 menu/src/main/kotlin/tech/carcadex/kotlinbukkitkit/menu/slot/SlotEventHandler.kt create mode 100644 menu/src/main/kotlin/tech/carcadex/kotlinbukkitkit/menu/slot/SlotEvents.kt create mode 100644 thirdparty/.gitignore create mode 100644 thirdparty/build.gradle.kts create mode 100644 thirdparty/src/main/kotlin/tech/carcadex/kotlinbukkitkit/bungeecord/BungeeCordController.kt create mode 100644 thirdparty/src/main/kotlin/tech/carcadex/kotlinbukkitkit/bungeecord/BungeeCordUtils.kt create mode 100644 thirdparty/src/main/kotlin/tech/carcadex/kotlinbukkitkit/bungeecord/ExBungeeCord.kt create mode 100644 utility/.gitignore create mode 100644 utility/build.gradle.kts create mode 100644 utility/src/main/kotlin/tech/carcadex/kotlinbukkitkit/utility/collections/ExpirationList.kt create mode 100644 utility/src/main/kotlin/tech/carcadex/kotlinbukkitkit/utility/collections/ExpirationMap.kt create mode 100644 utility/src/main/kotlin/tech/carcadex/kotlinbukkitkit/utility/collections/ObservableCollections.kt create mode 100644 utility/src/main/kotlin/tech/carcadex/kotlinbukkitkit/utility/collections/OnlinePlayerCollections.kt create mode 100644 utility/src/main/kotlin/tech/carcadex/kotlinbukkitkit/utility/extensions/Collection.kt create mode 100644 utility/src/main/kotlin/tech/carcadex/kotlinbukkitkit/utility/extensions/Duration.kt create mode 100644 utility/src/main/kotlin/tech/carcadex/kotlinbukkitkit/utility/extensions/PositionRange.kt create mode 100644 utility/src/main/kotlin/tech/carcadex/kotlinbukkitkit/utility/extensions/Random.kt create mode 100644 utility/src/main/kotlin/tech/carcadex/kotlinbukkitkit/utility/extensions/String.kt create mode 100644 utility/src/main/kotlin/tech/carcadex/kotlinbukkitkit/utility/types/Position.kt create mode 100644 utility/src/main/kotlin/tech/carcadex/kotlinbukkitkit/utility/types/TimeFormat.kt create mode 100644 utility/src/main/kotlin/tech/carcadex/kotlinbukkitkit/utility/utils/ComparatorUtils.kt diff --git a/README.md b/README.md index db1654b..e956efa 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,97 @@ # KotlinBukkitKit -KotlinBukkitKit is an API for Bukkit/SpigotAPI using features Kotlin. DevSrSouza / KotlinBukkitAPI. +KotlinBukkitKit is an API for Bukkit/SpigotAPI using features Kotlin. +Thanks for DevSrSouza / KotlinBukkitAPI. ```kotlin -implementation("tech.carcadex:kotlinbukkitkit-architecture:1.0.0.2") -implementation("tech.carcadex:kotlinbukkitkit-extensions:1.0.0.2") -implementation("tech.carcadex:kotlinbukkitkit-serialization:1.0.0.2") -implementation("tech.carcadex:kotlinbukkitkit-genref:1.0.0.2") -implementation("tech.carcadex:kotlinbukkitkit-messages:1.0.0.2") -implementation("tech.carcadex:kotlinbukkitkit-commands:1.0.0.2") +implementation("tech.carcadex:kotlinbukkitkit-architecture:1.0.0.3") +implementation("tech.carcadex:kotlinbukkitkit-extensions:1.0.0.3") +implementation("tech.carcadex:kotlinbukkitkit-serialization:1.0.0.3") +implementation("tech.carcadex:kotlinbukkitkit-messages:1.0.0.3") +implementation("tech.carcadex:kotlinbukkitkit-commands:1.0.0.3") +implementation("tech.carcadex:kotlinbukkitkit-utility:1.0.0.3") +implementation("tech.carcadex:kotlinbukkitkit-exposed:1.0.0.3") + +compileOnly("tech.carcadex:kotlinbukkitkit-genref:1.0.0.3") +ksp("tech.carcadex:kotlinbukkitkit-genref:1.0.0.3") ``` Documentation will be there: https://red-tea.gitbook.io/kotlinbukkitkit/ + + +KotlinBukkitAPI + +## KotlinBukkitKit + +![Kotlin version](https://img.shields.io/static/v1?label=Kotlin&message=1.9.23&color=Orange&style=for-the-badge) +[![GitHub stars](https://img.shields.io/github/stars/CarcadeX/KotlinBukkitKit.svg?style=for-the-badge&color=orange&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAADYklEQVR4XuWbvbPNQBiHn9uo6NFT0GEYHa0/QofOR2MojI8xxmh8VFwVf4RaxTB0KOhdPRUF8zPJmdy1J3l3N9ndxDb3zr0n2exzfu/zJjk5G5Qdh5vp35c6jI1SEzfzbgK/gXOljqMkgF3A1wbAXuBHCQglAehdf9wsWr8rDdlHSQCq+0MdBxzJvnqgFADJ752zYAHILsNSABT3Mw4A/S27DEsAaOW30wHwHcguwxIAuvJzyz67DEsA6MrPBaD/ZZVhbgA++bkQssowNwCf/FwAWWWYE8A6+bkAssowJ4A++RWTYU4AffIrJsNcACzyKyLDXAAs8isiwxwArPIrIsMcAELkl12GOQCEyC+7DAUgpj5LXLpPMeemAOwAXgLHp5ih4n2+Ak62JbCnuRmhn//D2ALUmre6DlAClAQlYsnjJ3ACeK1FuhI8CzxZ8uqbu06rG7C+LrBkKf5zpekDsFQp/pUeoBJYjXXnAUuT4kp6bnn3nQgtRYrbpBcCQK9dghR7b7RaToXnLMXB22sWAHOVold6oSXQvn5uUlwrvVgA2m4uUuyVXgqAuUgx6NMliwN8t6rcDzZrOXselF5qArR9rVI0SW8MANpHbVI0S28sADVJMUh6YwKoRYpB0hsbwEHgQ2ED6hg+xR5DTBfoznUDuB47+Ujb6Rhuxu4rFcBH4EDs5CNtp3dfKYgaKQBqiH+76OgySAFQQ/xbANFlkAKghvi3AKLLIBZATfFPKoNYADXFP6kMYgHUFP+kMogBUGP8o8sgBsBY8f8MXGqO/D6wP6qRb98ouBvEAEiNvx6DuwU8BH41x69L7AvANUBPlMSO4G4QCiAl/vpqzDPgKvBtzQp3A3eB0wmP8gedFIUCiI3/G+A88Nb41h4DHgFHja93r0/M1wahAELjr3f6CvC8+W5QyHp0bEqCEqFkWEdQGYQACIm/blI8AG4DqvmUISfIDXKE9dkFcxmEALDG/wVwEfiSsmrPtvsaqKcM+zV3gxAAQ/Fv25oATDkEYKhtmsvACqAv/r62NiUA7dvSNk1lYAXgi7+lrU0Noq9tmsrACsCNf2hbmxqEr22aysACoBv/lLY2NQRf2xwsAwsARUlnb2O1talBdNvmnaEbphYA94CnE7S1qUGobeozzMt9E/0BUX274HXoySkAAAAASUVORK5CYII=)](https://github.com/DevSrSouza/KotlinBukkitAPI/stargazers) +[![Github Issues](https://img.shields.io/github/issues-raw/CarcadeX/KotlinBukkitKit.svg?style=for-the-badge&color=orange&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAGfklEQVR4Xu2bd6glNRSHv7Vjw46KDRULCqLYFVSwIHZFFCvYKzZU7A17r6CsiqjYG4oigqIidtQ/RCzYOzbsDZUPMst9900myczc3bfuHnj/vElyzvklOTntTmIGp0kzuP7MBGDmCRg9ArMCawMbA6sCKwNLA/OFP7//BnwPfAa8C7wBvAC8BPw5ShFHdQXmALYF9gQ2B+ZvqcQvwBPAXcCDwO8t14lO6xuARYBjgEOAhXoW9jtgMnA58GVfa/cFwDzAacCRwNx9CRdZx+tyNXAu8GNXXn0AsBNwFbBUV2EK538BHA48UDhvzPAuAMwVFD+wiwA9zL05AOHJKKa2ALjbjwBrFHMczYTXge2BT0qXbwPAKsDjwDKlzEY8/vPw4rxVwqcUAJV/Bli0hAnwD/Ai8BTwCvAO4B3+GfgbmBdYIvgI6wCbARsAsxTyOQG4uGROCQAe++cKd/5j4BrgdsAdKqHFgb3D/V42Y+JZwJkZ41oZQQ2enlnunf8aOAW4BfirVKih8bMB+wHnAItF1kopXwEzDqDcE3A9cFCmIrcCRwXXNnNK1rAFw/uvdzlIOcqfESaMG5sDgO/8/RkiutN6gDclxv6b+J6SyWf3WmB2oET5iu2YOSlmGietasrJ+RXYObwOKay6AuD6WwIaS73BGHncq50fHjMFhBQAFwAnJjRy57fLVN6l+gAgBXKT8mNOQhMAPnUfZvj2BwA3piQa+N4FAI2x8/9oufPjTkITAB6vkxOKafD2KVC+ywlQeUNi/QavW12eIGfnxxjQGADG8zoqTSGtT53JjR+mAgCV8lsFXg8Buw49scXK6zfEABDh+xKKaY2Nz0up9AoMK1/xU77dw4lopbwLxQBwcUGIkR7eii2dnBIAYspXct0Z3OrTC3Yh+QzqeZl9MWcXo2Kfu4URTClfoPOUoVmO0HrB7Y0xMLAxqVnq21fr5Z4ADd4ObbSMzKl1muquwHHAJQ2Mnwc27CBYLgDbBA9Ug9yVoh5jHQAatv0bOJ4XAp22QuUC4Po7AvcAXsu21Ogu1wHwbMjhxxhqHLvk4UoAUAafuzsA6wellIoVal8Bvb+m+Ht14M1SSVoYwUEWewA6XSUJkqTyMqg7Ad8mHKCFwyvRFoPSE1Dx2RcwAZqKXxyfpXwMAF1MQ80YzdmxXNUWAOXR+TI30QRCtvLTIwDKbC3ANFsdpZQ/CTh/cOL0dAUG5T46lMgG/5dSXndZ8MYkdOsAmIhGsG639UYvDB9ylDc58gGwfOoEpJ7BXTJTZDEb0sUGDK9pPdLnsSkbPBgoPQ1smgIg5Qh5h1J5grYvRN/zhqPEG4CDUwAcC1zaIInpcYsWE53qQmRL91ekAMgJhiyL2c0xUSmWH7BT5dUUAN4pw+Gmrg4TpRdNUO1jytuCYwOH0ewUijkU9wIauxhZhV1hKiRESjFuygzdDew2vGAMgJxiiEUQvbJS6vMVGOSdSotZPn84FwBjcBMe+v0x+iYkRb0uJTQKAFLKK+uSdSe2yae2GHlqQjPDVCO1EuobgJTyylaUEKmUcfc/AmyAaqLSq9AnAIPeYEzGn0J4rxEcR6nQMqc4YqHC3N2jmcegLwC80yZmUjmCsxtqhMnY2pY3i6OpdhiLo2ZuckDoAwDzhabuDc2bSN9/tdCJWjsudQKcJNJWYlLkSTgi42XoCsChoTstJ08oUI2bkgOAil8HyDiHNIw2TJpZ6pMs09kXYDUoh+xdtFGjkXIB8KiZDl8ztWD4rvJGalaNuzY7m52yAu1d1pPLoZdDYjfJOxcAmfqO2iS1XI4EYcynYddskirt4bNrrGqSStmgQZHeBzbK7ScuAUAmKwHmC2LNSjFs9L9tj3syBCNvh+qzT5Q2wU4UAbbeuC6wCbB+hoUf5vdV2Pn3cjepFIAKBBslS05Crjxdxrnzls+zlZdZGwCq66BfvVYXiXuc6+myTae4jb4tAMquYbwMOKxHRdospbU/vq2x7QJAJazI27+f083ZRsHYHJ0c/Y4c5yvKtw8AXFyP0Zy76WoN2ihJw+mvRuxga9UiPyhcXwBUa+qs6Hx4LXLf7FywDGl1hK7sswu1bwAqZXRetgb2ArYAFsjVcmicEZw/mroNeCz0A7Vcqn7aqAAY5GaOUQ9y8GdzOja24FR5R3/749G290gfwQBMf+O14Rxer9p3eAb7lmOarTc1TsA0Uy6H8UwAclD6P4/5D0siPlDBJvVTAAAAAElFTkSuQmCC)](https://github.com/DevSrSouza/KotlinBukkitAPI/issues) +[![GitHub last commit](https://img.shields.io/github/last-commit/CarcadeX/KotlinBukkitKit/main.svg?style=for-the-badge&color=orange&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADgAAABACAYAAABP97SyAAADZElEQVRoQ+2ZSciOURTHf1+ZZdoZMpdhIUOyJGTe2CpLyRhKYYGwQUghQ1nJ3sZQhpIQGbKhEMk8LMg8Rf/PfdT3ed/nTs/z9Xi7d/Uuzv+c8z//59577nmbaPDV1OD8SAT/d4WTgknBilcgfaIVF8iaXlLQWqKKGyQFKy6QNb2koLVEFTdICkYI1B2YA0wBxgKDgB7G3zvgIXATOAccB95HxKoLLUPBocA6YB7QxTHpT8BRYCvwwBHjZFYkwY7AZmAV0N4p+r9G34AdwCZAv6NXUQQHAseAMdEZ/XFwHZgLPIn1VwTBkWYf9Y5NphX+GTAZuBvjN5Zgf+AK0CcmiRysFJwAPA/1H0NQ++wSMD40uCPuMjAR+OFo38IshuAGcxiExPXF6FTWCeu9Qgn2A+4DnRwi6q47aPbpI2OvO1H34yJgtIOPj4Cun5cOtoUouBtYYQn2GVgOHM6xU4EXAvJnK9Z2YE1bENR99wLomRNM5GYAFxwTmgScspB8A/QFvjv6bDYL+URnm9YqL84Ci3K1sPpc91uSnw6c9iX4ywfgYKs9N87BrrWJii2sy550di+nRROUEjpUQtYSYF8IsB6mDILDgHuBSY4A7gRia8LKIKhDKLRRFvZLIuhRgTIUHB7RIKtxv+2Rv9W0DIKLgQPWyLUNlgF7ArFttgdvmRGF7+msYgs7qmiCvv5mAScsoJCrYimw1+J3GnDGJ+GQTkYnnd5nvXIC6SScCZx3TEYP25OAfNdbrwE1+aW3akpgl5m95OUvkiuBQznNhAqsy32nhZzibAPWOhbsr1mIggKr6dVzqbNDQO0rkdR4UKNCrSHAVPOScNlzei4J88ohXguTUIJyst5M0XxjhthLOSnovWIItgMumpmJd2APgGLoOfXTAxP9iWYOtOmvmk82JL4N89gUUO/PoBWjYBZQDbL2V9GTtadmbBjauDfnVwRB+RlgBr/6D6KIdc0MfkUyahVFUEl0ADYCq83vkMS+Apq9bPG97+oFK5JgFmOwGQ7NB7o6svwAHDEnZTZ5c4Tmm5VBMIvYDVBbl/19JuLZoOqtuRNvAGfNwEkkC19lEiw82RCHiWBI1aqESQpWSY2QXJKCIVWrEiYpWCU1QnJJCoZUrUqYpGCV1AjJJSkYUrUqYRpewd/nrH4569+7pgAAAABJRU5ErkJggg==)](https://github.com/DevSrSouza/KotlinBukkitAPI/commit) +[![Discord](https://img.shields.io/discord/1133859884685856878?style=for-the-badge&color=ORANGE&label=Discord)](https://discord.gg/pyrh4dUPZD) +[![MIT License](https://img.shields.io/badge/license-MIT-blue.svg?color=ORANGE&style=for-the-badge)](https://choosealicense.com/licenses/mit) + +KotlinBukkitKit is an API for Bukkit/SpigotAPI using the cool and nifty features Kotlin has to make your life more easier. + +### Contents: + +* [Documentation](https://red-tea.gitbook.io/kotlinbukkitkit) +* [IDEA Plugin](https://github.com/CarcadeX/KotlinBukkitKit-Tooling) + + +# Samples + +## [We have more Samples in the Documentation](https://red-tea.gitbook.io/kotlinbukkitkit) + +KotlinBukkitAPI goes beyond this samples, and you can find all of it in the [wiki/documentation](https://red-tea.gitbook.io/kotlinbukkitkit). + +Event DSL sample +```kotlin +plugin.events { + event { + player.msg("&3Welcome ${player.name}".translateColor()) + } + + event { + broadcast("&eThe player &c${player.name} &eleft :(".translateColor()) + } +} +``` + +Simple Command DSL example +```kotlin +plugin.simpleCommand("hi") { + sender.msg("&eHallo".translateColor()) +} +``` + +Item meta DSL and other stuff +```kotlin +val gem = item(Material.DIAMOND).apply { + amount = 5 + meta { + displayName = "&bGem".translateColor() + } +} +val encbook = item(Material.ENCHANTED_BOOK).meta { + displayName = "&4&lThe powerful BOOK".translateColor() + addStoredEnchant(Enchantment.DAMAGE_ALL, 10, true) // putting sharpness 10 to the book +} +``` + +Another approach: +```kotlin +val gem = item(Material.DIAMOND, amount = 5).displayName("&bGem".translateColor()) + +val encbook = metadataItem(Material.ENCHANTED_BOOK) { + displayName = "&4&lThe powerful BOOK".translateColor() + addStoredEnchant(Enchantment.DAMAGE_ALL, 10, true) // putting sharpness 10 to the book +} +``` + + + +## IDEA Plugin - project wizard + +You can find it on a official plugin page +![https://files.gitbook.com/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F1PBqxCHnbDKJwzj8IhJk%2Fuploads%2Fo2jPveKWzw47uGTxMm2Q%2Fimage.png?alt=media&token=403c70a1-fa12-435d-99c2-aab79493de97](img.png) \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 5e2a2ed..5e6f6ad 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,12 +1,12 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { - kotlin("jvm") version "1.9.0" + kotlin("jvm") version "1.9.23" id("io.codearte.nexus-staging") version "0.30.0" } group = "tech.carcadex" -version = "1.0.0.2" +version = project.findProperty("version").toString() repositories { mavenCentral() @@ -34,4 +34,9 @@ tasks.register("publishAll") { dependsOn(tasks.getByPath("serialization:publish")) dependsOn(tasks.getByPath("genref:publish")) dependsOn(tasks.getByPath("messages:publish")) + dependsOn(tasks.getByPath("commands:publish")) + dependsOn(tasks.getByPath("utility:publish")) + dependsOn(tasks.getByPath("exposed:publish")) + dependsOn(tasks.getByPath("thirdparty:publish")) + dependsOn(tasks.getByPath("menu:publish")) } \ No newline at end of file diff --git a/commands/src/main/kotlin/tech/carcadex/kotlinbukkitkit/commands/Test.kt b/commands/src/main/kotlin/tech/carcadex/kotlinbukkitkit/commands/Test.kt new file mode 100644 index 0000000..c573e32 --- /dev/null +++ b/commands/src/main/kotlin/tech/carcadex/kotlinbukkitkit/commands/Test.kt @@ -0,0 +1,12 @@ +package tech.carcadex.kotlinbukkitkit.commands + +import tech.carcadex.kotlinbukkitkit.architecture.KotlinPlugin +import tech.carcadex.kotlinbukkitkit.commands.dsl.builder.simpleCommand + +fun KotlinPlugin.start() { + simpleCommand("test", permission = "test.use") { + val arg1 = string(0) + val arg2 = integer(1) + sender.sendMessage(arg1) + } +} \ No newline at end of file diff --git a/exposed/.gitignore b/exposed/.gitignore new file mode 100644 index 0000000..b63da45 --- /dev/null +++ b/exposed/.gitignore @@ -0,0 +1,42 @@ +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### IntelliJ IDEA ### +.idea/modules.xml +.idea/jarRepositories.xml +.idea/compiler.xml +.idea/libraries/ +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store \ No newline at end of file diff --git a/exposed/build.gradle.kts b/exposed/build.gradle.kts new file mode 100644 index 0000000..e8c007f --- /dev/null +++ b/exposed/build.gradle.kts @@ -0,0 +1,121 @@ +plugins { + id("java") + kotlin("jvm") + kotlin("plugin.serialization") version "1.8.21" + id("com.github.johnrengelman.shadow") version "7.1.2" + id("maven-publish") + id("signing") +} + +group = "tech.carcadex" +val globalArtifactId = "kotlinbukkitkit-exposed" +val globalName = "kotlinbukkitkit-exposed" + +repositories { + mavenCentral() + maven { + name = "spigotmc-repo" + url = uri("https://hub.spigotmc.org/nexus/content/repositories/snapshots/") + } +} + +dependencies { + implementation("org.jetbrains.exposed", "exposed-core", "0.45.0") + implementation("org.jetbrains.exposed", "exposed-dao", "0.45.0") + implementation("com.zaxxer", "HikariCP", "4.0.3") + + compileOnly("org.spigotmc:spigot-api:1.16.5-R0.1-SNAPSHOT") + api(project(":architecture")) + api(project(":extensions")) + api(project(":utility")) + compileOnly("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1") +} + +tasks.test { + useJUnitPlatform() +} + +kotlin { + jvmToolchain(8) +} + +kotlin { + sourceSets.main { + kotlin.srcDir("build/generated/ksp/main/kotlin") + } + sourceSets.test { + kotlin.srcDir("build/generated/ksp/test/kotlin") + } +} + +java { + withJavadocJar() + withSourcesJar() +} + +publishing { + publications { + create("maven") { + artifactId = globalArtifactId + groupId = "tech.carcadex" + version = version + from(components["java"]) + versionMapping { + usage("java-api") { + fromResolutionOf("runtimeClasspath") + } + usage("java-runtime") { + fromResolutionResult() + } + } + pom { + artifactId = globalArtifactId + groupId = "tech.carcadex" + version = version + name.set(globalArtifactId) + description.set(globalArtifactId) + url.set("https://github.com/CarcadeX/KotlinBukkitKit") + + licenses { + license { + name.set("MIT") + url.set("https://mit-license.org/") + } + } + developers { + developer { + id.set("itzRedTea") + name.set("Red Tea") + email.set("red__tea@outlook.com") + } + } + scm { + connection.set("scm:https://github.com/CarcadeX/$globalName.git") + developerConnection.set("git@github.com:CarcadeX/$globalName.git") + url.set("https://github.com/CarcadeX/KotlinBukkitKit") + } + } + + } + } + + + + repositories { + maven { + val releasesRepoUrl = "https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/" + val snapshotsRepoUrl = "https://s01.oss.sonatype.org/content/repositories/snapshots/" + url = uri(if (version.toString().endsWith("SNAPSHOT")) snapshotsRepoUrl else releasesRepoUrl) + credentials { + username = "itzRedTea" + password = project.findProperty("password").toString() + } + } + + } +} + +signing { + useGpgCmd() + sign(publishing.publications.getByName("maven")) +} diff --git a/exposed/src/main/kotlin/tech/carcadex/kotlinbukkitkit/exposed/DatabaseHelper.kt b/exposed/src/main/kotlin/tech/carcadex/kotlinbukkitkit/exposed/DatabaseHelper.kt new file mode 100644 index 0000000..03d4f5b --- /dev/null +++ b/exposed/src/main/kotlin/tech/carcadex/kotlinbukkitkit/exposed/DatabaseHelper.kt @@ -0,0 +1,64 @@ +package tech.carcadex.kotlinbukkitkit.exposed + +import kotlinx.serialization.Serializable +import java.io.File +import java.sql.SQLException + +public interface DatabaseTypeConfigHeader { + public val type: String + public val hostname: String + public val port: Short + public val database: String + public val user: String + public val password: String +} + +@Serializable +public data class DatabaseTypeConfig( + override var type: String = "H2", + override var hostname: String = "localhost", + override var port: Short = 3306, + override var database: String = "kbapi_database", + override var user: String = "root", + override var password: String = "12345", +) : DatabaseTypeConfigHeader + +public fun databaseTypeFrom(dataFolder: File, config: DatabaseTypeConfig): DatabaseType { + val type = DatabaseType.byName(config.type) + if (type != null) { + return when (type) { + DatabaseType.H2::class -> DatabaseType.H2( + dataFolder, + config.database, + ) + DatabaseType.SQLite::class -> DatabaseType.SQLite( + dataFolder, + config.database, + ) + DatabaseType.MySQL::class -> DatabaseType.MySQL( + config.hostname, + config.port, + config.database, + config.user, + config.password, + ) + DatabaseType.PostgreSQL::class -> DatabaseType.PostgreSQL( + config.hostname, + config.port, + config.database, + config.user, + config.password, + ) + DatabaseType.SQLServer::class -> DatabaseType.SQLServer( + config.hostname, + config.port, + config.database, + config.user, + config.password, + ) + else -> throw SQLException("SQL type not finded or supported.") + } + } else { + throw SQLException("SQL type not finded or supported.") + } +} diff --git a/exposed/src/main/kotlin/tech/carcadex/kotlinbukkitkit/exposed/DatabaseType.kt b/exposed/src/main/kotlin/tech/carcadex/kotlinbukkitkit/exposed/DatabaseType.kt new file mode 100644 index 0000000..aa2bf1e --- /dev/null +++ b/exposed/src/main/kotlin/tech/carcadex/kotlinbukkitkit/exposed/DatabaseType.kt @@ -0,0 +1,232 @@ +package tech.carcadex.kotlinbukkitkit.exposed + +import com.zaxxer.hikari.HikariConfig +import com.zaxxer.hikari.HikariDataSource +import java.io.File +import java.io.FileOutputStream +import java.io.IOException +import java.net.URL +import java.net.URLClassLoader +import java.sql.SQLException +import kotlin.reflect.KClass + +private const val KEY_FILE = "{file}" + +public sealed class DatabaseType( + public val name: String, + public val jdbc: String, + public val driverClass: String, + public val driverLink: String, +) { + + public companion object { + public val fileDatabases: Map> = + mapOf("h2" to H2::class, "sqlite" to SQLite::class) + public val remoteDatabases: Map> = + mapOf("mysql" to MySQL::class, "postgresql" to PostgreSQL::class, "sqlserver" to SQLServer::class) + public val databases: Map> = fileDatabases + remoteDatabases + + public fun byName(name: String): KClass? = databases[name.toLowerCase()] + } + + public abstract fun dataSource(): HikariDataSource + + public abstract fun config(): HikariConfig + + public abstract class FileDatabaseType( + name: String, + jdbc: String, + driverClass: String, + driverLink: String, + public val dataFolder: File, + public val file: String, + public val databaseExtension: String, + public val needFileCreation: Boolean, + ) : DatabaseType(name, jdbc, driverClass, driverLink) { + + private val realFile = File(dataFolder, "$file.$databaseExtension") + + override fun dataSource(): HikariDataSource { + if (needFileCreation && !realFile.exists()) realFile.createNewFile() + + loadDependency() + + return HikariDataSource(config()) + } + + override fun config(): HikariConfig = HikariConfig().apply { + driverClassName = driverClass + jdbcUrl = jdbc.replace(KEY_FILE, realFile.path) + } + } + + public abstract class RemoteDatabaseType( + name: String, + jdbc: String, + driverClass: String, + driverLink: String, + public val hostname: String, + public val port: Short, + public val database: String, + public val username: String, + public val password: String, + ) : DatabaseType(name, jdbc, driverClass, driverLink) { + override fun dataSource(): HikariDataSource { + loadDependency() + + return HikariDataSource(config()) + } + + override fun config(): HikariConfig = HikariConfig().apply { + driverClassName = driverClass + jdbcUrl = jdbc + username = this@RemoteDatabaseType.username + password = this@RemoteDatabaseType.password + } + } + + public class H2( + dataFolder: File, + file: String, + ) : FileDatabaseType( + "H2", + "jdbc:h2:file:./$KEY_FILE", + "org.h2.Driver", + "https://repo1.maven.org/maven2/com/h2database/h2/1.4.199/h2-1.4.199.jar", + dataFolder, + file, + "h2.db", + false, + ) + + public class SQLite( + dataFolder: File, + file: String, + ) : FileDatabaseType( + "SQLite", + "jdbc:sqlite:./$KEY_FILE", + "org.sqlite.JDBC", + "https://bitbucket.org/xerial/sqlite-jdbc/downloads/sqlite-jdbc-3.23.1.jar", + dataFolder, + file, + "sqlite.db", + true, + ) { + // https://github.com/brettwooldridge/HikariCP/issues/393#issuecomment-135580191 + override fun config(): HikariConfig { + return super.config().apply { + connectionTestQuery = "SELECT 1" + } + } + } + + public class MySQL( + hostname: String, + port: Short, + database: String, + username: String, + password: String, + ) : RemoteDatabaseType( + "MySQL", + "jdbc:mysql://$hostname:$port/$database", + "com.mysql.jdbc.Driver", + "https://repo1.maven.org/maven2/mysql/mysql-connector-java/8.0.11/mysql-connector-java-8.0.11.jar", + hostname, port, database, + username, password, + ) + + public class PostgreSQL( + hostname: String, + port: Short, + database: String, + username: String, + password: String, + ) : RemoteDatabaseType( + "PostgreSQL", + "jdbc:postgresql://$hostname:$port/$database", + "org.postgresql.Driver", + "https://jdbc.postgresql.org/download/postgresql-42.2.2.jre7.jar", + hostname, port, database, + username, password, + ) + + public class SQLServer( + hostname: String, + port: Short, + database: String, + username: String, + password: String, + ) : RemoteDatabaseType( + "SQLServer", + "jdbc:sqlserver://$hostname:$port;databaseName=$database", + "com.microsoft.sqlserver.jdbc.SQLServerDriver", + "https://repo1.maven.org/maven2/com/microsoft/sqlserver/mssql-jdbc/6.4.0.jre8/mssql-jdbc-6.4.0.jre8.jar", + hostname, port, database, + username, password, + ) + + private val libsFolder = File("klibs").apply { mkdirs() } + private val jarFile = File(libsFolder, "$name-Driver.jar") + + protected fun loadDependency() { + try { + Class.forName(driverClass) + } catch (e: ClassNotFoundException) { + if (jarFile.exists()) { + loadDriver() + } else { + try { + downloadDriver() + } catch (e: Exception) { + throw SQLException("Cant download the driver dependencie of $name") + } + try { + loadDriver() + Class.forName(driverClass) + } catch (e: Exception) { + jarFile.delete() + throw SQLException("Cant load the driver dependencies of $name") + } + } + } + } + + private fun loadDriver() { + val url = jarFile.toURI().toURL() + val classLoader = DatabaseType::class.java.classLoader as URLClassLoader + val method = URLClassLoader::class.java.getDeclaredMethod("addURL", URL::class.java) + method.isAccessible = true + method.invoke(classLoader, url) + } + + private fun downloadDriver() { + val input = URL(driverLink).openStream() + if (jarFile.exists()) { + if (jarFile.isDirectory) { + throw IOException("File '" + jarFile.name + "' is a directory") + } + + if (!jarFile.canWrite()) { + throw IOException("File '" + jarFile.name + "' cannot be written") + } + } else { + val parent = jarFile.parentFile + if (parent != null && !parent.exists() && !parent.mkdirs()) { + throw IOException("File '" + jarFile.name + "' could not be created") + } + } + + val output = FileOutputStream(jarFile) + + val buffer = ByteArray(4096) + var n = input.read(buffer) + while (-1 != n) { + output.write(buffer, 0, n) + + n = input.read(buffer) + } + + input.close() + output.close() + } +} diff --git a/exposed/src/main/kotlin/tech/carcadex/kotlinbukkitkit/exposed/delegate/Block.kt b/exposed/src/main/kotlin/tech/carcadex/kotlinbukkitkit/exposed/delegate/Block.kt new file mode 100644 index 0000000..1b75d94 --- /dev/null +++ b/exposed/src/main/kotlin/tech/carcadex/kotlinbukkitkit/exposed/delegate/Block.kt @@ -0,0 +1,162 @@ +package tech.carcadex.kotlinbukkitkit.exposed.delegate + +import org.bukkit.Bukkit +import org.bukkit.block.Block +import org.jetbrains.exposed.dao.Entity +import org.jetbrains.exposed.sql.Column +import kotlin.reflect.KProperty + +public fun Entity<*>.block(column: Column): ExposedDelegate = BlockExposedDelegate(column) + +@JvmName("blockNullable") +public fun Entity<*>.block(column: Column): ExposedDelegate = BlockExposedDelegateNullable(column) + +public fun Entity<*>.block( + worldColumn: Column, + xColumn: Column, + yColumn: Column, + zColumn: Column, +): ExposedDelegate = BlockMultiColumnExposedDelegate(worldColumn, xColumn, yColumn, zColumn) + +@JvmName("blockNullable") +public fun Entity<*>.nullableBlock( + worldColumn: Column, + xColumn: Column, + yColumn: Column, + zColumn: Column, +): ExposedDelegate = BlockMultiColumnExposedDelegateNullable(worldColumn, xColumn, yColumn, zColumn) + +public class BlockExposedDelegate( + public val column: Column, +) : ExposedDelegate { + override operator fun > getValue( + entity: Entity, + desc: KProperty<*>, + ): Block { + val data = entity.run { column.getValue(this, desc) } + val slices = data.split(";") + val worldName = slices[0] + val world = requireNotNull(Bukkit.getWorld(worldName)) { "World '$worldName' retrieving from database unavailable." } + return world.getBlockAt( + slices[1].toInt(), + slices[2].toInt(), + slices[3].toInt(), + ) + } + + override operator fun > setValue( + entity: Entity, + desc: KProperty<*>, + value: Block, + ) { + val parsed = value.run { "${world.name};$x;$y;$z" } + entity.apply { column.setValue(this, desc, parsed) } + } +} + +public class BlockExposedDelegateNullable( + public val column: Column, +) : ExposedDelegate { + override operator fun > getValue( + entity: Entity, + desc: KProperty<*>, + ): Block? { + val data = entity.run { column.getValue(this, desc) } + val slices = data?.split(";") + return slices?.let { + val worldName = it[0] + val world = requireNotNull(Bukkit.getWorld(worldName)) { "World '$worldName' retrieving from database unavailable." } + world.getBlockAt( + it[1].toInt(), + it[2].toInt(), + it[3].toInt(), + ) + } + } + + override operator fun > setValue( + entity: Entity, + desc: KProperty<*>, + value: Block?, + ) { + val parsed = value?.run { "${world.name};$x;$y;$z" } + entity.apply { column.setValue(this, desc, parsed) } + } +} + +public class BlockMultiColumnExposedDelegate( + public val worldColumn: Column, + public val xColumn: Column, + public val yColumn: Column, + public val zColumn: Column, +) : ExposedDelegate { + override operator fun > getValue( + entity: Entity, + desc: KProperty<*>, + ): Block { + val worldName = entity.run { worldColumn.getValue(this, desc) } + val x = entity.run { xColumn.getValue(this, desc) } + val y = entity.run { yColumn.getValue(this, desc) } + val z = entity.run { zColumn.getValue(this, desc) } + + val world = requireNotNull(Bukkit.getWorld(worldName)) { "World '$worldName' retrieving from database unavailable." } + + return world.getBlockAt(x, y, z) + } + + override operator fun > setValue( + entity: Entity, + desc: KProperty<*>, + value: Block, + ) { + entity.apply { + value.apply { + worldColumn.setValue(entity, desc, world.name) + xColumn.setValue(entity, desc, x) + yColumn.setValue(entity, desc, y) + zColumn.setValue(entity, desc, z) + } + } + } +} + +public class BlockMultiColumnExposedDelegateNullable( + public val worldColumn: Column, + public val xColumn: Column, + public val yColumn: Column, + public val zColumn: Column, +) : ExposedDelegate { + override operator fun > getValue( + entity: Entity, + desc: KProperty<*>, + ): Block? { + val worldName = entity.run { worldColumn.getValue(this, desc) } + val x = entity.run { xColumn.getValue(this, desc) } + val y = entity.run { yColumn.getValue(this, desc) } + val z = entity.run { zColumn.getValue(this, desc) } + + return if ( + worldName != null && + x != null && y != null && z != null + ) { + val world = requireNotNull(Bukkit.getWorld(worldName)) { "World '$worldName' retrieving from database unavailable." } + + world.getBlockAt(x, y, z) + } else { + null + } + } + + override operator fun > setValue( + entity: Entity, + desc: KProperty<*>, + value: Block?, + ) { + entity.apply { + worldColumn.setValue(entity, desc, value?.world?.name) + xColumn.setValue(entity, desc, value?.x) + yColumn.setValue(entity, desc, value?.y) + zColumn.setValue(entity, desc, value?.z) + } + } +} diff --git a/exposed/src/main/kotlin/tech/carcadex/kotlinbukkitkit/exposed/delegate/BlockPos.kt b/exposed/src/main/kotlin/tech/carcadex/kotlinbukkitkit/exposed/delegate/BlockPos.kt new file mode 100644 index 0000000..1331350 --- /dev/null +++ b/exposed/src/main/kotlin/tech/carcadex/kotlinbukkitkit/exposed/delegate/BlockPos.kt @@ -0,0 +1,148 @@ +package tech.carcadex.kotlinbukkitkit.exposed.delegate + +import tech.carcadex.kotlinbukkitkit.utility.types.BlockPos +import org.jetbrains.exposed.dao.Entity +import org.jetbrains.exposed.sql.Column +import kotlin.reflect.KProperty + +public fun Entity<*>.blockPos(column: Column): ExposedDelegate = BlockPosExposedDelegate(column) + +@JvmName("blockPosNullable") +public fun Entity<*>.blockPos(column: Column): ExposedDelegate = BlockPosExposedDelegateNullable(column) + +public fun Entity<*>.blockPos( + xColumn: Column, + yColumn: Column, + zColumn: Column, +): ExposedDelegate = BlockPosMultiColumnExposedDelegate(xColumn, yColumn, zColumn) + +@JvmName("blockPosNullable") +public fun Entity<*>.nullableBlockPos( + xColumn: Column, + yColumn: Column, + zColumn: Column, +): ExposedDelegate = BlockPosMultiColumnExposedDelegateNullable(xColumn, yColumn, zColumn) + +public class BlockPosExposedDelegate( + public val column: Column, +) : ExposedDelegate { + override operator fun > getValue( + entity: Entity, + desc: KProperty<*>, + ): BlockPos { + val data = entity.run { column.getValue(this, desc) } + val slices = data.split(";") + return BlockPos( + slices[0].toInt(), + slices[1].toInt(), + slices[2].toInt(), + ) + } + + override operator fun > setValue( + entity: Entity, + desc: KProperty<*>, + value: BlockPos, + ) { + val parsed = value.run { "$x;$y;$z" } + entity.apply { column.setValue(this, desc, parsed) } + } +} + +public class BlockPosExposedDelegateNullable( + public val column: Column, +) : ExposedDelegate { + override operator fun > getValue( + entity: Entity, + desc: KProperty<*>, + ): BlockPos? { + val data = entity.run { column.getValue(this, desc) } + val slices = data?.split(";") + return slices?.let { + BlockPos( + it[0].toInt(), + it[1].toInt(), + it[2].toInt(), + ) + } + } + + override operator fun > setValue( + entity: Entity, + desc: KProperty<*>, + value: BlockPos?, + ) { + val parsed = value?.run { "$x;$y;$z" } + entity.apply { column.setValue(this, desc, parsed) } + } +} + +public class BlockPosMultiColumnExposedDelegate( + public val xColumn: Column, + public val yColumn: Column, + public val zColumn: Column, +) : ExposedDelegate { + override operator fun > getValue( + entity: Entity, + desc: KProperty<*>, + ): BlockPos { + val x = entity.run { xColumn.getValue(this, desc) } + val y = entity.run { yColumn.getValue(this, desc) } + val z = entity.run { zColumn.getValue(this, desc) } + + return BlockPos(x, y, z) + } + + override operator fun > setValue( + entity: Entity, + desc: KProperty<*>, + value: BlockPos, + ) { + entity.apply { + value.apply { + xColumn.setValue(entity, desc, x) + yColumn.setValue(entity, desc, y) + zColumn.setValue(entity, desc, z) + } + } + } +} + +public class BlockPosMultiColumnExposedDelegateNullable( + public val xColumn: Column, + public val yColumn: Column, + public val zColumn: Column, +) : ExposedDelegate { + override operator fun > getValue( + entity: Entity, + desc: KProperty<*>, + ): BlockPos? { + val x = entity.run { xColumn.getValue(this, desc) } + val y = entity.run { yColumn.getValue(this, desc) } + val z = entity.run { zColumn.getValue(this, desc) } + + return if ( + x != null && y != null && z != null + ) { + BlockPos( + x, + y, + z, + ) + } else { + null + } + } + + override operator fun > setValue( + entity: Entity, + desc: KProperty<*>, + value: BlockPos?, + ) { + entity.apply { + xColumn.setValue(entity, desc, value?.x) + yColumn.setValue(entity, desc, value?.y) + zColumn.setValue(entity, desc, value?.z) + } + } +} diff --git a/exposed/src/main/kotlin/tech/carcadex/kotlinbukkitkit/exposed/delegate/Cached.kt b/exposed/src/main/kotlin/tech/carcadex/kotlinbukkitkit/exposed/delegate/Cached.kt new file mode 100644 index 0000000..d3b6ca9 --- /dev/null +++ b/exposed/src/main/kotlin/tech/carcadex/kotlinbukkitkit/exposed/delegate/Cached.kt @@ -0,0 +1,35 @@ +package tech.carcadex.kotlinbukkitkit.exposed.delegate + +import org.jetbrains.exposed.dao.Entity +import kotlin.reflect.KProperty + +public fun ExposedDelegate.cached(): ExposedDelegate = CachedExposedDelegate(this) + +public class CachedExposedDelegate( + private val delegate: ExposedDelegate, +) : ExposedDelegate { + private var cache: T? = null + private var isCached: Boolean = false + + override fun > getValue( + entity: Entity, + desc: KProperty<*>, + ): T { + if (!isCached) { + cache = delegate.getValue(entity, desc) + isCached = true + } + + return cache!! + } + + override fun > setValue( + entity: Entity, + desc: KProperty<*>, + value: T, + ) { + delegate.setValue(entity, desc, value) + cache = value + isCached = true + } +} diff --git a/exposed/src/main/kotlin/tech/carcadex/kotlinbukkitkit/exposed/delegate/Chunk.kt b/exposed/src/main/kotlin/tech/carcadex/kotlinbukkitkit/exposed/delegate/Chunk.kt new file mode 100644 index 0000000..627f85e --- /dev/null +++ b/exposed/src/main/kotlin/tech/carcadex/kotlinbukkitkit/exposed/delegate/Chunk.kt @@ -0,0 +1,152 @@ +package tech.carcadex.kotlinbukkitkit.exposed.delegate + +import org.bukkit.Bukkit +import org.bukkit.Chunk +import org.jetbrains.exposed.dao.Entity +import org.jetbrains.exposed.sql.Column +import kotlin.reflect.KProperty + +public fun Entity<*>.chunk(column: Column): ExposedDelegate = ChunkExposedDelegate(column) + +@JvmName("chunkNullable") +public fun Entity<*>.chunk(column: Column): ExposedDelegate = ChunkExposedDelegateNullable(column) + +public fun Entity<*>.chunk( + worldColumn: Column, + xColumn: Column, + zColumn: Column, +): ExposedDelegate = ChunkMultiColumnExposedDelegate(worldColumn, xColumn, zColumn) + +@JvmName("chunkNullable") +public fun Entity<*>.chunk( + worldColumn: Column, + xColumn: Column, + zColumn: Column, +): ExposedDelegate = ChunkMultiColumnExposedDelegateNullable(worldColumn, xColumn, zColumn) + +public class ChunkExposedDelegate( + public val column: Column, +) : ExposedDelegate { + override operator fun > getValue( + entity: Entity, + desc: KProperty<*>, + ): Chunk { + val data = entity.run { column.getValue(this, desc) } + val slices = data.split(";") + val worldName = slices[0] + val world = requireNotNull(Bukkit.getWorld(worldName)) { "World '$worldName' retrieving from database unavailable." } + return world.getChunkAt( + slices[1].toInt(), + slices[2].toInt(), + ) + } + + override operator fun > setValue( + entity: Entity, + desc: KProperty<*>, + value: Chunk, + ) { + val parsed = value.run { "${world.name};$x;$z" } + entity.apply { column.setValue(this, desc, parsed) } + } +} + +public class ChunkExposedDelegateNullable( + public val column: Column, +) : ExposedDelegate { + override operator fun > getValue( + entity: Entity, + desc: KProperty<*>, + ): Chunk? { + val data = entity.run { column.getValue(this, desc) } + val slices = data?.split(";") + return slices?.let { + val worldName = it[0] + val world = requireNotNull(Bukkit.getWorld(worldName)) { "World '$worldName' retrieving from database unavailable." } + world.getChunkAt( + it[1].toInt(), + it[2].toInt(), + ) + } + } + + override operator fun > setValue( + entity: Entity, + desc: KProperty<*>, + value: Chunk?, + ) { + val parsed = value?.run { "${world.name};$x;$z" } + entity.apply { column.setValue(this, desc, parsed) } + } +} + +public class ChunkMultiColumnExposedDelegate( + public val worldColumn: Column, + public val xColumn: Column, + public val zColumn: Column, +) : ExposedDelegate { + override operator fun > getValue( + entity: Entity, + desc: KProperty<*>, + ): Chunk { + val worldName = entity.run { worldColumn.getValue(this, desc) } + val x = entity.run { xColumn.getValue(this, desc) } + val z = entity.run { zColumn.getValue(this, desc) } + + val world = requireNotNull(Bukkit.getWorld(worldName)) { "World '$worldName' retrieving from database unavailable." } + + return world.getChunkAt(x, z) + } + + override operator fun > setValue( + entity: Entity, + desc: KProperty<*>, + value: Chunk, + ) { + entity.apply { + value.apply { + worldColumn.setValue(entity, desc, world.name) + xColumn.setValue(entity, desc, x) + zColumn.setValue(entity, desc, z) + } + } + } +} + +public class ChunkMultiColumnExposedDelegateNullable( + public val worldColumn: Column, + public val xColumn: Column, + public val zColumn: Column, +) : ExposedDelegate { + override operator fun > getValue( + entity: Entity, + desc: KProperty<*>, + ): Chunk? { + val worldName = entity.run { worldColumn.getValue(this, desc) } + val x = entity.run { xColumn.getValue(this, desc) } + val z = entity.run { zColumn.getValue(this, desc) } + + return if ( + worldName != null && + x != null && z != null + ) { + val world = requireNotNull(Bukkit.getWorld(worldName)) { "World '$worldName' retrieving from database unavailable." } + + world.getChunkAt(x, z) + } else { + null + } + } + + override operator fun > setValue( + entity: Entity, + desc: KProperty<*>, + value: Chunk?, + ) { + entity.apply { + worldColumn.setValue(entity, desc, value?.world?.name) + xColumn.setValue(entity, desc, value?.x) + zColumn.setValue(entity, desc, value?.z) + } + } +} diff --git a/exposed/src/main/kotlin/tech/carcadex/kotlinbukkitkit/exposed/delegate/ChunkPos.kt b/exposed/src/main/kotlin/tech/carcadex/kotlinbukkitkit/exposed/delegate/ChunkPos.kt new file mode 100644 index 0000000..089124d --- /dev/null +++ b/exposed/src/main/kotlin/tech/carcadex/kotlinbukkitkit/exposed/delegate/ChunkPos.kt @@ -0,0 +1,137 @@ +package tech.carcadex.kotlinbukkitkit.exposed.delegate + +import tech.carcadex.kotlinbukkitkit.utility.types.ChunkPos +import org.jetbrains.exposed.dao.Entity +import org.jetbrains.exposed.sql.Column +import kotlin.reflect.KProperty + +public fun Entity<*>.chunkPos(column: Column): ExposedDelegate = ChunkPosExposedDelegate(column) + +@JvmName("chunkPosNullable") +public fun Entity<*>.chunkPos(column: Column): ExposedDelegate = ChunkPosExposedDelegateNullable(column) + +public fun Entity<*>.chunkPos( + xColumn: Column, + zColumn: Column, +): ExposedDelegate = ChunkPosMultiColumnExposedDelegate(xColumn, zColumn) + +@JvmName("chunkPosNullable") +public fun Entity<*>.chunkPos( + xColumn: Column, + zColumn: Column, +): ExposedDelegate = ChunkPosMultiColumnExposedDelegateNullable(xColumn, zColumn) + +public class ChunkPosExposedDelegate( + public val column: Column, +) : ExposedDelegate { + override operator fun > getValue( + entity: Entity, + desc: KProperty<*>, + ): ChunkPos { + val data = entity.run { column.getValue(this, desc) } + val slices = data.split(";") + return ChunkPos( + slices[0].toInt(), + slices[1].toInt(), + ) + } + + override operator fun > setValue( + entity: Entity, + desc: KProperty<*>, + value: ChunkPos, + ) { + val parsed = value.run { "$x;$z" } + entity.apply { column.setValue(this, desc, parsed) } + } +} + +public class ChunkPosExposedDelegateNullable( + public val column: Column, +) : ExposedDelegate { + override operator fun > getValue( + entity: Entity, + desc: KProperty<*>, + ): ChunkPos? { + val data = entity.run { column.getValue(this, desc) } + val slices = data?.split(";") + return slices?.let { + ChunkPos( + it[0].toInt(), + it[1].toInt(), + ) + } + } + + override operator fun > setValue( + entity: Entity, + desc: KProperty<*>, + value: ChunkPos?, + ) { + val parsed = value?.run { "$x;$z" } + entity.apply { column.setValue(this, desc, parsed) } + } +} + +public class ChunkPosMultiColumnExposedDelegate( + public val xColumn: Column, + public val zColumn: Column, +) : ExposedDelegate { + override operator fun > getValue( + entity: Entity, + desc: KProperty<*>, + ): ChunkPos { + val x = entity.run { xColumn.getValue(this, desc) } + val z = entity.run { zColumn.getValue(this, desc) } + + return ChunkPos(x, z) + } + + override operator fun > setValue( + entity: Entity, + desc: KProperty<*>, + value: ChunkPos, + ) { + entity.apply { + value.apply { + xColumn.setValue(entity, desc, x) + zColumn.setValue(entity, desc, z) + } + } + } +} + +public class ChunkPosMultiColumnExposedDelegateNullable( + public val xColumn: Column, + public val zColumn: Column, +) : ExposedDelegate { + override operator fun > getValue( + entity: Entity, + desc: KProperty<*>, + ): ChunkPos? { + val x = entity.run { xColumn.getValue(this, desc) } + val z = entity.run { zColumn.getValue(this, desc) } + + return if ( + x != null && z != null + ) { + ChunkPos( + x, + z, + ) + } else { + null + } + } + + override operator fun > setValue( + entity: Entity, + desc: KProperty<*>, + value: ChunkPos?, + ) { + entity.apply { + xColumn.setValue(entity, desc, value?.x) + zColumn.setValue(entity, desc, value?.z) + } + } +} diff --git a/exposed/src/main/kotlin/tech/carcadex/kotlinbukkitkit/exposed/delegate/ExposedDelegate.kt b/exposed/src/main/kotlin/tech/carcadex/kotlinbukkitkit/exposed/delegate/ExposedDelegate.kt new file mode 100644 index 0000000..d3fec6d --- /dev/null +++ b/exposed/src/main/kotlin/tech/carcadex/kotlinbukkitkit/exposed/delegate/ExposedDelegate.kt @@ -0,0 +1,18 @@ +package tech.carcadex.kotlinbukkitkit.exposed.delegate + +import org.jetbrains.exposed.dao.Entity +import kotlin.reflect.KProperty + +public interface ExposedDelegate { + + public operator fun > getValue( + entity: Entity, + desc: KProperty<*>, + ): T + + public operator fun > setValue( + entity: Entity, + desc: KProperty<*>, + value: T, + ) +} diff --git a/exposed/src/main/kotlin/tech/carcadex/kotlinbukkitkit/exposed/delegate/Item.kt b/exposed/src/main/kotlin/tech/carcadex/kotlinbukkitkit/exposed/delegate/Item.kt new file mode 100644 index 0000000..9fc2cbc --- /dev/null +++ b/exposed/src/main/kotlin/tech/carcadex/kotlinbukkitkit/exposed/delegate/Item.kt @@ -0,0 +1,82 @@ +package tech.carcadex.kotlinbukkitkit.exposed.delegate + +import org.bukkit.inventory.ItemStack +import org.bukkit.util.io.BukkitObjectInputStream +import org.bukkit.util.io.BukkitObjectOutputStream +import org.jetbrains.exposed.dao.Entity +import org.jetbrains.exposed.sql.Column +import org.jetbrains.exposed.sql.statements.api.ExposedBlob +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import kotlin.reflect.KProperty + +/** + * NOTE: This not save NBT and could break version to version, if you pretend + * to upgrade your server and this keep the same, I can't guarantee that. + */ +public fun Entity<*>.itemStack(column: Column): ExposedDelegate = ItemStackExposedDelegate(column) + +@JvmName("itemStackNullable") +public fun Entity<*>.itemStack(column: Column): ExposedDelegate = ItemStackExposedDelegateNullable(column) + +public class ItemStackExposedDelegate( + public val column: Column, +) : ExposedDelegate { + override operator fun > getValue( + entity: Entity, + desc: KProperty<*>, + ): ItemStack { + val blob = entity.run { column.getValue(this, desc) } + return toItemStack(blob.bytes) + } + + override operator fun > setValue( + entity: Entity, + desc: KProperty<*>, + value: ItemStack, + ) { + val byteArray = toByteArray(value) + val blob = ExposedBlob(byteArray) + entity.apply { column.setValue(this, desc, blob) } + } +} + +public class ItemStackExposedDelegateNullable( + public val column: Column, +) : ExposedDelegate { + override operator fun > getValue( + entity: Entity, + desc: KProperty<*>, + ): ItemStack? { + val blob = entity.run { column.getValue(this, desc) } + return blob?.bytes?.let { toItemStack(it) } + } + + override operator fun > setValue( + entity: Entity, + desc: KProperty<*>, + value: ItemStack?, + ) { + val byteArray = value?.let { toByteArray(it) } + val blob = byteArray?.let { ExposedBlob(it) } + entity.apply { column.setValue(this, desc, blob) } + } +} + +private fun toByteArray(item: ItemStack): ByteArray { + val outputStream = ByteArrayOutputStream() + + BukkitObjectOutputStream(outputStream).use { + it.writeObject(item) + } + + return outputStream.toByteArray() +} + +private fun toItemStack(byteArray: ByteArray): ItemStack { + return ByteArrayInputStream(byteArray).use { + BukkitObjectInputStream(it).use { + it.readObject() as ItemStack + } + } +} diff --git a/exposed/src/main/kotlin/tech/carcadex/kotlinbukkitkit/exposed/delegate/Location.kt b/exposed/src/main/kotlin/tech/carcadex/kotlinbukkitkit/exposed/delegate/Location.kt new file mode 100644 index 0000000..f25851f --- /dev/null +++ b/exposed/src/main/kotlin/tech/carcadex/kotlinbukkitkit/exposed/delegate/Location.kt @@ -0,0 +1,190 @@ +package tech.carcadex.kotlinbukkitkit.exposed.delegate + +import tech.carcadex.kotlinbukkitkit.extensions.world +import org.bukkit.Bukkit +import org.bukkit.Location +import org.jetbrains.exposed.dao.Entity +import org.jetbrains.exposed.sql.Column +import kotlin.reflect.KProperty + +public fun Entity<*>.location(column: Column): ExposedDelegate = LocationExposedDelegate(column) + +@JvmName("locationNullable") +public fun Entity<*>.location(column: Column): ExposedDelegate = LocationExposedDelegateNullable(column) + +public fun Entity<*>.location( + worldColumn: Column, + xColumn: Column, + yColumn: Column, + zColumn: Column, + yawColumn: Column, + pitchColumn: Column, +): ExposedDelegate = LocationMultiColumnExposedDelegate(worldColumn, xColumn, yColumn, zColumn, yawColumn, pitchColumn) + +@JvmName("locationNullable") +public fun Entity<*>.location( + worldColumn: Column, + xColumn: Column, + yColumn: Column, + zColumn: Column, + yawColumn: Column, + pitchColumn: Column, +): ExposedDelegate = LocationMultiColumnExposedDelegateNullable(worldColumn, xColumn, yColumn, zColumn, yawColumn, pitchColumn) + +public class LocationExposedDelegate( + public val column: Column, +) : ExposedDelegate { + override operator fun > getValue( + entity: Entity, + desc: KProperty<*>, + ): Location { + val data = entity.run { column.getValue(this, desc) } + val slices = data.split(";") + return Location( + Bukkit.getWorld(slices[0]), + slices[1].toDouble(), + slices[2].toDouble(), + slices[3].toDouble(), + slices[4].toFloat(), + slices[5].toFloat(), + ) + } + + override operator fun > setValue( + entity: Entity, + desc: KProperty<*>, + value: Location, + ) { + val world = requireNotNull(value.world) { "Location with nullable world can't be stored in database" } + val parsed = value.run { "${world.name};$x;$y;$z;$yaw;$pitch" } + entity.apply { column.setValue(this, desc, parsed) } + } +} + +public class LocationExposedDelegateNullable( + public val column: Column, +) : ExposedDelegate { + override operator fun > getValue( + entity: Entity, + desc: KProperty<*>, + ): Location? { + val data = entity.run { column.getValue(this, desc) } + val slices = data?.split(";") + return slices?.let { + Location( + Bukkit.getWorld(it[0]), + it[1].toDouble(), + it[2].toDouble(), + it[3].toDouble(), + it[4].toFloat(), + it[5].toFloat(), + ) + } + } + + override operator fun > setValue( + entity: Entity, + desc: KProperty<*>, + value: Location?, + ) { + val parsed = value?.run { + val world = requireNotNull(world) { "Location with nullable world can't be stored in database" } + "${world.name};$x;$y;$z;$yaw;$pitch" + } + entity.apply { column.setValue(this, desc, parsed) } + } +} + +public class LocationMultiColumnExposedDelegate( + public val worldColumn: Column, + public val xColumn: Column, + public val yColumn: Column, + public val zColumn: Column, + public val yawColumn: Column, + public val pitchColumn: Column, +) : ExposedDelegate { + override operator fun > getValue( + entity: Entity, + desc: KProperty<*>, + ): Location { + val worldName = entity.run { worldColumn.getValue(this, desc) } + val x = entity.run { xColumn.getValue(this, desc) } + val y = entity.run { yColumn.getValue(this, desc) } + val z = entity.run { zColumn.getValue(this, desc) } + val yaw = entity.run { yawColumn.getValue(this, desc) } + val pitch = entity.run { pitchColumn.getValue(this, desc) } + + return Location( + Bukkit.getWorld(worldName), + x, + y, + z, + yaw, + pitch, + ) + } + + override operator fun > setValue( + entity: Entity, + desc: KProperty<*>, + value: Location, + ) { + entity.apply { + value.apply { + val world = requireNotNull(world) { "Location with nullable world can't be stored in database" } + worldColumn.setValue(entity, desc, world.name) + xColumn.setValue(entity, desc, x) + yColumn.setValue(entity, desc, y) + zColumn.setValue(entity, desc, z) + yawColumn.setValue(entity, desc, yaw) + pitchColumn.setValue(entity, desc, pitch) + } + } + } +} + +public class LocationMultiColumnExposedDelegateNullable( + public val worldColumn: Column, + public val xColumn: Column, + public val yColumn: Column, + public val zColumn: Column, + public val yawColumn: Column, + public val pitchColumn: Column, +) : ExposedDelegate { + override operator fun > getValue( + entity: Entity, + desc: KProperty<*>, + ): Location? { + val worldName = entity.run { worldColumn.getValue(this, desc) } + val x = entity.run { xColumn.getValue(this, desc) } + val y = entity.run { yColumn.getValue(this, desc) } + val z = entity.run { zColumn.getValue(this, desc) } + val yaw = entity.run { yawColumn.getValue(this, desc) } + val pitch = entity.run { pitchColumn.getValue(this, desc) } + + return if ( + worldName != null && + x != null && y != null && z != null && + yaw != null && pitch != null + ) { + Location(Bukkit.getWorld(worldName), x, y, z, yaw, pitch) + } else { + null + } + } + + override operator fun > setValue( + entity: Entity, + desc: KProperty<*>, + value: Location?, + ) { + entity.apply { + worldColumn.setValue(entity, desc, value?.world?.name) + xColumn.setValue(entity, desc, value?.x) + yColumn.setValue(entity, desc, value?.y) + zColumn.setValue(entity, desc, value?.z) + yawColumn.setValue(entity, desc, value?.yaw) + pitchColumn.setValue(entity, desc, value?.pitch) + } + } +} diff --git a/exposed/src/main/kotlin/tech/carcadex/kotlinbukkitkit/exposed/delegate/LocationPos.kt b/exposed/src/main/kotlin/tech/carcadex/kotlinbukkitkit/exposed/delegate/LocationPos.kt new file mode 100644 index 0000000..b4d67ab --- /dev/null +++ b/exposed/src/main/kotlin/tech/carcadex/kotlinbukkitkit/exposed/delegate/LocationPos.kt @@ -0,0 +1,177 @@ +package tech.carcadex.kotlinbukkitkit.exposed.delegate + +import tech.carcadex.kotlinbukkitkit.utility.types.LocationPos +import org.jetbrains.exposed.dao.Entity +import org.jetbrains.exposed.sql.Column +import kotlin.reflect.KProperty + +public fun Entity<*>.locationPos(column: Column): ExposedDelegate = LocationPosExposedDelegate(column) + +@JvmName("locationPosNullable") +public fun Entity<*>.locationPos(column: Column): ExposedDelegate = LocationPosExposedDelegateNullable(column) + +public fun Entity<*>.locationPos( + xColumn: Column, + yColumn: Column, + zColumn: Column, + yawColumn: Column, + pitchColumn: Column, +): ExposedDelegate = LocationPosMultiColumnExposedDelegate(xColumn, yColumn, zColumn, yawColumn, pitchColumn) + +@JvmName("locationPosNullable") +public fun Entity<*>.locationPos( + xColumn: Column, + yColumn: Column, + zColumn: Column, + yawColumn: Column, + pitchColumn: Column, +): ExposedDelegate = LocationPosMultiColumnExposedDelegateNullable(xColumn, yColumn, zColumn, yawColumn, pitchColumn) + +public class LocationPosExposedDelegate( + public val column: Column, +) : ExposedDelegate { + override operator fun > getValue( + entity: Entity, + desc: KProperty<*>, + ): LocationPos { + val data = entity.run { column.getValue(this, desc) } + val slices = data.split(";") + return LocationPos( + slices[0].toDouble(), + slices[1].toDouble(), + slices[2].toDouble(), + slices[3].toFloat(), + slices[4].toFloat(), + ) + } + + override operator fun > setValue( + entity: Entity, + desc: KProperty<*>, + value: LocationPos, + ) { + val parsed = value.run { "$x;$y;$z;$yaw;$pitch" } + entity.apply { column.setValue(this, desc, parsed) } + } +} + +public class LocationPosExposedDelegateNullable( + public val column: Column, +) : ExposedDelegate { + override operator fun > getValue( + entity: Entity, + desc: KProperty<*>, + ): LocationPos? { + val data = entity.run { column.getValue(this, desc) } + val slices = data?.split(";") + return slices?.let { + LocationPos( + it[0].toDouble(), + it[1].toDouble(), + it[2].toDouble(), + it[3].toFloat(), + it[4].toFloat(), + ) + } + } + + override operator fun > setValue( + entity: Entity, + desc: KProperty<*>, + value: LocationPos?, + ) { + val parsed = value?.run { "$x;$y;$z;$yaw;$pitch" } + entity.apply { column.setValue(this, desc, parsed) } + } +} + +public class LocationPosMultiColumnExposedDelegate( + public val xColumn: Column, + public val yColumn: Column, + public val zColumn: Column, + public val yawColumn: Column, + public val pitchColumn: Column, +) : ExposedDelegate { + override operator fun > getValue( + entity: Entity, + desc: KProperty<*>, + ): LocationPos { + val x = entity.run { xColumn.getValue(this, desc) } + val y = entity.run { yColumn.getValue(this, desc) } + val z = entity.run { zColumn.getValue(this, desc) } + val yaw = entity.run { yawColumn.getValue(this, desc) } + val pitch = entity.run { pitchColumn.getValue(this, desc) } + + return LocationPos( + x, + y, + z, + yaw, + pitch, + ) + } + + override operator fun > setValue( + entity: Entity, + desc: KProperty<*>, + value: LocationPos, + ) { + entity.apply { + value.apply { + xColumn.setValue(entity, desc, x) + yColumn.setValue(entity, desc, y) + zColumn.setValue(entity, desc, z) + yawColumn.setValue(entity, desc, yaw) + pitchColumn.setValue(entity, desc, pitch) + } + } + } +} + +public class LocationPosMultiColumnExposedDelegateNullable( + public val xColumn: Column, + public val yColumn: Column, + public val zColumn: Column, + public val yawColumn: Column, + public val pitchColumn: Column, +) : ExposedDelegate { + override operator fun > getValue( + entity: Entity, + desc: KProperty<*>, + ): LocationPos? { + val x = entity.run { xColumn.getValue(this, desc) } + val y = entity.run { yColumn.getValue(this, desc) } + val z = entity.run { zColumn.getValue(this, desc) } + val yaw = entity.run { yawColumn.getValue(this, desc) } + val pitch = entity.run { pitchColumn.getValue(this, desc) } + + return if ( + x != null && y != null && z != null && + yaw != null && pitch != null + ) { + LocationPos( + x, + y, + z, + yaw, + pitch, + ) + } else { + null + } + } + + override operator fun > setValue( + entity: Entity, + desc: KProperty<*>, + value: LocationPos?, + ) { + entity.apply { + xColumn.setValue(entity, desc, value?.x) + yColumn.setValue(entity, desc, value?.y) + zColumn.setValue(entity, desc, value?.z) + yawColumn.setValue(entity, desc, value?.yaw) + pitchColumn.setValue(entity, desc, value?.pitch) + } + } +} diff --git a/exposed/src/main/kotlin/tech/carcadex/kotlinbukkitkit/exposed/delegate/OfflinePlayer.kt b/exposed/src/main/kotlin/tech/carcadex/kotlinbukkitkit/exposed/delegate/OfflinePlayer.kt new file mode 100644 index 0000000..448c945 --- /dev/null +++ b/exposed/src/main/kotlin/tech/carcadex/kotlinbukkitkit/exposed/delegate/OfflinePlayer.kt @@ -0,0 +1,47 @@ +package tech.carcadex.kotlinbukkitkit.exposed.delegate + +import tech.carcadex.kotlinbukkitkit.extensions.offlinePlayer +import org.bukkit.OfflinePlayer +import org.jetbrains.exposed.dao.Entity +import org.jetbrains.exposed.sql.Column +import java.util.* +import kotlin.reflect.KProperty + +public fun Entity<*>.offlinePlayer(column: Column): ExposedDelegate = OfflinePlayerExposedDelegate(column) + +@JvmName("offlinePlayerNullable") +public fun Entity<*>.offlinePlayer(column: Column): ExposedDelegate = OfflinePlayerExposedDelegateNullable(column) + +public class OfflinePlayerExposedDelegate( + public val column: Column, +) : ExposedDelegate { + override operator fun > getValue( + entity: Entity, + desc: KProperty<*>, + ): OfflinePlayer { + val uuid = entity.run { column.getValue(this, desc) } + return offlinePlayer(uuid) + } + override operator fun > setValue(entity: Entity, desc: KProperty<*>, value: OfflinePlayer) { + entity.apply { column.setValue(this, desc, value.uniqueId) } + } +} + +public class OfflinePlayerExposedDelegateNullable( + public val column: Column, +) : ExposedDelegate { + override operator fun > getValue( + entity: Entity, + desc: KProperty<*>, + ): OfflinePlayer? { + val uuid = entity.run { column.getValue(this, desc) } + return uuid?.let { offlinePlayer(it) } + } + override operator fun > setValue( + entity: Entity, + desc: KProperty<*>, + value: OfflinePlayer?, + ) { + entity.apply { column.setValue(this, desc, value?.uniqueId) } + } +} diff --git a/exposed/src/main/kotlin/tech/carcadex/kotlinbukkitkit/exposed/delegate/World.kt b/exposed/src/main/kotlin/tech/carcadex/kotlinbukkitkit/exposed/delegate/World.kt new file mode 100644 index 0000000..9bae40b --- /dev/null +++ b/exposed/src/main/kotlin/tech/carcadex/kotlinbukkitkit/exposed/delegate/World.kt @@ -0,0 +1,52 @@ +package tech.carcadex.kotlinbukkitkit.exposed.delegate + +import org.bukkit.Bukkit +import org.bukkit.World +import org.jetbrains.exposed.dao.Entity +import org.jetbrains.exposed.sql.Column +import kotlin.reflect.KProperty + +public fun Entity<*>.world(column: Column): ExposedDelegate = WorldExposedDelegate(column) + +@JvmName("worldNullable") +public fun Entity<*>.world(column: Column): ExposedDelegate = WorldExposedDelegateNullable(column) + +public class WorldExposedDelegate( + public val column: Column, +) : ExposedDelegate { + override operator fun > getValue( + entity: Entity, + desc: KProperty<*>, + ): World { + val data = entity.run { column.getValue(this, desc) } + return requireNotNull(Bukkit.getWorld(data)) { "World '$data' retrieving from database not loaded at server." } + } + + override operator fun > setValue( + entity: Entity, + desc: KProperty<*>, + value: World, + ) { + entity.apply { column.setValue(this, desc, value.name) } + } +} + +public class WorldExposedDelegateNullable( + public val column: Column, +) : ExposedDelegate { + override operator fun > getValue( + entity: Entity, + desc: KProperty<*>, + ): World? { + val data = entity.run { column.getValue(this, desc) } + return data?.let { Bukkit.getWorld(it) } + } + + override operator fun > setValue( + entity: Entity, + desc: KProperty<*>, + value: World?, + ) { + entity.apply { column.setValue(this, desc, value?.name) } + } +} diff --git a/gradle.properties b/gradle.properties index 3df5c1c..efca688 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=1.0.0.2 +version=1.0.0.3 signing.gnupg.executable=***REMOVED*** signing.gnupg.useLegacyGpg=false diff --git a/menu/.gitignore b/menu/.gitignore new file mode 100644 index 0000000..b63da45 --- /dev/null +++ b/menu/.gitignore @@ -0,0 +1,42 @@ +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### IntelliJ IDEA ### +.idea/modules.xml +.idea/jarRepositories.xml +.idea/compiler.xml +.idea/libraries/ +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store \ No newline at end of file diff --git a/menu/build.gradle.kts b/menu/build.gradle.kts new file mode 100644 index 0000000..649e5aa --- /dev/null +++ b/menu/build.gradle.kts @@ -0,0 +1,117 @@ +plugins { + id("java") + kotlin("jvm") + kotlin("plugin.serialization") version "1.8.21" + id("com.github.johnrengelman.shadow") version "7.1.2" + id("maven-publish") + id("signing") +} + +group = "tech.carcadex" +val globalArtifactId = "kotlinbukkitkit-menu" +val globalName = "kotlinbukkitkit-menu" + +repositories { + mavenCentral() + maven { + name = "spigotmc-repo" + url = uri("https://hub.spigotmc.org/nexus/content/repositories/snapshots/") + } +} + +dependencies { + compileOnly("org.spigotmc:spigot-api:1.16.5-R0.1-SNAPSHOT") + api(project(":architecture")) + api(project(":extensions")) + api(project(":utility")) + compileOnly("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1") +} + +tasks.test { + useJUnitPlatform() +} + +kotlin { + jvmToolchain(8) +} + +kotlin { + sourceSets.main { + kotlin.srcDir("build/generated/ksp/main/kotlin") + } + sourceSets.test { + kotlin.srcDir("build/generated/ksp/test/kotlin") + } +} + +java { + withJavadocJar() + withSourcesJar() +} + +publishing { + publications { + create("maven") { + artifactId = globalArtifactId + groupId = "tech.carcadex" + version = version + from(components["java"]) + versionMapping { + usage("java-api") { + fromResolutionOf("runtimeClasspath") + } + usage("java-runtime") { + fromResolutionResult() + } + } + pom { + artifactId = globalArtifactId + groupId = "tech.carcadex" + version = version + name.set(globalArtifactId) + description.set(globalArtifactId) + url.set("https://github.com/CarcadeX/KotlinBukkitKit") + + licenses { + license { + name.set("MIT") + url.set("https://mit-license.org/") + } + } + developers { + developer { + id.set("itzRedTea") + name.set("Red Tea") + email.set("red__tea@outlook.com") + } + } + scm { + connection.set("scm:https://github.com/CarcadeX/$globalName.git") + developerConnection.set("git@github.com:CarcadeX/$globalName.git") + url.set("https://github.com/CarcadeX/KotlinBukkitKit") + } + } + + } + } + + + + repositories { + maven { + val releasesRepoUrl = "https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/" + val snapshotsRepoUrl = "https://s01.oss.sonatype.org/content/repositories/snapshots/" + url = uri(if (version.toString().endsWith("SNAPSHOT")) snapshotsRepoUrl else releasesRepoUrl) + credentials { + username = "itzRedTea" + password = project.findProperty("password").toString() + } + } + + } +} + +signing { + useGpgCmd() + sign(publishing.publications.getByName("maven")) +} diff --git a/menu/src/main/kotlin/tech/carcadex/kotlinbukkitkit/menu/Menu.kt b/menu/src/main/kotlin/tech/carcadex/kotlinbukkitkit/menu/Menu.kt new file mode 100644 index 0000000..265af1d --- /dev/null +++ b/menu/src/main/kotlin/tech/carcadex/kotlinbukkitkit/menu/Menu.kt @@ -0,0 +1,51 @@ +package tech.carcadex.kotlinbukkitkit.menu + +import tech.carcadex.kotlinbukkitkit.architecture.extensions.WithPlugin +import tech.carcadex.kotlinbukkitkit.menu.slot.Slot +import org.bukkit.entity.Player +import org.bukkit.inventory.Inventory +import org.bukkit.inventory.InventoryHolder +import org.bukkit.plugin.Plugin +import java.util.* + +public interface Menu : WithPlugin, InventoryHolder { + + public var title: String + public var lines: Int + public var cancelOnClick: Boolean + + public val viewers: Map + public val slots: TreeMap + + public val data: WeakHashMap + public val playerData: WeakHashMap> + + public val eventHandler: MenuEventHandler + + public var baseSlot: T + public var updateDelay: Long + + public fun setSlot(slot: Int, slotObj: T) + + public fun update(players: Set = viewers.keys) + public fun updateSlot(slot: Slot, players: Set = viewers.keys) + + public fun update(vararg players: Player): Unit = update(players.toSet()) + public fun updateSlot(slot: Slot, vararg players: Player): Unit = updateSlot(slot, players.toSet()) + + public fun openToPlayer(vararg players: Player) + + public fun clearData() { + data.clear() + for (slot in slotsWithBaseSlot()) + slot.clearSlotData() + } + + public fun clearPlayerData(player: Player) { + playerData.remove(player) + for (slot in slotsWithBaseSlot()) + slot.clearPlayerData(player) + } + + public fun close(player: Player, closeInventory: Boolean = true) +} diff --git a/menu/src/main/kotlin/tech/carcadex/kotlinbukkitkit/menu/MenuController.kt b/menu/src/main/kotlin/tech/carcadex/kotlinbukkitkit/menu/MenuController.kt new file mode 100644 index 0000000..cec5cd3 --- /dev/null +++ b/menu/src/main/kotlin/tech/carcadex/kotlinbukkitkit/menu/MenuController.kt @@ -0,0 +1,105 @@ +package tech.carcadex.kotlinbukkitkit.menu + +import tech.carcadex.kotlinbukkitkit.architecture.PluginDisableAware +import tech.carcadex.kotlinbukkitkit.architecture.PluginDisableAwareController +import tech.carcadex.kotlinbukkitkit.extensions.KListener +import tech.carcadex.kotlinbukkitkit.extensions.onlinePlayers +import tech.carcadex.kotlinbukkitkit.extensions.registerEvents +import tech.carcadex.kotlinbukkitkit.menu.slot.MenuPlayerSlotInteract +import org.bukkit.entity.Player +import org.bukkit.event.EventHandler +import org.bukkit.event.inventory.InventoryClickEvent +import org.bukkit.event.inventory.InventoryCloseEvent +import org.bukkit.event.inventory.InventoryDragEvent +import org.bukkit.event.inventory.InventoryType +import org.bukkit.event.player.PlayerPickupItemEvent +import org.bukkit.plugin.Plugin + +internal object MenuControllerPlugins : PluginDisableAwareController() { + override val factory: (plugin: Plugin) -> MenuController = { MenuController(it) } +} + +internal class MenuController( + override val plugin: Plugin, +) : KListener, PluginDisableAware { + + init { + registerEvents(plugin) + } + + override fun onDisable() { + onlinePlayers.forEach { + val menu = getMenuFromPlayer(it) + ?.takeIf { it.plugin.name == plugin.name } + menu?.close(it, true) + } + } + + @EventHandler(ignoreCancelled = true) + fun clickInventoryEvent(event: InventoryClickEvent) { + if (event.view.type == InventoryType.CHEST) { + val player = event.whoClicked as Player + val inv = event.inventory + + val menu = getMenuFromInventory(event.inventory)?.takeIfHasPlayer(player) + + if (menu != null) { + if (menu.plugin.name != plugin.name) return + + if (event.rawSlot == event.slot) { + val clickedSlot = event.slot + 1 + val slot = menu.slotOrBaseSlot(clickedSlot) + + val interact = MenuPlayerSlotInteract( + menu, clickedSlot, slot, + player, inv, slot.cancel, + event.click, event.action, + event.currentItem, event.cursor, + event.hotbarButton, + ) + + slot.eventHandler.interact(interact) + + if (interact.canceled) event.isCancelled = true + } else { + // TODO move item + event.isCancelled = true + } + } + } + } + + @EventHandler(ignoreCancelled = true) + fun clickInteractEvent(event: InventoryDragEvent) { + if (event.view.type == InventoryType.CHEST) { + val player = event.whoClicked as Player + val menu = getMenuFromInventory(event.inventory)?.takeIfHasPlayer(player) + if (menu != null) { + if (menu.plugin.name != plugin.name) return + val pass = event.inventorySlots.firstOrNull { it in event.rawSlots } + if (pass != null) event.isCancelled = true + } + } + } + + @EventHandler + fun onClose(event: InventoryCloseEvent) { + if (event.view.type == InventoryType.CHEST) { + val player = event.player as Player + val menu = getMenuFromPlayer(player) + if (menu != null) { + if (menu.plugin.name != plugin.name) return + menu.close(player, false) + } + } + } + + @EventHandler(ignoreCancelled = true) + fun onPickupItemEvent(event: PlayerPickupItemEvent) { + val menu = getMenuFromPlayer(event.player) + if (menu != null) { + if (menu.plugin.name != plugin.name) return + event.isCancelled = true + } + } +} diff --git a/menu/src/main/kotlin/tech/carcadex/kotlinbukkitkit/menu/MenuEventHandler.kt b/menu/src/main/kotlin/tech/carcadex/kotlinbukkitkit/menu/MenuEventHandler.kt new file mode 100644 index 0000000..4b79e59 --- /dev/null +++ b/menu/src/main/kotlin/tech/carcadex/kotlinbukkitkit/menu/MenuEventHandler.kt @@ -0,0 +1,14 @@ +package tech.carcadex.kotlinbukkitkit.menu + +public interface MenuEventHandler { + + public fun update(update: MenuPlayerUpdate) + + public fun close(close: MenuPlayerClose) + + public fun moveToMenu(moveToMenu: MenuPlayerMoveTo) + + public fun preOpen(preOpen: MenuPlayerPreOpen) + + public fun open(open: MenuPlayerOpen) +} diff --git a/menu/src/main/kotlin/tech/carcadex/kotlinbukkitkit/menu/MenuEvents.kt b/menu/src/main/kotlin/tech/carcadex/kotlinbukkitkit/menu/MenuEvents.kt new file mode 100644 index 0000000..ac069cf --- /dev/null +++ b/menu/src/main/kotlin/tech/carcadex/kotlinbukkitkit/menu/MenuEvents.kt @@ -0,0 +1,121 @@ +package tech.carcadex.kotlinbukkitkit.menu + +import org.bukkit.entity.Player +import org.bukkit.inventory.Inventory +import org.bukkit.inventory.ItemStack +import java.util.* + +public interface MenuPlayer { + public val menu: Menu<*> + public val player: Player + + public fun putPlayerData(key: String, value: Any) { + val map = menu.playerData[player] + if (map != null) { + map[key] = value + } else menu.playerData[player] = WeakHashMap().apply { put(key, value) } + } + + public fun getPlayerData(key: String): Any? = menu.playerData.get(player)?.get(key) +} + +public interface MenuPlayerInventory : MenuPlayer { + public val inventory: Inventory + + public fun close(): Unit = player.closeInventory() + + public fun getItem(line: Int, slot: Int): ItemStack? { + return getItem(calculateSlot(line, slot)) + } + + public fun getItem(slot: Int): ItemStack? = inventory.getItem(slot - 1) + + public fun setItem(line: Int, slot: Int, item: ItemStack?) { + setItem(calculateSlot(line, slot), item) + } + + public fun setItem(slot: Int, item: ItemStack?) { + inventory.setItem(rawSlot(slot), item) + } + + public fun getPlayerItem(line: Int, slot: Int): ItemStack? { + return getPlayerItem(calculateSlot(line, slot)) + } + + public fun getPlayerItem(slot: Int): ItemStack? = player.inventory.getItem(rawSlot(slot)) + + public fun setPlayerItem(line: Int, slot: Int, item: ItemStack?) { + setPlayerItem(calculateSlot(line, slot), item) + } + + public fun setPlayerItem(slot: Int, item: ItemStack?) { + player.inventory.setItem(rawSlot(slot), item) + } + + private fun getLineRange(line: Int) = calculateSlot(line, 1)..calculateEndLine(line) + + public fun getLine(line: Int): List = getLineRange(line).map { getItem(it) } + + public fun getPlayerLine(line: Int): List = getLineRange(line).map { getPlayerItem(it) } + + public fun updateToPlayer() { + menu.update(player) + } +} + +public interface MenuPlayerCancellable : MenuPlayer { + public var canceled: Boolean +} + +public open class MenuPlayerInteract( + override val menu: Menu<*>, + override val player: Player, + override val inventory: Inventory, + override var canceled: Boolean, +) : MenuPlayerInventory, MenuPlayerCancellable + +public class MenuPlayerPreOpen( + override val menu: Menu<*>, + override val player: Player, + override var canceled: Boolean = false, +) : MenuPlayerCancellable + +public class MenuPlayerOpen( + override val menu: Menu<*>, + override val player: Player, + override val inventory: Inventory, +) : MenuPlayerInventory + +public class MenuPlayerUpdate( + override val menu: Menu<*>, + override val player: Player, + override val inventory: Inventory, + public var title: String, +) : MenuPlayerInventory + +public class MenuPlayerClose( + override val menu: Menu<*>, + override val player: Player, +) : MenuPlayer + +public interface MenuPlayerMove : MenuPlayerInventory, MenuPlayerCancellable { + public val toMoveSlot: Int + public val toMoveItem: ItemStack? +} + +public class MenuPlayerMoveTo( + menu: Menu<*>, + player: Player, + inventory: Inventory, + canceled: Boolean, + override val toMoveSlot: Int, + override val toMoveItem: ItemStack?, + public var targetSlot: Int, +) : MenuPlayerInteract(menu, player, inventory, canceled), MenuPlayerMove { + + public var targetCurrentItem: ItemStack? + set(value) { + setItem(targetSlot, value) + } + get() = getItem(targetSlot) +} diff --git a/menu/src/main/kotlin/tech/carcadex/kotlinbukkitkit/menu/Utils.kt b/menu/src/main/kotlin/tech/carcadex/kotlinbukkitkit/menu/Utils.kt new file mode 100644 index 0000000..0bb5c85 --- /dev/null +++ b/menu/src/main/kotlin/tech/carcadex/kotlinbukkitkit/menu/Utils.kt @@ -0,0 +1,29 @@ +package tech.carcadex.kotlinbukkitkit.menu + +import tech.carcadex.kotlinbukkitkit.menu.slot.Slot +import org.bukkit.entity.Player +import org.bukkit.inventory.Inventory + +public fun calculateSlot(line: Int, slot: Int): Int = calculateStartLine(line) + slot + +public fun calculateStartLine(line: Int): Int = calculateEndLine(line) - 9 +public fun calculateEndLine(line: Int): Int = line * 9 + +public fun rawSlot(slot: Int): Int = slot - 1 + +public fun Menu<*>.slotOrBaseSlot(slot: Int): Slot = slots[slot] ?: baseSlot +public fun Menu<*>.rangeOfSlots(): IntRange = 1..calculateEndLine(lines) +public fun Menu<*>.viewersFromPlayers(players: Set): Map = viewers.filterKeys { it in players } + +public fun Menu<*>.slotsWithBaseSlot(): List = slots.values + baseSlot + +public fun Menu<*>.hasPlayer(player: Player): Boolean = viewers.containsKey(player) +public fun Menu<*>.takeIfHasPlayer(player: Player): Menu<*>? = if (hasPlayer(player)) this else null + +public fun inventoryIsMenu(inventory: Inventory): Boolean = inventory.holder is Menu<*> + +public fun getMenuFromInventory(inventory: Inventory): Menu<*>? = inventory.holder as? Menu<*> + +public fun getMenuFromPlayer(player: Player): Menu<*>? { + return getMenuFromInventory(player.openInventory.topInventory)?.takeIfHasPlayer(player) +} diff --git a/menu/src/main/kotlin/tech/carcadex/kotlinbukkitkit/menu/dsl/Extensions.kt b/menu/src/main/kotlin/tech/carcadex/kotlinbukkitkit/menu/dsl/Extensions.kt new file mode 100644 index 0000000..d0dbf66 --- /dev/null +++ b/menu/src/main/kotlin/tech/carcadex/kotlinbukkitkit/menu/dsl/Extensions.kt @@ -0,0 +1,8 @@ +package tech.carcadex.kotlinbukkitkit.menu.dsl + +import org.bukkit.entity.Player +import java.util.* + +public fun MenuDSL.putPlayerData(player: Player, key: String, value: Any): Any? = playerData.getOrPut(player, { WeakHashMap() }).put(key, value) + +public fun MenuDSL.getPlayerData(player: Player, key: String): Any? = playerData.get(player)?.get(key) diff --git a/menu/src/main/kotlin/tech/carcadex/kotlinbukkitkit/menu/dsl/MenuDSL.kt b/menu/src/main/kotlin/tech/carcadex/kotlinbukkitkit/menu/dsl/MenuDSL.kt new file mode 100644 index 0000000..6397d3f --- /dev/null +++ b/menu/src/main/kotlin/tech/carcadex/kotlinbukkitkit/menu/dsl/MenuDSL.kt @@ -0,0 +1,46 @@ +package tech.carcadex.kotlinbukkitkit.menu.dsl + +import tech.carcadex.kotlinbukkitkit.menu.Menu +import tech.carcadex.kotlinbukkitkit.menu.calculateSlot +import tech.carcadex.kotlinbukkitkit.menu.dsl.slot.SlotDSL +import org.bukkit.inventory.ItemStack + +public inline fun MenuDSL.slot( + line: Int, + slot: Int, + item: ItemStack?, + block: SlotDSL.() -> Unit = {}, +): SlotDSL = slot(calculateSlot(line, slot), item, block) + +public inline fun MenuDSL.slot( + slot: Int, + item: ItemStack?, + block: SlotDSL.() -> Unit = {}, +): SlotDSL = baseSlot.clone(item).apply(block).also { + setSlot(slot, it) +} + +public interface MenuDSL : Menu { + + override val eventHandler: MenuEventHandlerDSL + + public fun onUpdate(update: MenuPlayerUpdateEvent) { + eventHandler.updateCallbacks.add(update) + } + + public fun onClose(close: MenuPlayerCloseEvent) { + eventHandler.closeCallbacks.add(close) + } + + public fun onMoveToMenu(moveToMenu: MenuPlayerMoveToEvent) { + eventHandler.moveToMenuCallbacks.add(moveToMenu) + } + + public fun preOpen(preOpen: MenuPlayerPreOpenEvent) { + eventHandler.preOpenCallbacks.add(preOpen) + } + + public fun onOpen(open: MenuPlayerOpenEvent) { + eventHandler.openCallbacks.add(open) + } +} diff --git a/menu/src/main/kotlin/tech/carcadex/kotlinbukkitkit/menu/dsl/MenuDSLImpl.kt b/menu/src/main/kotlin/tech/carcadex/kotlinbukkitkit/menu/dsl/MenuDSLImpl.kt new file mode 100644 index 0000000..7bbcdff --- /dev/null +++ b/menu/src/main/kotlin/tech/carcadex/kotlinbukkitkit/menu/dsl/MenuDSLImpl.kt @@ -0,0 +1,199 @@ +package tech.carcadex.kotlinbukkitkit.menu.dsl + +import tech.carcadex.kotlinbukkitkit.architecture.extensions.WithPlugin +import tech.carcadex.kotlinbukkitkit.extensions.task +import tech.carcadex.kotlinbukkitkit.menu.MenuControllerPlugins +import tech.carcadex.kotlinbukkitkit.menu.MenuPlayerClose +import tech.carcadex.kotlinbukkitkit.menu.MenuPlayerOpen +import tech.carcadex.kotlinbukkitkit.menu.MenuPlayerPreOpen +import tech.carcadex.kotlinbukkitkit.menu.MenuPlayerUpdate +import tech.carcadex.kotlinbukkitkit.menu.dsl.slot.SlotDSL +import tech.carcadex.kotlinbukkitkit.menu.dsl.slot.SlotDSLImpl +import tech.carcadex.kotlinbukkitkit.menu.dsl.slot.SlotEventHandlerDSL +import tech.carcadex.kotlinbukkitkit.menu.rangeOfSlots +import tech.carcadex.kotlinbukkitkit.menu.slot.MenuPlayerSlotRender +import tech.carcadex.kotlinbukkitkit.menu.slot.MenuPlayerSlotUpdate +import tech.carcadex.kotlinbukkitkit.menu.slot.Slot +import tech.carcadex.kotlinbukkitkit.menu.slotOrBaseSlot +import tech.carcadex.kotlinbukkitkit.menu.viewersFromPlayers +import org.bukkit.Bukkit +import org.bukkit.entity.Player +import org.bukkit.inventory.Inventory +import org.bukkit.plugin.Plugin +import org.bukkit.scheduler.BukkitTask +import java.util.* + +public inline fun WithPlugin<*>.menu( + displayName: String, + lines: Int, + cancelOnClick: Boolean = true, + block: MenuDSL.() -> Unit, +): MenuDSL = plugin.menu(displayName, lines, cancelOnClick, block) + +public inline fun Plugin.menu( + displayName: String, + lines: Int, + cancelOnClick: Boolean = true, + block: MenuDSL.() -> Unit, +): MenuDSL = menu(displayName, lines, this, cancelOnClick, block) + +public inline fun menu( + displayName: String, + lines: Int, + plugin: Plugin, + cancelOnClick: Boolean = true, + block: MenuDSL.() -> Unit, +): MenuDSL = MenuDSLImpl(plugin, displayName, lines, cancelOnClick).apply(block) + +public class MenuDSLImpl( + public override val plugin: Plugin, + override var title: String, + override var lines: Int, + override var cancelOnClick: Boolean, +) : MenuDSL { + + init { + MenuControllerPlugins.ensureInitialized(plugin) + } + + private var task: BukkitTask? = null + override var updateDelay: Long = 0 + set(value) { + field = value + removeTask() + if (value > 0 && viewers.isNotEmpty()) { + setNewTask() + } + } + + override val viewers: MutableMap = WeakHashMap() + override val slots: TreeMap = TreeMap() + + override val data: WeakHashMap = WeakHashMap() + override val playerData: WeakHashMap> = WeakHashMap>() + + override val eventHandler: MenuEventHandlerDSL = MenuEventHandlerDSL(this) + + override var baseSlot: SlotDSL = SlotDSLImpl(null, cancelOnClick, SlotEventHandlerDSL()) + + override fun setSlot(slot: Int, slotObj: SlotDSL) { + slots.put(slot, slotObj) + } + + override fun update(players: Set) { + val viewers = viewersFromPlayers(players) + for ((player, inventory) in viewers) { + val update = MenuPlayerUpdate(this, player, inventory, title) + eventHandler.update(update) + + // TODO title + /*if(update.title != title)*/ + + for (i in rangeOfSlots()) { + val slot = slotOrBaseSlot(i) + updateSlotOnlyPos(i, slot, player, inventory) + } + } + } + + override fun updateSlot(slot: Slot, players: Set) { + val slots: Map = if (slot === baseSlot) { + rangeOfSlots().mapNotNull { if (slots[it] == null) it to slot else null }.toMap() + } else { + rangeOfSlots().mapNotNull { if (slot === slots[it]) it to slot else null }.toMap() + } + + for ((player, inventory) in viewersFromPlayers(players)) { + for ((pos, s) in slots) { + updateSlotOnlyPos(pos, s, player, inventory) + } + } + } + + private fun updateSlotOnlyPos(pos: Int, slot: Slot, player: Player, inventory: Inventory) { + val slotUpdate = MenuPlayerSlotUpdate(this, pos, slot, player, inventory) + slot.eventHandler.update(slotUpdate) + } + + override fun openToPlayer(vararg players: Player) { + for (player in players) { + close(player, false) + + try { + val inv = inventory + + viewers.put(player, inv) + + val preOpen = MenuPlayerPreOpen(this, player) + eventHandler.preOpen(preOpen) + + if (preOpen.canceled) return + + for (i in rangeOfSlots()) { + val slot = slotOrBaseSlot(i) + + val render = MenuPlayerSlotRender(this, i, slot, player, inv) + + slot.eventHandler.render(render) + } + + player.openInventory(inv) + + val open = MenuPlayerOpen(this, player, inventory) + eventHandler.open(open) + + if (task == null && updateDelay > 0 && viewers.isNotEmpty()) { + setNewTask() + } + } catch (e: Throwable) { + e.printStackTrace() + removePlayer(player, true) + } + } + } + + override fun getInventory(): Inventory { + val slots = rangeOfSlots() + val inventory = Bukkit.createInventory(this, slots.endInclusive, title) + + for (i in slots) { + val slot = slotOrBaseSlot(i) + + val item = slot.item?.clone() + inventory.setItem(i - 1, item) + } + + return inventory + } + + private fun removePlayer(player: Player, closeInventory: Boolean): Boolean { + if (closeInventory) player.closeInventory() + + val viewing = viewers.remove(player) != null + if (viewing) { + clearPlayerData(player) + } + + return viewing + } + + override fun close(player: Player, closeInventory: Boolean) { + if (removePlayer(player, closeInventory)) { + val menuClose = MenuPlayerClose(this, player) + eventHandler.close(menuClose) + + if (task != null && updateDelay > 0 && viewers.isEmpty()) { + removeTask() + } + } + } + + private fun setNewTask() { + task = task(repeatDelay = updateDelay) { update() } + } + + private fun removeTask() { + task?.cancel() + task = null + } +} diff --git a/menu/src/main/kotlin/tech/carcadex/kotlinbukkitkit/menu/dsl/MenuEventHandlerDSL.kt b/menu/src/main/kotlin/tech/carcadex/kotlinbukkitkit/menu/dsl/MenuEventHandlerDSL.kt new file mode 100644 index 0000000..6610b61 --- /dev/null +++ b/menu/src/main/kotlin/tech/carcadex/kotlinbukkitkit/menu/dsl/MenuEventHandlerDSL.kt @@ -0,0 +1,54 @@ +package tech.carcadex.kotlinbukkitkit.menu.dsl + +import tech.carcadex.kotlinbukkitkit.menu.MenuEventHandler +import tech.carcadex.kotlinbukkitkit.menu.MenuPlayerClose +import tech.carcadex.kotlinbukkitkit.menu.MenuPlayerMoveTo +import tech.carcadex.kotlinbukkitkit.menu.MenuPlayerOpen +import tech.carcadex.kotlinbukkitkit.menu.MenuPlayerPreOpen +import tech.carcadex.kotlinbukkitkit.menu.MenuPlayerUpdate + +public typealias MenuPlayerUpdateEvent = MenuPlayerUpdate.() -> Unit +public typealias MenuPlayerCloseEvent = MenuPlayerClose.() -> Unit +public typealias MenuPlayerMoveToEvent = MenuPlayerMoveTo.() -> Unit + +public typealias MenuPlayerPreOpenEvent = MenuPlayerPreOpen.() -> Unit +public typealias MenuPlayerOpenEvent = MenuPlayerOpen.() -> Unit + +public class MenuEventHandlerDSL(public val menu: MenuDSL) : MenuEventHandler { + + internal val updateCallbacks: MutableList = mutableListOf() + internal val closeCallbacks: MutableList = mutableListOf() + internal val moveToMenuCallbacks: MutableList = mutableListOf() + internal val preOpenCallbacks: MutableList = mutableListOf() + internal val openCallbacks: MutableList = mutableListOf() + + override fun update(update: MenuPlayerUpdate) { + for (callback in updateCallbacks) { + callback(update) + } + } + + override fun close(close: MenuPlayerClose) { + for (callback in closeCallbacks) { + callback(close) + } + } + + override fun moveToMenu(moveToMenu: MenuPlayerMoveTo) { + for (callback in moveToMenuCallbacks) { + callback(moveToMenu) + } + } + + override fun preOpen(preOpen: MenuPlayerPreOpen) { + for (callback in preOpenCallbacks) { + callback(preOpen) + } + } + + override fun open(open: MenuPlayerOpen) { + for (callback in openCallbacks) { + callback(open) + } + } +} diff --git a/menu/src/main/kotlin/tech/carcadex/kotlinbukkitkit/menu/dsl/pagination/MenuPagination.kt b/menu/src/main/kotlin/tech/carcadex/kotlinbukkitkit/menu/dsl/pagination/MenuPagination.kt new file mode 100644 index 0000000..36910d3 --- /dev/null +++ b/menu/src/main/kotlin/tech/carcadex/kotlinbukkitkit/menu/dsl/pagination/MenuPagination.kt @@ -0,0 +1,134 @@ +package tech.carcadex.kotlinbukkitkit.menu.dsl.pagination + +import tech.carcadex.kotlinbukkitkit.menu.MenuPlayer +import tech.carcadex.kotlinbukkitkit.menu.MenuPlayerInventory +import tech.carcadex.kotlinbukkitkit.menu.dsl.MenuDSL +import tech.carcadex.kotlinbukkitkit.menu.dsl.pagination.slot.PaginationSlotDSL +import tech.carcadex.kotlinbukkitkit.menu.dsl.slot.SlotDSL +import tech.carcadex.kotlinbukkitkit.utility.collections.ObservableCollection +import org.bukkit.entity.Player +import java.lang.IllegalArgumentException +import java.util.TreeMap +import java.util.WeakHashMap + +public typealias ItemsProvider = () -> ObservableCollection +public typealias ItemsAdapter = MenuPlayer.(List) -> List + +public enum class Orientation { HORIZONTAL, VERTICAL } + +public inline fun MenuPagination.slot( + builder: PaginationSlotDSL.() -> Unit, +) { + for (paginationSlot in paginationSlots.values) { + paginationSlot.builder() + } +} + +public fun MenuDSL.setPlayerOpenPage(player: Player, page: Int) { + playerData.put( + player, + WeakHashMap().apply { put(PAGINATION_OPEN_PAGE_KEY, page) }, + ) +} +public inline fun MenuDSL.pagination( + itemsProvider: ObservableCollection, + nextPageSlot: SlotDSL, + previousPageSlot: SlotDSL, + autoUpdateSwitchPageSlot: Boolean = true, + startLine: Int = 1, + endLine: Int = lines - 1, + startSlot: Int = 1, + endSlot: Int = 9, + orientation: Orientation = Orientation.HORIZONTAL, + noinline itemsAdapterOnOpen: ItemsAdapter? = null, + noinline itemsAdapterOnUpdate: ItemsAdapter? = null, + builder: MenuPaginationImpl.() -> Unit, +): MenuPaginationImpl { + return pagination( + { itemsProvider }, + nextPageSlot, + previousPageSlot, + autoUpdateSwitchPageSlot, + startLine, + endLine, + startSlot, + endSlot, + orientation, + itemsAdapterOnOpen, + itemsAdapterOnUpdate, + builder, + ) +} + +public inline fun MenuDSL.pagination( + noinline itemsProvider: ItemsProvider, + nextPageSlot: SlotDSL, + previousPageSlot: SlotDSL, + autoUpdateSwitchPageSlot: Boolean = true, + startLine: Int = 1, + endLine: Int = lines - 1, + startSlot: Int = 1, + endSlot: Int = 9, + orientation: Orientation = Orientation.HORIZONTAL, + noinline itemsAdapterOnOpen: ItemsAdapter? = null, + noinline itemsAdapterOnUpdate: ItemsAdapter? = null, + builder: MenuPaginationImpl.() -> Unit, +): MenuPaginationImpl { + if (startSlot > endSlot) throw IllegalArgumentException() + if (startLine > endLine) throw IllegalArgumentException() + + return MenuPaginationImpl( + this, + itemsProvider, + nextPageSlot, + previousPageSlot, + autoUpdateSwitchPageSlot, + startLine, + endLine, + startSlot, + endSlot, + orientation, + itemsAdapterOnOpen, + itemsAdapterOnUpdate, + ).apply(builder) +} + +public interface MenuPagination { + public val menu: MenuDSL + public val paginationSlots: TreeMap> + public val paginationEventHandler: PaginationEventHandler + + public val itemsProvider: ItemsProvider + + public val nextPageSlot: SlotDSL + public val previousPageSlot: SlotDSL + + public val autoUpdateSwitchPageSlot: Boolean + + public val startLine: Int + public val endLine: Int + + public val startSlot: Int + public val endSlot: Int + + public val orientation: Orientation + + public val itemsAdapterOnOpen: ItemsAdapter? + public val itemsAdapterOnUpdate: ItemsAdapter? + + public fun onPageChange(pageChange: MenuPlayerPageChangeEvent) { + paginationEventHandler.pageChangeCallbacks.add(pageChange) + } + + public fun onPageAvailable(pageAvailable: MenuPlayerPageAvailableEvent) { + paginationEventHandler.pageAvailableCallbacks.add(pageAvailable) + } + + public fun hasPreviousPage(player: Player): Boolean + + public fun hasNextPage(player: Player): Boolean + + public fun getPlayerCurrentPage(player: Player): Int + + public fun updateItemsToPlayer(menuPlayer: MenuPlayerInventory) +} diff --git a/menu/src/main/kotlin/tech/carcadex/kotlinbukkitkit/menu/dsl/pagination/MenuPaginationImpl.kt b/menu/src/main/kotlin/tech/carcadex/kotlinbukkitkit/menu/dsl/pagination/MenuPaginationImpl.kt new file mode 100644 index 0000000..d7671f3 --- /dev/null +++ b/menu/src/main/kotlin/tech/carcadex/kotlinbukkitkit/menu/dsl/pagination/MenuPaginationImpl.kt @@ -0,0 +1,265 @@ +package tech.carcadex.kotlinbukkitkit.menu.dsl.pagination + +import tech.carcadex.kotlinbukkitkit.menu.MenuPlayerInventory +import tech.carcadex.kotlinbukkitkit.menu.calculateSlot +import tech.carcadex.kotlinbukkitkit.menu.dsl.MenuDSL +import tech.carcadex.kotlinbukkitkit.menu.dsl.pagination.slot.PaginationSlotDSL +import tech.carcadex.kotlinbukkitkit.menu.dsl.pagination.slot.PaginationSlotDSLImpl +import tech.carcadex.kotlinbukkitkit.menu.dsl.slot +import tech.carcadex.kotlinbukkitkit.menu.dsl.slot.SlotDSL +import org.bukkit.entity.Player +import java.util.* + +internal const val PAGINATION_OPEN_PAGE_KEY = "PAGINATION:open_page" + +public class MenuPaginationImpl( + override val menu: MenuDSL, + override val itemsProvider: ItemsProvider, + override val nextPageSlot: SlotDSL, + override val previousPageSlot: SlotDSL, + override val autoUpdateSwitchPageSlot: Boolean, + override val startLine: Int = 1, + override val endLine: Int = menu.lines - 1, + override val startSlot: Int = 1, + override val endSlot: Int = 9, + override val orientation: Orientation = Orientation.HORIZONTAL, + override val itemsAdapterOnOpen: ItemsAdapter? = null, + override val itemsAdapterOnUpdate: ItemsAdapter? = null, +) : MenuPagination { + override val paginationEventHandler: PaginationEventHandler = PaginationEventHandler() + + override val paginationSlots: TreeMap> = TreeMap() + internal val currentPlayerPage = WeakHashMap() + + // should be null when is not mapped (for not allocate a new list everytime) + internal val currentPlayerItems = WeakHashMap>() + + internal val itemSlotData = WeakHashMap>() + internal val itemPlayerSlotData = WeakHashMap>>() + + init { + // setup next page button click + nextPageSlot.onClick { + if (hasNextPage(player)) { + nextPage(this) + } + } + + // setup previous page button click + previousPageSlot.onClick { + if (hasPreviousPage(player)) { + previousPage(this) + } + } + + menu.preOpen { + // adapting items for player + val items = itemsAdapterOnOpen?.let { it(getPlayerItems(player).toList()) } + if (items != null) { + currentPlayerItems[player] = items + } + + // set the menu page if player informed in [MenuDSL.setPlayerOpenPage] + val openPage = menu.playerData.get(player)?.get(PAGINATION_OPEN_PAGE_KEY) as? Int? + if (openPage != null) { + // check if openPage is valid or cancel and trigger a event + if (isPageAvailable(player, openPage)) { + currentPlayerPage[player] = openPage + } else { + canceled = true + paginationEventHandler.pageAvailable(this) + } + } + } + + // cleaning player data on menu close + menu.onClose { + currentPlayerPage.remove(player) + currentPlayerItems.remove(player) + + for (value in itemPlayerSlotData.values) { + value.remove(player) + } + } + + // if enabled, update next and previous slots when page change + if (autoUpdateSwitchPageSlot) { + onPageChange { + menu.updateSlot(nextPageSlot, player) + menu.updateSlot(previousPageSlot, player) + } + } + + // setup pagination slots + for (line in startLine..endLine) { + for (slotPos in startSlot..endSlot) { + val menuSlot = calculateSlot(line, slotPos) + val slotRoot = menu.slot(menuSlot, null) + + val paginationSlot = PaginationSlotDSLImpl( + this, + slotRoot, + ) + + slotRoot.apply { + fun getCurrentItemForPlayer(player: Player): T? { + return getCurrentItemForPlayer( + menuSlot, + getPlayerCurrentPage(player), + player, + ) + } + + onRender { + paginationSlot.paginationEventHandler.handleRender( + getCurrentItemForPlayer(player), + this, + ) + } + onUpdate { + paginationSlot.paginationEventHandler.handleUpdate( + getCurrentItemForPlayer(player), + this, + ) + } + onClick { + paginationSlot.paginationEventHandler.handleInteract( + getCurrentItemForPlayer(player), + this, + ) + } + } + + paginationSlots.put(menuSlot, paginationSlot) + } + } + } + + override fun hasPreviousPage(player: Player): Boolean { + return isPageAvailable(player, getPlayerCurrentPage(player) - 1) + } + override fun hasNextPage(player: Player): Boolean { + return isPageAvailable(player, getPlayerCurrentPage(player) + 1) + } + + override fun getPlayerCurrentPage(player: Player): Int = currentPlayerPage.getOrDefault(player, 1) + + /** + * Update current item list calling itemsUpdateFilter + */ + override fun updateItemsToPlayer(menuPlayer: MenuPlayerInventory) { + val player = menuPlayer.player + + // checking if adapter is not null, other wise, ignore + if (itemsAdapterOnUpdate != null) { + val items = itemsAdapterOnUpdate.let { menuPlayer.it(itemsProvider().toList()) } + + currentPlayerItems[player] = items + + // setting player to the last page if he is not in a page with items after adapting + if (!isPageAvailable(player, getPlayerCurrentPage(player))) { + currentPlayerPage[player] = maxPages(player) + } + + // calling update slot to the PaginationSlotDSL + forEachSlot { slotPos, pageSlot -> + val currentPage = getPlayerCurrentPage(player) + + val item: T? = getCurrentItemForPlayer(slotPos, currentPage, player) + + pageSlot.updateSlot(item, item, slotPos, menuPlayer) + } + + // calling event because this remove items causing no more page needed + paginationEventHandler.pageChange(menuPlayer) + } + } + + internal fun getCurrentItemForPlayer(slotPos: Int, page: Int, player: Player): T? { + val items = getPlayerItems(player) + + val startLineNotUsage = startLine - 1 + val lineEndSlotNotUsage = 9 - endSlot + val startSlotNotUsage = startSlot - 1 + + val startLineSlotsUsage = startLineNotUsage * 9 + val currentLine = (slotPos / 9) + 1 + val currentLineUsage = currentLine - startLineNotUsage + + val currentUsageStartSlots = startSlotNotUsage * currentLineUsage + val currentEndSlotUsage = if (currentLineUsage > 1) lineEndSlotNotUsage * (currentLineUsage - 1) else 0 + + val slotItemIndex = pageStartIndex(page) + (slotPos - startLineSlotsUsage - currentUsageStartSlots - currentEndSlotUsage) + + return items.asSequence().elementAtOrNull(slotItemIndex - 1) + } + + internal fun nextPage(menuPlayerInventory: MenuPlayerInventory) { + val nextPage = getPlayerCurrentPage(menuPlayerInventory.player) + 1 + + changePage(menuPlayerInventory, nextPage) + } + + internal fun previousPage(menuPlayerInventory: MenuPlayerInventory) { + val previousPage = getPlayerCurrentPage(menuPlayerInventory.player) - 1 + + changePage(menuPlayerInventory, previousPage) + } + + private fun changePage(menuPlayerInventory: MenuPlayerInventory, nextPage: Int) { + val player = menuPlayerInventory.player + + val currentPage = getPlayerCurrentPage(player) + + currentPlayerPage[player] = nextPage + + forEachSlot { slotPos, pageSlot -> + val actualItem: T? = getCurrentItemForPlayer(slotPos, currentPage, player) + + val nextItem: T? = getCurrentItemForPlayer(slotPos, nextPage, player) + + pageSlot.updateSlot(actualItem, nextItem, slotPos, menuPlayerInventory, true) + + paginationEventHandler.pageChange(menuPlayerInventory) + } + } + + private inline fun forEachSlot( + block: (slotPos: Int, pageSlot: PaginationSlotDSLImpl) -> Unit, + ) { + for (line in startLine..endLine) { + for (slot in startSlot..endSlot) { + val slotPos = calculateSlot(line, slot) + val pageSlot = paginationSlots[slotPos] as? PaginationSlotDSLImpl + ?: continue + + block(slotPos, pageSlot) + } + } + } + + private fun pageStartIndex(page: Int) = ((page - 1) * maxSlotPerPage()) + + private fun getPlayerItems(player: Player) = currentPlayerItems[player] ?: itemsProvider() + + private fun maxSlotPerPage() = (endLine - startLine + 1) * (endSlot - startSlot + 1) + + private fun maxPages(player: Player): Int { + val itemsCount = getPlayerItems(player).size + + val maxPerPage = maxSlotPerPage() + + var pages = (itemsCount / maxPerPage).toInt() + val mod = itemsCount % maxPerPage + + if (mod > 0) pages++ + + return pages + } + + private fun isPageAvailable(player: Player, page: Int): Boolean { + val maxPages = maxPages(player) + + return page in 1..maxPages + } +} diff --git a/menu/src/main/kotlin/tech/carcadex/kotlinbukkitkit/menu/dsl/pagination/PaginationEventHandler.kt b/menu/src/main/kotlin/tech/carcadex/kotlinbukkitkit/menu/dsl/pagination/PaginationEventHandler.kt new file mode 100644 index 0000000..f47394c --- /dev/null +++ b/menu/src/main/kotlin/tech/carcadex/kotlinbukkitkit/menu/dsl/pagination/PaginationEventHandler.kt @@ -0,0 +1,23 @@ +package tech.carcadex.kotlinbukkitkit.menu.dsl.pagination + +import tech.carcadex.kotlinbukkitkit.menu.MenuPlayer + +public typealias MenuPlayerPageChangeEvent = MenuPlayer.() -> Unit +public typealias MenuPlayerPageAvailableEvent = MenuPlayer.() -> Unit + +public class PaginationEventHandler { + internal val pageChangeCallbacks: MutableList = mutableListOf() + internal val pageAvailableCallbacks: MutableList = mutableListOf() + + public fun pageChange(pageChange: MenuPlayer) { + for (callback in pageChangeCallbacks) { + callback(pageChange) + } + } + + public fun pageAvailable(pageAvailable: MenuPlayer) { + for (callback in pageAvailableCallbacks) { + callback(pageAvailable) + } + } +} diff --git a/menu/src/main/kotlin/tech/carcadex/kotlinbukkitkit/menu/dsl/pagination/slot/PaginationSlotDSL.kt b/menu/src/main/kotlin/tech/carcadex/kotlinbukkitkit/menu/dsl/pagination/slot/PaginationSlotDSL.kt new file mode 100644 index 0000000..d4975c5 --- /dev/null +++ b/menu/src/main/kotlin/tech/carcadex/kotlinbukkitkit/menu/dsl/pagination/slot/PaginationSlotDSL.kt @@ -0,0 +1,35 @@ +package tech.carcadex.kotlinbukkitkit.menu.dsl.pagination.slot + +import tech.carcadex.kotlinbukkitkit.menu.dsl.slot.SlotDSL +import org.bukkit.entity.Player +import java.util.* + +public interface PaginationSlotDSL { + public val slotRoot: SlotDSL + + public val paginationEventHandler: PaginationSlotEventHandler + + public val slotData: WeakHashMap + public val playerSlotData: WeakHashMap> + + /** + * Cancel the interaction with this slot + */ + public var cancel: Boolean + + public fun onPageChange(pageChange: MenuPlayerSlotPageChangeEvent) { + paginationEventHandler.pageChangeCallbacks.add(pageChange) + } + + public fun onClick(click: MenuPlayerPageSlotInteractEvent) { + paginationEventHandler.interactCallbacks.add(click) + } + + public fun onRender(render: MenuPlayerPageSlotRenderEvent) { + paginationEventHandler.renderCallbacks.add(render) + } + + public fun onUpdate(update: MenuPlayerPageSlotUpdateEvent) { + paginationEventHandler.updateCallbacks.add(update) + } +} diff --git a/menu/src/main/kotlin/tech/carcadex/kotlinbukkitkit/menu/dsl/pagination/slot/PaginationSlotDSLImpl.kt b/menu/src/main/kotlin/tech/carcadex/kotlinbukkitkit/menu/dsl/pagination/slot/PaginationSlotDSLImpl.kt new file mode 100644 index 0000000..7bb4105 --- /dev/null +++ b/menu/src/main/kotlin/tech/carcadex/kotlinbukkitkit/menu/dsl/pagination/slot/PaginationSlotDSLImpl.kt @@ -0,0 +1,94 @@ +package tech.carcadex.kotlinbukkitkit.menu.dsl.pagination.slot + +import tech.carcadex.kotlinbukkitkit.menu.MenuPlayerInventory +import tech.carcadex.kotlinbukkitkit.menu.dsl.pagination.MenuPaginationImpl +import tech.carcadex.kotlinbukkitkit.menu.dsl.slot.SlotDSL +import tech.carcadex.kotlinbukkitkit.menu.slot.MenuPlayerSlotRender +import org.bukkit.entity.Player +import java.util.* + +public class PaginationSlotDSLImpl( + private val pagination: MenuPaginationImpl, + override val slotRoot: SlotDSL, +) : PaginationSlotDSL { + override val paginationEventHandler: PaginationSlotEventHandler = PaginationSlotEventHandler() + + override var cancel: Boolean + get() = slotRoot.cancel + set(value) { slotRoot.cancel = value } + + override val slotData: WeakHashMap + get() = slotRoot.slotData + override val playerSlotData: WeakHashMap> + get() = slotRoot.playerSlotData + + internal fun updateSlot( + actualItem: T?, + nextItem: T?, + slotPos: Int, + menuPlayerInventory: MenuPlayerInventory, + isPageChange: Boolean = false, + ) { + if (isPageChange) { + relocateSlotData(actualItem, nextItem) + + // triggering event + paginationEventHandler.handlePageChange( + actualItem, + MenuPlayerSlotPageChange( + pagination.menu, + slotPos, + slotRoot, + menuPlayerInventory.player, + menuPlayerInventory.inventory, + ), + ) + } + + // cleaning item in the inventory slot + menuPlayerInventory.setItem(slotPos, null) + + paginationEventHandler.handleRender( + nextItem, + MenuPlayerSlotRender( + pagination.menu, + slotPos, + slotRoot, + menuPlayerInventory.player, + menuPlayerInventory.inventory, + ), + ) + } + + internal fun relocateSlotData(actualItem: T?, nextItem: T?) { + if (actualItem != null) { + // caching the current Data from Slot + val slotData = WeakHashMap(slotData) + val playerSlotData = WeakHashMap(playerSlotData) + + if (slotData.isNotEmpty()) { + pagination.itemSlotData[actualItem] = slotData + } + + if (playerSlotData.isNotEmpty()) { + pagination.itemPlayerSlotData[actualItem] = playerSlotData + } + } + + // cleaning current data in the Slot + slotData.clear() + playerSlotData.clear() + + if (nextItem != null) { + val nextSlotData = pagination.itemSlotData[nextItem] + val nextPlayerSlotData = pagination.itemPlayerSlotData[nextItem] + + if (nextSlotData != null) { + slotData.putAll(nextSlotData) + } + if (nextPlayerSlotData != null) { + playerSlotData.putAll(nextPlayerSlotData) + } + } + } +} diff --git a/menu/src/main/kotlin/tech/carcadex/kotlinbukkitkit/menu/dsl/pagination/slot/PaginationSlotEventHandler.kt b/menu/src/main/kotlin/tech/carcadex/kotlinbukkitkit/menu/dsl/pagination/slot/PaginationSlotEventHandler.kt new file mode 100644 index 0000000..912d818 --- /dev/null +++ b/menu/src/main/kotlin/tech/carcadex/kotlinbukkitkit/menu/dsl/pagination/slot/PaginationSlotEventHandler.kt @@ -0,0 +1,54 @@ +package tech.carcadex.kotlinbukkitkit.menu.dsl.pagination.slot + +import tech.carcadex.kotlinbukkitkit.menu.Menu +import tech.carcadex.kotlinbukkitkit.menu.slot.MenuPlayerInventorySlot +import tech.carcadex.kotlinbukkitkit.menu.slot.MenuPlayerSlotInteract +import tech.carcadex.kotlinbukkitkit.menu.slot.MenuPlayerSlotRender +import tech.carcadex.kotlinbukkitkit.menu.slot.MenuPlayerSlotUpdate +import tech.carcadex.kotlinbukkitkit.menu.slot.Slot +import org.bukkit.entity.Player +import org.bukkit.inventory.Inventory + +public typealias MenuPlayerSlotPageChangeEvent = MenuPlayerSlotPageChange.(T?) -> Unit +public typealias MenuPlayerPageSlotInteractEvent = MenuPlayerSlotInteract.(T?) -> Unit +public typealias MenuPlayerPageSlotRenderEvent = MenuPlayerSlotRender.(T?) -> Unit +public typealias MenuPlayerPageSlotUpdateEvent = MenuPlayerSlotUpdate.(T?) -> Unit + +public class PaginationSlotEventHandler { + internal val pageChangeCallbacks = mutableListOf>() + internal val interactCallbacks = mutableListOf>() + internal val renderCallbacks = mutableListOf>() + internal val updateCallbacks = mutableListOf>() + + public fun handlePageChange(currentItem: T?, pageChange: MenuPlayerSlotPageChange) { + for (callback in pageChangeCallbacks) { + callback(pageChange, currentItem) + } + } + + public fun handleRender(currentItem: T?, render: MenuPlayerSlotRender) { + for (callback in renderCallbacks) { + callback(render, currentItem) + } + } + + public fun handleUpdate(currentItem: T?, update: MenuPlayerSlotUpdate) { + for (callback in updateCallbacks) { + callback(update, currentItem) + } + } + + public fun handleInteract(currentItem: T?, interact: MenuPlayerSlotInteract) { + for (callback in interactCallbacks) { + callback(interact, currentItem) + } + } +} + +public class MenuPlayerSlotPageChange( + override val menu: Menu<*>, + override val slotPos: Int, + override val slot: Slot, + override val player: Player, + override val inventory: Inventory, +) : MenuPlayerInventorySlot diff --git a/menu/src/main/kotlin/tech/carcadex/kotlinbukkitkit/menu/dsl/slot/SlotDSL.kt b/menu/src/main/kotlin/tech/carcadex/kotlinbukkitkit/menu/dsl/slot/SlotDSL.kt new file mode 100644 index 0000000..a708cfe --- /dev/null +++ b/menu/src/main/kotlin/tech/carcadex/kotlinbukkitkit/menu/dsl/slot/SlotDSL.kt @@ -0,0 +1,27 @@ +package tech.carcadex.kotlinbukkitkit.menu.dsl.slot + +import tech.carcadex.kotlinbukkitkit.menu.slot.Slot +import org.bukkit.inventory.ItemStack + +public interface SlotDSL : Slot { + + override val eventHandler: SlotEventHandlerDSL + + public fun onClick(click: MenuPlayerSlotInteractEvent) { + eventHandler.interactCallbacks.add(click) + } + + public fun onRender(render: MenuPlayerSlotRenderEvent) { + eventHandler.renderCallbacks.add(render) + } + + public fun onUpdate(update: MenuPlayerSlotUpdateEvent) { + eventHandler.updateCallbacks.add(update) + } + + public fun onMoveToSlot(moveToSlot: MenuPlayerSlotMoveToEvent) { + eventHandler.moveToSlotCallbacks.add(moveToSlot) + } + + override fun clone(item: ItemStack?): SlotDSL +} diff --git a/menu/src/main/kotlin/tech/carcadex/kotlinbukkitkit/menu/dsl/slot/SlotDSLImpl.kt b/menu/src/main/kotlin/tech/carcadex/kotlinbukkitkit/menu/dsl/slot/SlotDSLImpl.kt new file mode 100644 index 0000000..7929fae --- /dev/null +++ b/menu/src/main/kotlin/tech/carcadex/kotlinbukkitkit/menu/dsl/slot/SlotDSLImpl.kt @@ -0,0 +1,22 @@ +package tech.carcadex.kotlinbukkitkit.menu.dsl.slot + +import tech.carcadex.kotlinbukkitkit.menu.dsl.MenuDSL +import org.bukkit.entity.Player +import org.bukkit.inventory.ItemStack +import java.util.* + +public fun MenuDSL.newSlot(item: ItemStack?, builder: SlotDSL.() -> Unit): SlotDSL { + return SlotDSLImpl(item, cancelOnClick).apply(builder) +} + +public class SlotDSLImpl( + override val item: ItemStack?, + override var cancel: Boolean, + override val eventHandler: SlotEventHandlerDSL = SlotEventHandlerDSL(), +) : SlotDSL { + + override val slotData: WeakHashMap = WeakHashMap() + override val playerSlotData: WeakHashMap> = WeakHashMap() + + override fun clone(item: ItemStack?): SlotDSL = SlotDSLImpl(item, cancel, eventHandler.clone()) +} diff --git a/menu/src/main/kotlin/tech/carcadex/kotlinbukkitkit/menu/dsl/slot/SlotEventHandlerDSL.kt b/menu/src/main/kotlin/tech/carcadex/kotlinbukkitkit/menu/dsl/slot/SlotEventHandlerDSL.kt new file mode 100644 index 0000000..5627412 --- /dev/null +++ b/menu/src/main/kotlin/tech/carcadex/kotlinbukkitkit/menu/dsl/slot/SlotEventHandlerDSL.kt @@ -0,0 +1,53 @@ +package tech.carcadex.kotlinbukkitkit.menu.dsl.slot + +import tech.carcadex.kotlinbukkitkit.menu.slot.MenuPlayerSlotInteract +import tech.carcadex.kotlinbukkitkit.menu.slot.MenuPlayerSlotMoveTo +import tech.carcadex.kotlinbukkitkit.menu.slot.MenuPlayerSlotRender +import tech.carcadex.kotlinbukkitkit.menu.slot.MenuPlayerSlotUpdate +import tech.carcadex.kotlinbukkitkit.menu.slot.SlotEventHandler + +public typealias MenuPlayerSlotInteractEvent = MenuPlayerSlotInteract.() -> Unit +public typealias MenuPlayerSlotRenderEvent = MenuPlayerSlotRender.() -> Unit +public typealias MenuPlayerSlotUpdateEvent = MenuPlayerSlotUpdate.() -> Unit +public typealias MenuPlayerSlotMoveToEvent = MenuPlayerSlotMoveTo.() -> Unit + +public class SlotEventHandlerDSL : SlotEventHandler { + + internal val interactCallbacks = mutableListOf() + internal val renderCallbacks = mutableListOf() + internal val updateCallbacks = mutableListOf() + internal val moveToSlotCallbacks = mutableListOf() + + override fun interact(interact: MenuPlayerSlotInteract) { + for (callback in interactCallbacks) { + callback(interact) + } + } + + override fun render(render: MenuPlayerSlotRender) { + for (callback in renderCallbacks) { + callback(render) + } + } + + override fun update(update: MenuPlayerSlotUpdate) { + for (callback in updateCallbacks) { + callback(update) + } + } + + override fun moveToSlot(moveToSlot: MenuPlayerSlotMoveTo) { + for (callback in moveToSlotCallbacks) { + callback(moveToSlot) + } + } + + override fun clone(): SlotEventHandlerDSL { + return SlotEventHandlerDSL().also { + it.interactCallbacks.addAll(interactCallbacks) + it.renderCallbacks.addAll(renderCallbacks) + it.updateCallbacks.addAll(updateCallbacks) + it.moveToSlotCallbacks.addAll(moveToSlotCallbacks) + } + } +} diff --git a/menu/src/main/kotlin/tech/carcadex/kotlinbukkitkit/menu/slot/Slot.kt b/menu/src/main/kotlin/tech/carcadex/kotlinbukkitkit/menu/slot/Slot.kt new file mode 100644 index 0000000..14fe09e --- /dev/null +++ b/menu/src/main/kotlin/tech/carcadex/kotlinbukkitkit/menu/slot/Slot.kt @@ -0,0 +1,33 @@ +package tech.carcadex.kotlinbukkitkit.menu.slot + +import org.bukkit.entity.Player +import org.bukkit.inventory.ItemStack +import java.util.* + +public interface Slot { + + public val item: ItemStack? + + public val eventHandler: SlotEventHandler + + public val slotData: WeakHashMap + public val playerSlotData: WeakHashMap> + + /** + * Cancel the interaction with this slot + */ + public var cancel: Boolean + + /** + * a clone of Slot without slotData and playerSlotData + */ + public fun clone(item: ItemStack?): Slot + + public fun clearSlotData() { + slotData.clear() + } + + public fun clearPlayerData(player: Player) { + playerSlotData.remove(player) + } +} diff --git a/menu/src/main/kotlin/tech/carcadex/kotlinbukkitkit/menu/slot/SlotEventHandler.kt b/menu/src/main/kotlin/tech/carcadex/kotlinbukkitkit/menu/slot/SlotEventHandler.kt new file mode 100644 index 0000000..6db1ed1 --- /dev/null +++ b/menu/src/main/kotlin/tech/carcadex/kotlinbukkitkit/menu/slot/SlotEventHandler.kt @@ -0,0 +1,14 @@ +package tech.carcadex.kotlinbukkitkit.menu.slot + +public interface SlotEventHandler { + + public fun interact(interact: MenuPlayerSlotInteract) + + public fun render(render: MenuPlayerSlotRender) + + public fun update(update: MenuPlayerSlotUpdate) + + public fun moveToSlot(moveToSlot: MenuPlayerSlotMoveTo) + + public fun clone(): SlotEventHandler +} diff --git a/menu/src/main/kotlin/tech/carcadex/kotlinbukkitkit/menu/slot/SlotEvents.kt b/menu/src/main/kotlin/tech/carcadex/kotlinbukkitkit/menu/slot/SlotEvents.kt new file mode 100644 index 0000000..becbd4f --- /dev/null +++ b/menu/src/main/kotlin/tech/carcadex/kotlinbukkitkit/menu/slot/SlotEvents.kt @@ -0,0 +1,85 @@ +package tech.carcadex.kotlinbukkitkit.menu.slot + +import tech.carcadex.kotlinbukkitkit.menu.Menu +import tech.carcadex.kotlinbukkitkit.menu.MenuPlayer +import tech.carcadex.kotlinbukkitkit.menu.MenuPlayerInteract +import tech.carcadex.kotlinbukkitkit.menu.MenuPlayerInventory +import tech.carcadex.kotlinbukkitkit.menu.MenuPlayerMove +import org.bukkit.Material +import org.bukkit.entity.Player +import org.bukkit.event.inventory.ClickType +import org.bukkit.event.inventory.InventoryAction +import org.bukkit.inventory.Inventory +import org.bukkit.inventory.ItemStack +import java.util.WeakHashMap + +public val MenuPlayerSlot.rawSlotPos: Int get() = slotPos - 1 + +public interface MenuPlayerSlot : MenuPlayer { + public val slotPos: Int + public val slot: Slot + + public fun putPlayerSlotData(key: String, value: Any) { + slot.playerSlotData.getOrPut(player) { + WeakHashMap() + }[key] = value + } + + public fun getPlayerSlotData(key: String): Any? = slot.playerSlotData.get(player)?.get(key) +} + +public interface MenuPlayerInventorySlot : MenuPlayerSlot, MenuPlayerInventory { + + public var showingItem: ItemStack? + get() = getItem(slotPos)?.takeUnless { it.type == Material.AIR } + set(value) = setItem(slotPos, value) + + public fun updateSlotToPlayer() { + menu.updateSlot(slot, player) + } + + public fun updateSlot() { + menu.updateSlot(slot) + } +} + +public open class MenuPlayerSlotInteract( + menu: Menu<*>, + override val slotPos: Int, + override val slot: Slot, + player: Player, + inventory: Inventory, + canceled: Boolean, + public val click: ClickType, + public val action: InventoryAction, + public val clicked: ItemStack?, + public val cursor: ItemStack?, + public val hotbarKey: Int, +) : MenuPlayerInteract(menu, player, inventory, canceled), MenuPlayerInventorySlot + +public class MenuPlayerSlotMoveTo( + override val menu: Menu<*>, + override val slotPos: Int, + override val slot: Slot, + override val player: Player, + override val inventory: Inventory, + override var canceled: Boolean, + override val toMoveSlot: Int, + override var toMoveItem: ItemStack?, +) : MenuPlayerInventorySlot, MenuPlayerMove + +public class MenuPlayerSlotRender( + override val menu: Menu<*>, + override val slotPos: Int, + override val slot: Slot, + override val player: Player, + override val inventory: Inventory, +) : MenuPlayerInventorySlot + +public class MenuPlayerSlotUpdate( + override val menu: Menu<*>, + override val slotPos: Int, + override val slot: Slot, + override val player: Player, + override val inventory: Inventory, +) : MenuPlayerInventorySlot diff --git a/messages/src/main/java/tech/carcadex/kotlinbukkitkit/messages/model/Message.java b/messages/src/main/java/tech/carcadex/kotlinbukkitkit/messages/model/Message.java index 5c935e4..208e135 100644 --- a/messages/src/main/java/tech/carcadex/kotlinbukkitkit/messages/model/Message.java +++ b/messages/src/main/java/tech/carcadex/kotlinbukkitkit/messages/model/Message.java @@ -38,12 +38,12 @@ public interface Message extends ComponentLike { @NotNull List asComponentList(); /** - * Get unparsed value of message + * @return unparsed value of message */ @NotNull List asUnparsedStringList(); /** - * Get unparsed value of message joined to one string + * @return unparsed value of message joined to one string */ @NotNull String asUnparsedString(@NotNull String newLine); diff --git a/settings.gradle.kts b/settings.gradle.kts index 96a784b..3506523 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -7,3 +7,7 @@ include("serialization") include("genref") include("messages") include("commands") +include("utility") +include("exposed") +include("thirdparty") +include("menu") diff --git a/thirdparty/.gitignore b/thirdparty/.gitignore new file mode 100644 index 0000000..b63da45 --- /dev/null +++ b/thirdparty/.gitignore @@ -0,0 +1,42 @@ +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### IntelliJ IDEA ### +.idea/modules.xml +.idea/jarRepositories.xml +.idea/compiler.xml +.idea/libraries/ +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store \ No newline at end of file diff --git a/thirdparty/build.gradle.kts b/thirdparty/build.gradle.kts new file mode 100644 index 0000000..80b71cd --- /dev/null +++ b/thirdparty/build.gradle.kts @@ -0,0 +1,116 @@ +plugins { + id("java") + kotlin("jvm") + kotlin("plugin.serialization") version "1.8.21" + id("com.github.johnrengelman.shadow") version "7.1.2" + id("maven-publish") + id("signing") +} + +group = "tech.carcadex" +val globalArtifactId = "kotlinbukkitkit-thirdparty" +val globalName = "kotlinbukkitkit-thirdparty" + +repositories { + mavenCentral() + maven { + name = "spigotmc-repo" + url = uri("https://hub.spigotmc.org/nexus/content/repositories/snapshots/") + } +} + +dependencies { + compileOnly("org.spigotmc:spigot-api:1.16.5-R0.1-SNAPSHOT") + api(project(":architecture")) + api(project(":utility")) + compileOnly("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1") +} + +tasks.test { + useJUnitPlatform() +} + +kotlin { + jvmToolchain(8) +} + +kotlin { + sourceSets.main { + kotlin.srcDir("build/generated/ksp/main/kotlin") + } + sourceSets.test { + kotlin.srcDir("build/generated/ksp/test/kotlin") + } +} + +java { + withJavadocJar() + withSourcesJar() +} + +publishing { + publications { + create("maven") { + artifactId = globalArtifactId + groupId = "tech.carcadex" + version = version + from(components["java"]) + versionMapping { + usage("java-api") { + fromResolutionOf("runtimeClasspath") + } + usage("java-runtime") { + fromResolutionResult() + } + } + pom { + artifactId = globalArtifactId + groupId = "tech.carcadex" + version = version + name.set(globalArtifactId) + description.set(globalArtifactId) + url.set("https://github.com/CarcadeX/KotlinBukkitKit") + + licenses { + license { + name.set("MIT") + url.set("https://mit-license.org/") + } + } + developers { + developer { + id.set("itzRedTea") + name.set("Red Tea") + email.set("red__tea@outlook.com") + } + } + scm { + connection.set("scm:https://github.com/CarcadeX/$globalName.git") + developerConnection.set("git@github.com:CarcadeX/$globalName.git") + url.set("https://github.com/CarcadeX/KotlinBukkitKit") + } + } + + } + } + + + + repositories { + maven { + val releasesRepoUrl = "https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/" + val snapshotsRepoUrl = "https://s01.oss.sonatype.org/content/repositories/snapshots/" + url = uri(if (version.toString().endsWith("SNAPSHOT")) snapshotsRepoUrl else releasesRepoUrl) + credentials { + username = "itzRedTea" + password = project.findProperty("password").toString() + } + } + + } +} + +signing { + useGpgCmd() + sign(publishing.publications.getByName("maven")) +} diff --git a/thirdparty/src/main/kotlin/tech/carcadex/kotlinbukkitkit/bungeecord/BungeeCordController.kt b/thirdparty/src/main/kotlin/tech/carcadex/kotlinbukkitkit/bungeecord/BungeeCordController.kt new file mode 100644 index 0000000..1e437e9 --- /dev/null +++ b/thirdparty/src/main/kotlin/tech/carcadex/kotlinbukkitkit/bungeecord/BungeeCordController.kt @@ -0,0 +1,55 @@ +package tech.carcadex.kotlinbukkitkit.bungeecord + +import org.bukkit.Bukkit +import org.bukkit.entity.Player +import org.bukkit.plugin.Plugin +import org.bukkit.plugin.messaging.PluginMessageListener +import java.nio.ByteBuffer +import java.nio.charset.Charset + + +object BungeeCordController: PluginMessageListener { + + private val queue = mutableListOf() + private var initialized = false; + private var plugin: Plugin? = null + + fun init(plugin: Plugin) { + if(initialized) return + this.plugin = plugin + Bukkit.getServer().messenger.registerOutgoingPluginChannel(plugin, "BungeeCord") + Bukkit.getServer().messenger.registerIncomingPluginChannel(plugin, "BungeeCord", this) + initialized = true + } + + fun Plugin.initBungeeCordController() { + init(this) + } + + override fun onPluginMessageReceived(channel: String, player: Player, message: ByteArray) { + if (channel != "BungeeCord") return + + val buffer = ByteBuffer.wrap(message) + val subChannel = buffer.readUTF() + val request = queue.firstOrNull { it.subChannel == subChannel } + if(request?.responseCallback != null) { + val infoBuffer = buffer.slice() + val info = ByteArray(infoBuffer.remaining()) + infoBuffer.get(info) + request.responseCallback.invoke(info) + queue.remove(request) + } + } + + fun sendBungeeCord(player: Player, message: ByteArray) { + if(!initialized) { + Bukkit.getLogger().severe("Could not execute sendBungeeCord method, because BungeeCordController" + + " has not been initialized!") + } + player.sendPluginMessage(plugin!!, "BungeeCord", message) + } + + fun addToQueue(request: BungeeCordRequest) = queue.add(request) + + private fun ByteBuffer.readUTF() = String(ByteArray(short.toInt()).apply { get(this) }, Charset.forName("UTF-8")) +} diff --git a/thirdparty/src/main/kotlin/tech/carcadex/kotlinbukkitkit/bungeecord/BungeeCordUtils.kt b/thirdparty/src/main/kotlin/tech/carcadex/kotlinbukkitkit/bungeecord/BungeeCordUtils.kt new file mode 100644 index 0000000..02bc3a4 --- /dev/null +++ b/thirdparty/src/main/kotlin/tech/carcadex/kotlinbukkitkit/bungeecord/BungeeCordUtils.kt @@ -0,0 +1,23 @@ +package tech.carcadex.kotlinbukkitkit.bungeecord + +import com.google.common.io.ByteStreams +import org.bukkit.entity.Player + +typealias ResponseCallback = (message: ByteArray) -> Unit + +class BungeeCordRequest( + val player: Player, + val subChannel: String, + val request: ByteArray? = null, + val responseCallback: ResponseCallback? = null +) { + fun send() { + val out = ByteStreams.newDataOutput() + out.writeUTF(subChannel) + if (request != null) out.write(request) + + player.sendBungeeCord(out.toByteArray()) + + if (responseCallback != null) BungeeCordController.addToQueue(this) + } +} \ No newline at end of file diff --git a/thirdparty/src/main/kotlin/tech/carcadex/kotlinbukkitkit/bungeecord/ExBungeeCord.kt b/thirdparty/src/main/kotlin/tech/carcadex/kotlinbukkitkit/bungeecord/ExBungeeCord.kt new file mode 100644 index 0000000..769f37e --- /dev/null +++ b/thirdparty/src/main/kotlin/tech/carcadex/kotlinbukkitkit/bungeecord/ExBungeeCord.kt @@ -0,0 +1,58 @@ +package tech.carcadex.kotlinbukkitkit.bungeecord + +import org.bukkit.entity.Player +import com.google.common.io.ByteStreams +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine + +// TODO: replace controller with KotlinPlugin lifecycle extension + +public val Player.bungeecord get() = BungeeCord(this) + +public fun Player.sendBungeeCord(message: ByteArray) = BungeeCordController.sendBungeeCord(this, message) + +@JvmInline +public value class BungeeCord(private val player: Player) { + + public fun sendToServer(server: String) = BungeeCordRequest( + player, + "Connect", + ByteStreams.newDataOutput().apply{writeUTF(server)}.toByteArray() + ).send() + + public fun retrieveIp(callback: (address: String, port: Int) -> Unit) = BungeeCordRequest(player, "IP") { + val input = ByteStreams.newDataInput(it) + val ip = input.readUTF() + val port = input.readInt() + callback(ip, port) + }.send() + + public suspend fun retrieveIp(): Pair = suspendCoroutine { continuation -> + retrieveIp { address, port -> continuation.resume(address to port) } + } + + public fun onlinePlayerAt(server: String = "ALL", callback: (playerCount: Int) -> Unit) + = BungeeCordRequest(player, "PlayerCount", ByteStreams.newDataOutput().apply{writeUTF(server)}.toByteArray()) { + val input = ByteStreams.newDataInput(it) + val serverSendTo = input.readUTF() + val playerCount = input.readInt() + callback(playerCount) + }.send() + + public suspend fun onlinePlayerAt(server: String = "ALL"): Int = suspendCoroutine { continuation -> + onlinePlayerAt { continuation.resume(it) } + } + + public fun retrieveServerName(callback: (server: String) -> Unit) = BungeeCordRequest(player, "GetServer") { + val input = ByteStreams.newDataInput(it) + val server = input.readUTF() + callback(server) + }.send() + + public suspend fun retrieveServerName(): String = suspendCoroutine { continuation -> + retrieveServerName { continuation.resume(it) } + } + + public fun kickPlayer(player: String, reason: String) = BungeeCordRequest(this.player, "KickPlayer", + ByteStreams.newDataOutput().apply{writeUTF(player);writeUTF(reason)}.toByteArray()).send() +} \ No newline at end of file diff --git a/utility/.gitignore b/utility/.gitignore new file mode 100644 index 0000000..b63da45 --- /dev/null +++ b/utility/.gitignore @@ -0,0 +1,42 @@ +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### IntelliJ IDEA ### +.idea/modules.xml +.idea/jarRepositories.xml +.idea/compiler.xml +.idea/libraries/ +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store \ No newline at end of file diff --git a/utility/build.gradle.kts b/utility/build.gradle.kts new file mode 100644 index 0000000..c47aef5 --- /dev/null +++ b/utility/build.gradle.kts @@ -0,0 +1,116 @@ +plugins { + id("java") + kotlin("jvm") + kotlin("plugin.serialization") version "1.8.21" + id("com.github.johnrengelman.shadow") version "7.1.2" + id("maven-publish") + id("signing") +} + +group = "tech.carcadex" +val globalArtifactId = "kotlinbukkitkit-utility" +val globalName = "kotlinbukkitkit-utility" + +repositories { + mavenCentral() + maven { + name = "spigotmc-repo" + url = uri("https://hub.spigotmc.org/nexus/content/repositories/snapshots/") + } +} + +dependencies { + compileOnly("org.spigotmc:spigot-api:1.16.5-R0.1-SNAPSHOT") + api(project(":architecture")) + api(project(":extensions")) + compileOnly("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1") +} + +tasks.test { + useJUnitPlatform() +} + +kotlin { + jvmToolchain(8) +} + +kotlin { + sourceSets.main { + kotlin.srcDir("build/generated/ksp/main/kotlin") + } + sourceSets.test { + kotlin.srcDir("build/generated/ksp/test/kotlin") + } +} + +java { + withJavadocJar() + withSourcesJar() +} + +publishing { + publications { + create("maven") { + artifactId = globalArtifactId + groupId = "tech.carcadex" + version = version + from(components["java"]) + versionMapping { + usage("java-api") { + fromResolutionOf("runtimeClasspath") + } + usage("java-runtime") { + fromResolutionResult() + } + } + pom { + artifactId = globalArtifactId + groupId = "tech.carcadex" + version = version + name.set(globalArtifactId) + description.set(globalArtifactId) + url.set("https://github.com/CarcadeX/KotlinBukkitKit") + + licenses { + license { + name.set("MIT") + url.set("https://mit-license.org/") + } + } + developers { + developer { + id.set("itzRedTea") + name.set("Red Tea") + email.set("red__tea@outlook.com") + } + } + scm { + connection.set("scm:https://github.com/CarcadeX/$globalName.git") + developerConnection.set("git@github.com:CarcadeX/$globalName.git") + url.set("https://github.com/CarcadeX/KotlinBukkitKit") + } + } + + } + } + + + + repositories { + maven { + val releasesRepoUrl = "https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/" + val snapshotsRepoUrl = "https://s01.oss.sonatype.org/content/repositories/snapshots/" + url = uri(if (version.toString().endsWith("SNAPSHOT")) snapshotsRepoUrl else releasesRepoUrl) + credentials { + username = "itzRedTea" + password = project.findProperty("password").toString() + } + } + + } +} + +signing { + useGpgCmd() + sign(publishing.publications.getByName("maven")) +} diff --git a/utility/src/main/kotlin/tech/carcadex/kotlinbukkitkit/utility/collections/ExpirationList.kt b/utility/src/main/kotlin/tech/carcadex/kotlinbukkitkit/utility/collections/ExpirationList.kt new file mode 100644 index 0000000..6e70f6e --- /dev/null +++ b/utility/src/main/kotlin/tech/carcadex/kotlinbukkitkit/utility/collections/ExpirationList.kt @@ -0,0 +1,352 @@ +package tech.carcadex.kotlinbukkitkit.utility.collections + +import tech.carcadex.kotlinbukkitkit.architecture.extensions.WithPlugin +import tech.carcadex.kotlinbukkitkit.extensions.scheduler +import org.bukkit.plugin.Plugin +import org.bukkit.scheduler.BukkitTask + +public typealias OnExpireCallback = (T) -> Unit + +public fun Plugin.expirationListOf(): ExpirationList = ExpirationListImpl(this) + +public fun WithPlugin<*>.expirationListOf(): ExpirationList = plugin.expirationListOf() + +public fun expirationListOf(expireTime: Int, plugin: Plugin, vararg elements: E): ExpirationList = plugin.expirationListOf().apply { elements.forEach { add(it, expireTime) } } + +public fun Plugin.expirationListOf(expireTime: Int, vararg elements: E): ExpirationList = expirationListOf(expireTime, this, elements = *elements) + +public fun WithPlugin<*>.expirationListOf(expireTime: Int, vararg elements: E): ExpirationList = plugin.expirationListOf(expireTime, *elements) + +public fun expirationListOf(expireTime: Int, plugin: Plugin, vararg elements: Pair>): ExpirationList = plugin.expirationListOf().apply { elements.forEach { (element, onExpire) -> add(element, expireTime, onExpire) } } + +public fun Plugin.expirationListOf(expireTime: Int, vararg elements: Pair>): ExpirationList = expirationListOf(expireTime, this, elements = *elements) + +public fun WithPlugin<*>.expirationListOf(expireTime: Int, vararg elements: Pair>): ExpirationList = plugin.expirationListOf(expireTime, *elements) + +public interface ExpirationList : MutableIterable { + + /** + * Returns the count of elements in this list. + */ + public val size: Int + + /** + * Returns `true` if the list contains no elements, `false` otherwise. + */ + public fun isEmpty(): Boolean + + /** + * Returns the time missing to expire or `null` if the list don't contain the element + */ + public fun missingTime(element: E): Int? + + /** + * Checks if the specified element is contained in this list. + */ + public operator fun contains(element: E): Boolean + + /** + * Returns the element at the specified index in the list. + */ + public operator fun get(index: Int): E? + + /** + * Returns the index of the first occurrence of the e element in the list, or -1 if the specified + * element is not contained in the list. + */ + public fun indexOf(element: E): Int + + /** + * Return the first element in the list or `null` if the list is empty + */ + public fun first(): E? + + /** + * Return the last element in the list or `null` if the list is empty + */ + public fun last(): E? + + /** + * Removes all elements from this list. + */ + public fun clear() + + /** + * Add the element to the list with an expiration time. + * + * [expireTime] in seconds + * callback [onExpire] is called when the element expires. + */ + public fun add(element: E, expireTime: Int, onExpire: OnExpireCallback? = null) + + /** + * Add the element in the start of list with an expiration time. + * + * [expireTime] in seconds + * callback [onExpire] is called when the element expires. + */ + public fun addFirst(element: E, expireTime: Int, onExpire: OnExpireCallback? = null) + + /** + * Removes an element at the specified [index] from the list. + * + * @return the element that has been removed or `null` if + * the element is not contained in the list. + */ + public fun removeAt(index: Int): E? + + /** + * Removes an element from the list + * + * @return `true` if the element was removed. + */ + public fun remove(element: E): Boolean + + /** + * Removes the first element from the list. + * + * @return the element if was removed. + */ + public fun removeFirst(): E? + + /** + * Removes the last element from the list. + * + * @return the element if was removed. + */ + public fun removeLast(): E? +} + +private class ExpirationNode(var element: E, val expireTime: Int) { + + var next: ExpirationNode? = null + var previous: ExpirationNode? = null + + var onExpire: OnExpireCallback? = null + val startTime: Long = System.currentTimeMillis() +} + +public class ExpirationListImpl(private val plugin: Plugin) : ExpirationList { + + private var head: ExpirationNode? = null + private var tail: ExpirationNode? = null + + private var task: BukkitTask? = null + private var emptyCount: Byte = 0 + + override val size: Int get() = _size + private var _size: Int = 0 + private set + + override fun isEmpty(): Boolean = _size == 0 + + override fun missingTime(element: E): Int? { + return getNodeByElement(element) + ?.let { it.expireTime - ((System.currentTimeMillis() - it.startTime) / 1000) } + ?.toInt() + } + + override operator fun contains(element: E): Boolean = indexOf(element) > -1 + + override operator fun get(index: Int): E? { + return getNode(index)?.element + } + + override fun first(): E? = head?.element + + override fun last(): E? = tail?.element + + override operator fun iterator(): MutableIterator { + return object : MutableIterator { + private val nodeIterator = nodeIterator() + + override fun hasNext() = nodeIterator.hasNext() + override fun next() = nodeIterator.next().element + override fun remove() = nodeIterator.remove() + } + } + + override fun indexOf(element: E): Int { + if (head == null) return -1 + var node = head + var count = 0 + do { + if (node?.element === element) { + return count + } + node = node?.next + count++ + } while (node != null) + return -1 + } + + override fun clear() { + head = null + tail = null + _size = 0 + } + + override fun add(element: E, expireTime: Int, onExpire: OnExpireCallback?) { + if (expireTime <= 0) throw IllegalArgumentException("expireTime need to be greater then 0") + val newNode = ExpirationNode(element, expireTime).also { it.onExpire = onExpire } + if (head == null) { + head = newNode + tail = newNode + } else { + tail?.next = newNode + tail = newNode + } + _size++ + generateTask() + } + + override fun addFirst(element: E, expireTime: Int, onExpire: OnExpireCallback?) { + if (expireTime <= 0) throw IllegalArgumentException("expireTime need to be greater then 0") + val newNode = ExpirationNode(element, expireTime).also { it.onExpire = onExpire } + if (head == null) { + head = newNode + tail = newNode + } else { + head?.previous = newNode + head = newNode + } + _size++ + generateTask() + } + + override fun removeAt(index: Int): E? { + return getNode(index)?.also { + removeNode(it) + }?.element + } + + override fun remove(element: E): Boolean = getNodeByElement(element)?.let { true.apply { removeNode(it) } } ?: false + + override fun removeFirst(): E? { + val next = head?.next + val headElement = head?.element + if (next == null) { + tail = null + head = null + } else { + head = next + next.previous = null + } + return headElement + } + + override fun removeLast(): E? { + val previous = tail?.previous + val tailElement = tail?.element + if (previous == null) { + tail = null + head = null + } else { + tail = previous + previous.next = null + } + return tailElement + } + + private fun getNode(index: Int): ExpirationNode? { + if (index < 0 || index >= _size) return null + + val mid = _size / 2 + return if (index > mid) { + getFromSpecificSide(index - mid, tail) { it?.previous } + } else { + getFromSpecificSide(index, head) { it?.next } + } + } + + private fun getNodeByElement(element: E): ExpirationNode? { + if (head == null) return null + var node = head + do { + if (node?.element === element) { + return node + } + node = node?.next + } while (node != null) + return null + } + + private inline fun getFromSpecificSide( + count: Int, + start: ExpirationNode?, + next: (ExpirationNode?) -> ExpirationNode?, + ): ExpirationNode { + var index = 0 + var current = start + while (index != count) { + current = next(current) + index++ + } + return current!! + } + + private fun nodeIterator(): MutableIterator> { + return object : MutableIterator> { + + private var current = head + + override fun hasNext(): Boolean { + return current != null + } + + override fun next(): ExpirationNode { + val aux = current + current = current?.next + return aux!! + } + + override fun remove() { + if (current != null) { + removeNode(current!!) + } + } + } + } + + private fun checkTime(current: Long, node: ExpirationNode) = ((current - node.startTime) / 1000) - node.expireTime >= 0 + + private fun generateTask() { + if (task == null) { + task = scheduler { + if (isEmpty()) { + emptyCount++ + } else { + emptyCount = 0 + val current = System.currentTimeMillis() + for (node in nodeIterator()) { + if (checkTime(current, node)) { + if (node.onExpire != null) node.onExpire?.invoke(node.element) + removeNode(node) + } + } + } + if (emptyCount > 9) { + cancel() + task = null + } + }.runTaskTimer(plugin, 0L, 20L) + } + } + + private fun removeNode(node: ExpirationNode) { + if (node === head && node === tail) { + head = null + tail = null + } else if (node === head) { + head = node.next?.apply { previous = null } + } else if (node === tail) { + tail = node.previous?.apply { next = null } + } else { + node.apply { + previous?.next = next + next?.previous = previous + } + } + _size-- + } +} diff --git a/utility/src/main/kotlin/tech/carcadex/kotlinbukkitkit/utility/collections/ExpirationMap.kt b/utility/src/main/kotlin/tech/carcadex/kotlinbukkitkit/utility/collections/ExpirationMap.kt new file mode 100644 index 0000000..fb75f3c --- /dev/null +++ b/utility/src/main/kotlin/tech/carcadex/kotlinbukkitkit/utility/collections/ExpirationMap.kt @@ -0,0 +1,175 @@ +package tech.carcadex.kotlinbukkitkit.utility.collections + +import tech.carcadex.kotlinbukkitkit.architecture.extensions.WithPlugin +import tech.carcadex.kotlinbukkitkit.extensions.scheduler +import tech.carcadex.kotlinbukkitkit.utility.extensions.now +import org.bukkit.plugin.Plugin +import org.bukkit.scheduler.BukkitTask +import java.util.* + +public typealias OnExpireMapCallback = (K, V) -> Unit + +public fun Plugin.expirationMapOf(): ExpirationMap = ExpirationMapImpl(this) + +public fun WithPlugin<*>.expirationMapOf(): ExpirationMap = plugin.expirationMapOf() + +public fun expirationMapOf(expireTime: Long, plugin: Plugin, vararg elements: Pair): ExpirationMap = plugin.expirationMapOf().apply { elements.forEach { (key, value) -> put(key, value, expireTime) } } + +public fun Plugin.expirationMapOf(expireTime: Long, vararg elements: Pair): ExpirationMap = expirationMapOf(expireTime, this, elements = *elements) + +public fun WithPlugin<*>.expirationMapOf(expireTime: Long, vararg elements: Pair): ExpirationMap = plugin.expirationMapOf(expireTime, *elements) + +public fun expirationMapOf(expireTime: Long, plugin: Plugin, vararg elements: Triple>): ExpirationMap = plugin.expirationMapOf().apply { elements.forEach { (key, value, onExpire) -> put(key, value, expireTime, onExpire) } } + +public fun Plugin.expirationMapOf(expireTime: Long, vararg elements: Triple>): ExpirationMap = expirationMapOf(expireTime, this, elements = *elements) + +public fun WithPlugin<*>.expirationMapOf(expireTime: Long, vararg elements: Triple>): ExpirationMap = plugin.expirationMapOf(expireTime, *elements) + +public interface ExpirationMap : MutableMap, WithPlugin { + + /** + * Returns the missing time on seconds to expire the key, + * -1 if was not specified the expiration time before(permanent key) or + * `null` if the key is not contained in the map. + */ + public fun missingTime(key: K): Long? + + /** + * Set expiration time to the key and returns `true` if the key is found + * or false if the key is not contained in the map. + */ + public fun expire(key: K, time: Long): Boolean + + /** + * Set expiration time to the key and returns `true` if the key is found + * or false if the key is not contained in the map. + * + * [time] in seconds. + * [callback] is called when the key expires. + */ + public fun expire(key: K, time: Long, callback: OnExpireMapCallback): Boolean + + /** + * Associates the specified [value] with the specified [key] in the map + * with an expiration time. + * + * @return the previous value associated with the key, or `null` if the key was not present in the map. + */ + public fun put(key: K, value: V, time: Long): V? + + /** + * Associates the specified [value] with the specified [key] in the map + * with an expiration time. + * + * [time] in seconds. + * [callback] is called when the key expires. + * + * @return the previous value associated with the key, or `null` if the key was not present in the map. + */ + public fun put(key: K, value: V, time: Long, callback: OnExpireMapCallback): V? +} + +public class ExpirationMapImpl( + override val plugin: Plugin, + private val initialMap: MutableMap = WeakHashMap(), +) : ExpirationMap, MutableMap by initialMap { + + private val putTime: MutableMap = mutableMapOf() + private val expiration: MutableMap = mutableMapOf() + private val whenExpire: MutableMap> = mutableMapOf() + + override fun missingTime(key: K): Long? { + return if (containsKey(key)) { + (expiration.get(key) ?: return -1) - ((now() - putTime.getOrPut(key) { now() }) / 1000) + } else { + null + } + } + + private fun ex(key: K, time: Long) { + putTime.put(key, now()) + expiration.put(key, time) + } + + private fun whenEx(key: K, callback: OnExpireMapCallback) { + whenExpire.put(key, callback) + } + + override fun expire(key: K, time: Long): Boolean { + if (containsKey(key)) { + ex(key, time) + return true + } else { + return false + } + } + + override fun expire(key: K, time: Long, callback: OnExpireMapCallback): Boolean { + if (expire(key, time)) { + whenEx(key, callback) + return true + } else { + return false + } + } + + override fun put(key: K, value: V): V? { + val result = initialMap.put(key, value) + generateTask() + return result + } + + override fun put(key: K, value: V, time: Long): V? { + if (time <= 0) throw IllegalArgumentException("expiration time can't be negative or zero") + val result = put(key, value) + ex(key, time) + return result + } + + override fun put(key: K, value: V, time: Long, callback: OnExpireMapCallback): V? { + if (time <= 0) throw IllegalArgumentException("expiration time can't be negative or zero") + val result = put(key, value) + ex(key, time) + whenEx(key, callback) + return result + } + + override fun remove(key: K): V? { + val result = initialMap.remove(key) + if (result != null) { + putTime.remove(key) + expiration.remove(key) + whenExpire.remove(key) + } + return result + } + + private fun checkTime(current: Long, key: K) = ((current - (putTime.getOrPut(key) { current })) / 1000) - (expiration.get(key) ?: 0) >= 0 + + private var task: BukkitTask? = null + private var emptyCount: Byte = 0 + private fun generateTask() { + if (task == null) { + task = scheduler { + if (isEmpty()) { + emptyCount++ + } else { + emptyCount = 0 + val current = now() + for ((key, value) in entries) { + if (checkTime(current, key)) { + whenExpire.remove(key)?.invoke(key, value) + initialMap.remove(key) + putTime.remove(key) + expiration.remove(key) + } + } + } + if (emptyCount > 5) { + cancel() + task = null + } + }.runTaskTimer(plugin, 0L, 20L) + } + } +} diff --git a/utility/src/main/kotlin/tech/carcadex/kotlinbukkitkit/utility/collections/ObservableCollections.kt b/utility/src/main/kotlin/tech/carcadex/kotlinbukkitkit/utility/collections/ObservableCollections.kt new file mode 100644 index 0000000..d10bd9a --- /dev/null +++ b/utility/src/main/kotlin/tech/carcadex/kotlinbukkitkit/utility/collections/ObservableCollections.kt @@ -0,0 +1,238 @@ +package tech.carcadex.kotlinbukkitkit.utility.collections + +public typealias ObservableListener = (ObservableAction) -> Unit +public enum class ObservableAction { ADD, SET, REPLACE, REMOVE, CLEAR } + +public fun observableListOf(mutableList: MutableList): ObservableList = mutableList.asObservable() +public fun observableSetOf(mutableSet: MutableSet): ObservableSet = mutableSet.asObservable() +public fun observableMapOf(mutableMap: MutableMap): ObservableMap = mutableMap.asObservable() + +public fun MutableList.asObservable(): ObservableList = ObservableList(this) +public fun MutableSet.asObservable(): ObservableSet = ObservableSet(this) +public fun MutableMap.asObservable(): ObservableMap = ObservableMap(this) + +public class ObservableList( + private val list: MutableList, +) : MutableList by list, ObservableCollection { + + override val listeners: MutableList = mutableListOf() + + override fun add(element: T): Boolean { + runListeners(ObservableAction.ADD) + return list.add(element) + } + + override fun add(index: Int, element: T) { + runListeners(ObservableAction.ADD) + return list.add(index, element) + } + + override fun addAll(index: Int, elements: Collection): Boolean { + runListeners(ObservableAction.ADD) + return list.addAll(index, elements) + } + + override fun addAll(elements: Collection): Boolean { + runListeners(ObservableAction.ADD) + return list.addAll(elements) + } + + override fun clear() { + list.clear() + runListeners(ObservableAction.CLEAR) + } + + override fun listIterator(): MutableListIterator { + return ObservableMutableListIterator( + list.listIterator(), + ::runListeners, + ) + } + + override fun listIterator(index: Int): MutableListIterator { + return ObservableMutableListIterator( + list.listIterator(index), + ::runListeners, + ) + } + + override fun remove(element: T): Boolean { + return list.remove(element).ifTrue { + runListeners(ObservableAction.REMOVE) + } + } + + override fun removeAll(elements: Collection): Boolean { + return list.removeAll(elements).ifTrue { + runListeners(ObservableAction.REMOVE) + } + } + + override fun removeAt(index: Int): T { + return list.removeAt(index).apply { + runListeners(ObservableAction.REMOVE) + } + } + + override fun retainAll(elements: Collection): Boolean { + return list.retainAll(elements).ifTrue { + runListeners(ObservableAction.REMOVE) + } + } + + override fun set(index: Int, element: T): T { + return list.set(index, element).apply { + runListeners(ObservableAction.SET) + } + } +} + +public class ObservableMutableListIterator( + private val iterator: MutableListIterator, + private val runListeners: ObservableListener, +) : MutableListIterator by iterator { + override fun add(element: T) { + iterator.add(element) + runListeners(ObservableAction.ADD) + } + + override fun remove() { + iterator.remove() + runListeners(ObservableAction.REMOVE) + } + + override fun set(element: T) { + iterator.set(element) + runListeners(ObservableAction.SET) + } +} + +public class ObservableSet( + private val set: MutableSet, +) : MutableSet by set, ObservableCollection { + + override val listeners: MutableList = mutableListOf() + + override fun add(element: T): Boolean { + return set.add(element).ifTrue { + runListeners(ObservableAction.ADD) + } + } + + override fun addAll(elements: Collection): Boolean { + return set.addAll(elements).ifTrue { + runListeners(ObservableAction.ADD) + } + } + + override fun clear() { + set.clear() + runListeners(ObservableAction.CLEAR) + } + + override fun iterator(): MutableIterator { + return ObservableMutableIterator( + set.iterator(), + ::runListeners, + ) + } + + override fun remove(element: T): Boolean { + return set.remove(element).ifTrue { + runListeners(ObservableAction.REMOVE) + } + } + + override fun removeAll(elements: Collection): Boolean { + return set.removeAll(elements).ifTrue { + runListeners(ObservableAction.REMOVE) + } + } + + override fun retainAll(elements: Collection): Boolean { + return set.retainAll(elements).ifTrue { + runListeners(ObservableAction.REMOVE) + } + } +} + +public class ObservableMutableIterator( + private val iterator: MutableIterator, + private val runListeners: ObservableListener, +) : MutableIterator by iterator { + override fun remove() { + iterator.remove() + runListeners(ObservableAction.REMOVE) + } +} + +public class ObservableMap( + private val map: MutableMap, +) : MutableMap by map, ObservableHolder { + + override val listeners: MutableList = mutableListOf() + + override fun clear() { + map.clear() + runListeners(ObservableAction.CLEAR) + } + + override fun put(key: K, value: V): V? { + return put(key, value).apply { + runListeners(ObservableAction.ADD) + } + } + + override fun putAll(from: Map) { + putAll(from) + runListeners(ObservableAction.ADD) + } + + override fun putIfAbsent(key: K, value: V): V? { + return map.putIfAbsent(key, value).also { + if (it == null) runListeners(ObservableAction.ADD) + } + } + + override fun remove(key: K): V? { + return map.remove(key)?.also { + runListeners(ObservableAction.REMOVE) + } + } + + override fun remove(key: K, value: V): Boolean { + return map.remove(key, value).ifTrue { + runListeners(ObservableAction.REMOVE) + } + } + + override fun replace(key: K, first: V, second: V): Boolean { + return map.replace(key, first, second).ifTrue { + runListeners(ObservableAction.REPLACE) + } + } + + override fun replace(key: K, value: V): V? { + return map.replace(key, value)?.also { + runListeners(ObservableAction.REPLACE) + } + } +} + +public interface ObservableCollection : MutableCollection, ObservableHolder + +public interface ObservableHolder { + public val listeners: MutableList + + public fun observe(listener: ObservableListener) { + listeners.add(listener) + } + + public fun runListeners(action: ObservableAction) { + for (listener in listeners) { + listener(action) + } + } +} + +private inline fun Boolean.ifTrue(block: () -> Unit) = apply { block() } diff --git a/utility/src/main/kotlin/tech/carcadex/kotlinbukkitkit/utility/collections/OnlinePlayerCollections.kt b/utility/src/main/kotlin/tech/carcadex/kotlinbukkitkit/utility/collections/OnlinePlayerCollections.kt new file mode 100644 index 0000000..0f5ea85 --- /dev/null +++ b/utility/src/main/kotlin/tech/carcadex/kotlinbukkitkit/utility/collections/OnlinePlayerCollections.kt @@ -0,0 +1,270 @@ +package tech.carcadex.kotlinbukkitkit.utility.collections + +import tech.carcadex.kotlinbukkitkit.architecture.extensions.WithPlugin +import tech.carcadex.kotlinbukkitkit.extensions.KListener +import tech.carcadex.kotlinbukkitkit.extensions.event +import tech.carcadex.kotlinbukkitkit.extensions.registerEvents +import tech.carcadex.kotlinbukkitkit.extensions.unregisterListener +import org.bukkit.entity.Player +import org.bukkit.event.player.PlayerKickEvent +import org.bukkit.event.player.PlayerQuitEvent +import org.bukkit.plugin.Plugin +import java.util.* +import kotlin.collections.HashMap +import kotlin.collections.HashSet + +public typealias WhenPlayerQuitCollectionCallback = Player.() -> Unit +public typealias WhenPlayerQuitMapCallback = Player.(V) -> Unit + +// List + +public fun Plugin.onlinePlayerListOf(): OnlinePlayerList = OnlinePlayerList(this) + +public fun WithPlugin<*>.onlinePlayerListOf(): OnlinePlayerList = plugin.onlinePlayerListOf() + +public fun onlinePlayerListOf(vararg players: Player, plugin: Plugin): OnlinePlayerList = OnlinePlayerList(plugin).apply { addAll(players) } + +public fun Plugin.onlinePlayerListOf(vararg players: Player): OnlinePlayerList = onlinePlayerListOf(*players, plugin = this) + +public fun WithPlugin<*>.onlinePlayerListOf(vararg players: Player): OnlinePlayerList = plugin.onlinePlayerListOf(*players) + +public fun onlinePlayerListOf(vararg pair: Pair, plugin: Plugin): OnlinePlayerList = OnlinePlayerList(plugin).apply { pair.forEach { (player, whenPlayerQuit) -> add(player, whenPlayerQuit) } } + +public fun Plugin.onlinePlayerListOf(vararg pair: Pair): OnlinePlayerList = onlinePlayerListOf(*pair, plugin = this) + +public fun WithPlugin<*>.onlinePlayerListOf(vararg pair: Pair): OnlinePlayerList = plugin.onlinePlayerListOf(*pair) + +// Set + +public fun Plugin.onlinePlayerSetOf(): OnlinePlayerSet = OnlinePlayerSet(this) + +public fun WithPlugin<*>.onlinePlayerSetOf(): OnlinePlayerSet = plugin.onlinePlayerSetOf() + +public fun onlinePlayerSetOf(vararg players: Player, plugin: Plugin): OnlinePlayerSet = OnlinePlayerSet(plugin).apply { addAll(players) } + +public fun Plugin.onlinePlayerSetOf(vararg players: Player): OnlinePlayerSet = onlinePlayerSetOf(*players, plugin = this) + +public fun WithPlugin<*>.onlinePlayerSetOf(vararg players: Player): OnlinePlayerSet = plugin.onlinePlayerSetOf(*players) + +public fun onlinePlayerSetOf(vararg pair: Pair, plugin: Plugin): OnlinePlayerSet = OnlinePlayerSet(plugin).apply { pair.forEach { (player, whenPlayerQuit) -> add(player, whenPlayerQuit) } } + +public fun Plugin.onlinePlayerSetOf(vararg pair: Pair): OnlinePlayerSet = onlinePlayerSetOf(*pair, plugin = this) + +public fun WithPlugin<*>.onlinePlayerSetOf(vararg pair: Pair): OnlinePlayerSet = plugin.onlinePlayerSetOf(*pair) + +// Map + +public fun Plugin.onlinePlayerMapOf(): OnlinePlayerMap = OnlinePlayerMap(this) + +public fun WithPlugin<*>.onlinePlayerMapOf(): OnlinePlayerMap = plugin.onlinePlayerMapOf() + +public fun onlinePlayerMapOf(vararg pair: Pair, plugin: Plugin): OnlinePlayerMap = OnlinePlayerMap(plugin).apply { putAll(pair) } + +public fun Plugin.onlinePlayerMapOf(vararg pair: Pair): OnlinePlayerMap = onlinePlayerMapOf(*pair, plugin = this) + +public fun WithPlugin<*>.onlinePlayerMapOf(vararg pair: Pair): OnlinePlayerMap = plugin.onlinePlayerMapOf(*pair) + +public fun onlinePlayerMapOf(vararg triple: Triple>, plugin: Plugin): OnlinePlayerMap = OnlinePlayerMap(plugin).apply { triple.forEach { (player, value, whenPlayerQuit) -> put(player, value, whenPlayerQuit) } } + +public fun Plugin.onlinePlayerMapOf(vararg triple: Triple>): OnlinePlayerMap = onlinePlayerMapOf(*triple, plugin = this) + +public fun WithPlugin<*>.onlinePlayerMapOf(vararg triple: Triple>): OnlinePlayerMap = plugin.onlinePlayerMapOf(*triple) + +public class OnlinePlayerList(override val plugin: Plugin) : LinkedList(), OnlinePlayerCollection { + private val whenQuit: MutableList> = LinkedList() + + override fun add(player: Player, whenPlayerQuitCallback: Player.() -> Unit): Boolean { + if (super.add(player, whenPlayerQuitCallback)) { + whenQuit.add(player to whenPlayerQuitCallback) + return true + } else { + return false + } + } + + override fun add(element: Player): Boolean { + if (isEmpty()) registerEvents(plugin) + return super.add(element) + } + + override fun quit(player: Player): Boolean { + if (super.quit(player)) { + val iterator = whenQuit.iterator() + for (pair in iterator) { + if (pair.first == player) { + iterator.remove() + pair.second.invoke(pair.first) + } + } + return true + } else { + return false + } + } + + override fun removeFirst(): Player { + return super.removeFirst().also { + checkRegistration() + } + } + + override fun removeLastOccurrence(p0: Any?): Boolean { + return super.removeLastOccurrence(p0).also { + if (it) checkRegistration() + } + } + + override fun removeAt(p0: Int): Player { + return super.removeAt(p0).also { + checkRegistration() + } + } + + override fun remove(element: Player): Boolean { + if (super.remove(element)) { + checkRegistration() + return true + } else { + return false + } + } + + override fun removeLast(): Player { + return super.removeLast().also { + checkRegistration() + } + } +} + +public class OnlinePlayerSet(override val plugin: Plugin) : HashSet(), OnlinePlayerCollection { + private val whenQuit: MutableMap = mutableMapOf() + + override fun add(player: Player, whenPlayerQuitCallback: WhenPlayerQuitCollectionCallback): Boolean { + if (super.add(player, whenPlayerQuitCallback)) { + whenQuit.put(player, whenPlayerQuitCallback) + + checkRegistration() + return true + } else { + return false + } + } + + override fun add(element: Player): Boolean { + return super.add(element).also { + if (it) checkRegistration() + } + } + + override fun remove(element: Player): Boolean { + return super.remove(element).also { + if (it) checkRegistration() + } + } + + override fun quit(player: Player): Boolean { + if (super.quit(player)) { + whenQuit.remove(player)?.also { block -> + block.invoke(player) + } + return true + } else { + return false + } + } +} + +public interface OnlinePlayerCollection : MutableCollection, KListener { + + /** + * Adds a new Player to the collection with a callback for when the player quits the server. + */ + public fun add(player: Player, whenPlayerQuit: WhenPlayerQuitCollectionCallback): Boolean { + return add(player).also { + if (it) checkRegistration() + } + } + + /** + * Removes the player from the collection, calling the [WhenPlayerQuitCollectionCallback] provided. + */ + public fun quit(player: Player): Boolean { + return remove(player).also { + if (it) checkRegistration() + } + } + + /** + * Clear the collection calling all [WhenPlayerQuitCollectionCallback] from the Players. + */ + public fun clearQuiting() { + toMutableList().forEach { + quit(it) + } + } +} + +internal fun OnlinePlayerCollection.checkRegistration() { + if (size == 1) { + event { quit(player) } + event { quit(player) } + } else if (size == 0) { + unregisterListener() + } +} + +public class OnlinePlayerMap(override val plugin: Plugin) : HashMap(), KListener { + private val whenQuit: HashMap> = hashMapOf() + + /** + * Puts a Player to the map with a [value] and a callback for when the player quits the server. + */ + public fun put(key: Player, value: V, whenPlayerQuit: WhenPlayerQuitMapCallback): V? { + whenQuit.put(key, whenPlayerQuit) + return put(key, value).also { + checkRegistration() + } + } + + /** + * Removes the player from the map, calling the [WhenPlayerQuitMapCallback] provided. + */ + public fun quit(player: Player) { + remove(player)?.also { + whenQuit.remove(player)?.also { block -> + block.invoke(player, it) + } + checkRegistration() + } + } + + /** + * Clear the map calling all [WhenPlayerQuitMapCallback] from the Players. + */ + public fun clearQuiting() { + keys.toMutableList().forEach { + quit(it) + } + } + + override fun remove(key: Player): V? { + return super.remove(key).also { + checkRegistration() + } + } + + override fun remove(key: Player, value: V): Boolean { + return super.remove(key, value).also { + checkRegistration() + } + } + + private fun checkRegistration() { + if (size == 1) { + event { quit(player) } + event { quit(player) } + } else if (size == 0) { + unregisterListener() + } + } +} diff --git a/utility/src/main/kotlin/tech/carcadex/kotlinbukkitkit/utility/extensions/Collection.kt b/utility/src/main/kotlin/tech/carcadex/kotlinbukkitkit/utility/extensions/Collection.kt new file mode 100644 index 0000000..4fdd6cc --- /dev/null +++ b/utility/src/main/kotlin/tech/carcadex/kotlinbukkitkit/utility/extensions/Collection.kt @@ -0,0 +1,28 @@ +package tech.carcadex.kotlinbukkitkit.utility.extensions + +public fun Collection.containsIgnoreCase( + element: String, +): Boolean = any { it.equals(element, true) } + +public fun MutableCollection.clear(onRemove: (T) -> Unit) { + toMutableList().forEach { + remove(it) + onRemove(it) + } +} + +public fun Array.containsIgnoreCase( + element: String, +): Boolean = any { it.equals(element, true) } + +public fun Map.containsKeyIgnoreCase( + key: String, +): Boolean = keys.containsIgnoreCase(key) + +public fun Map.getIgnoreCase( + key: String, +): V? = entries.find { it.key.equals(key, true) }?.value + +public fun MutableMap.clear(onRemove: (K, V) -> Unit) { + keys.toMutableSet().forEach { onRemove(it, remove(it)!!) } +} diff --git a/utility/src/main/kotlin/tech/carcadex/kotlinbukkitkit/utility/extensions/Duration.kt b/utility/src/main/kotlin/tech/carcadex/kotlinbukkitkit/utility/extensions/Duration.kt new file mode 100644 index 0000000..a5b1b59 --- /dev/null +++ b/utility/src/main/kotlin/tech/carcadex/kotlinbukkitkit/utility/extensions/Duration.kt @@ -0,0 +1,19 @@ +package tech.carcadex.kotlinbukkitkit.utility.extensions + +import kotlin.time.Duration +import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.DurationUnit + +public fun now(): Long = System.currentTimeMillis() +public fun nowNano(): Long = System.nanoTime() + +public val Long.ticks: Duration get() = toDouble().ticks +public val Int.ticks: Duration get() = toDouble().ticks +public val Double.ticks: Duration get() = tickToMilliseconds(this).milliseconds + +public val Duration.inTicks: Double get() = millisecondsToTick(toDouble(DurationUnit.MILLISECONDS)) + +public fun Duration.toLongTicks(): Long = inTicks.toLong() + +private fun tickToMilliseconds(value: Double): Double = value * 50.0 +private fun millisecondsToTick(value: Double): Double = value / 50.0 diff --git a/utility/src/main/kotlin/tech/carcadex/kotlinbukkitkit/utility/extensions/PositionRange.kt b/utility/src/main/kotlin/tech/carcadex/kotlinbukkitkit/utility/extensions/PositionRange.kt new file mode 100644 index 0000000..a4e6b47 --- /dev/null +++ b/utility/src/main/kotlin/tech/carcadex/kotlinbukkitkit/utility/extensions/PositionRange.kt @@ -0,0 +1,122 @@ +package tech.carcadex.kotlinbukkitkit.utility.extensions + +import tech.carcadex.kotlinbukkitkit.utility.types.BlockPos +import tech.carcadex.kotlinbukkitkit.utility.types.ChunkPos +import tech.carcadex.kotlinbukkitkit.utility.types.VectorComparable +import tech.carcadex.kotlinbukkitkit.utility.types.asBlockPos +import tech.carcadex.kotlinbukkitkit.utility.types.asBukkitBlock +import tech.carcadex.kotlinbukkitkit.utility.types.asBukkitChunk +import tech.carcadex.kotlinbukkitkit.utility.types.asBukkitLocation +import tech.carcadex.kotlinbukkitkit.utility.types.asPos +import org.bukkit.Chunk +import org.bukkit.Location +import org.bukkit.block.Block + +// BUKKIT THINGS + +public operator fun PosRange<*, BlockPos>.contains(other: Location): Boolean = contains(other.asPos()) +public operator fun PosRange<*, BlockPos>.contains(other: Block): Boolean = contains(other.asPos()) +public operator fun PosRange<*, ChunkPos>.contains(other: Chunk): Boolean = contains(other.asPos()) + +public operator fun Location.rangeTo(other: Location): PosRange { + return PosRange(this.asBlockPos(), other.asBlockPos()) { + RangeIteratorWithFactor( + this, + other, + { it.asBukkitLocation(world) }, + { it.asBlockPos() }, + ) + } +} + +public operator fun Block.rangeTo(other: Block): PosRange { + return PosRange(this.asPos(), other.asPos()) { + RangeIteratorWithFactor( + this, + other, + { it.asBukkitBlock(world) }, + { it.asPos() }, + ) + } +} + +public operator fun Chunk.rangeTo(other: Chunk): PosRange { + return PosRange(this.asPos(), other.asPos()) { + RangeIteratorWithFactor( + this, + other, + { it.asBukkitChunk(world) }, + { it.asPos() }, + ) + } +} + +public class PosRange>( + public val first: POS, + public val last: POS, + private val buildIterator: () -> Iterator, +) : ClosedRange, Iterable { + override val endInclusive: POS get() = last + override val start: POS get() = first + + override fun contains(value: POS): Boolean { + val firstAxis = first.axis() + val lastAxis = last.axis() + return value.axis().withIndex().all { (index, it) -> + it >= firstAxis[index] && it <= lastAxis[index] + } + } + + override fun iterator(): Iterator = buildIterator() +} + +public class PosRangeIterator>( + first: T, + last: T, + private val factor: (axis: IntArray) -> T, +) : Iterator { + private val firstAxis = first.axis() + private val lastAxis = last.axis() + private val closedAxisRanges = firstAxis.mapIndexed { index, it -> + IntProgression.fromClosedRange(it.toInt(), lastAxis[index].toInt(), 1) + } + private val iteratorAxis = closedAxisRanges.map { it.iterator() }.toTypedArray() + + private val actualAxis = iteratorAxis.toList().subList(0, iteratorAxis.size - 1) + .map { it.nextInt() } + .toTypedArray() + + override fun hasNext(): Boolean { + return iteratorAxis.any { it.hasNext() } + } + + override fun next(): T { + val lastIndex = iteratorAxis.size - 1 + val last = iteratorAxis[lastIndex] + if (last.hasNext()) { + val axis = IntArray(actualAxis.size) { actualAxis[it] } + last.nextInt() + return factor(axis) + } + for (i in lastIndex - 1 downTo 0) { + val axis = iteratorAxis[i] + if (axis.hasNext()) { + actualAxis[i] = axis.nextInt() + iteratorAxis[i + 1] = closedAxisRanges[i + 1].iterator() + break + } + } + return next() + } +} + +public class RangeIteratorWithFactor>( + start: T, + end: T, + private val factor: (POS) -> T, + private val posFactor: (T) -> POS, +) : Iterator { + public val iterator: PosRangeIterator = PosRangeIterator(posFactor(start), posFactor(end), posFactor(start)::factor) + + override fun hasNext(): Boolean = iterator.hasNext() + override fun next(): T = factor(iterator.next()) +} diff --git a/utility/src/main/kotlin/tech/carcadex/kotlinbukkitkit/utility/extensions/Random.kt b/utility/src/main/kotlin/tech/carcadex/kotlinbukkitkit/utility/extensions/Random.kt new file mode 100644 index 0000000..a882c7f --- /dev/null +++ b/utility/src/main/kotlin/tech/carcadex/kotlinbukkitkit/utility/extensions/Random.kt @@ -0,0 +1,19 @@ +package tech.carcadex.kotlinbukkitkit.utility.extensions + +import kotlin.random.Random + +public fun Collection.randomOrNull(): E? = runCatching { random() }.getOrNull() +public fun Collection.randomIndex(): Int = if (size > 0) Random.nextInt(size) else -1 + +public fun Collection.randomize(): MutableList { + val oldList = toMutableList() + val newList = mutableListOf() + + for (i in 0 until size) { + val index = oldList.randomIndex() + newList.add(oldList.get(index)) + oldList.removeAt(index) + } + + return newList +} diff --git a/utility/src/main/kotlin/tech/carcadex/kotlinbukkitkit/utility/extensions/String.kt b/utility/src/main/kotlin/tech/carcadex/kotlinbukkitkit/utility/extensions/String.kt new file mode 100644 index 0000000..69de176 --- /dev/null +++ b/utility/src/main/kotlin/tech/carcadex/kotlinbukkitkit/utility/extensions/String.kt @@ -0,0 +1,34 @@ +package tech.carcadex.kotlinbukkitkit.utility.extensions + +private val unicodeRegex = "((\\\\u)([0-9]{4}))".toRegex() + +public fun String.javaUnicodeToCharacter(): String = unicodeRegex.replace(this) { + String(charArrayOf(it.destructured.component3().toInt(16).toChar())) +} + +public fun T.print(): T = also { println(it) } + +public fun String.centralize( + length: Int, + spacer: String = " ", + prefix: String = "", + suffix: String = "", +): String { + if (this.length >= length) return this + val part = prefix + spacer.repeat((length - this.length) / 2) + suffix + return part + this + part +} + +public val TRUE_CASES: Array = arrayOf("true") + get() = field.clone() +public val FALSE_CASES: Array = arrayOf("false") + get() = field.clone() + +public fun String.toBooleanOrNull( + trueCases: Array = TRUE_CASES, + falseCases: Array = FALSE_CASES, +): Boolean? = when { + trueCases.any { it.equals(this, true) } -> true + falseCases.any { it.equals(this, true) } -> false + else -> null +} diff --git a/utility/src/main/kotlin/tech/carcadex/kotlinbukkitkit/utility/types/Position.kt b/utility/src/main/kotlin/tech/carcadex/kotlinbukkitkit/utility/types/Position.kt new file mode 100644 index 0000000..8b5ecbf --- /dev/null +++ b/utility/src/main/kotlin/tech/carcadex/kotlinbukkitkit/utility/types/Position.kt @@ -0,0 +1,87 @@ +package tech.carcadex.kotlinbukkitkit.utility.types + +import tech.carcadex.kotlinbukkitkit.utility.extensions.PosRange +import tech.carcadex.kotlinbukkitkit.utility.extensions.PosRangeIterator +import org.bukkit.Chunk +import org.bukkit.Location +import org.bukkit.World +import org.bukkit.block.Block +import kotlin.math.sqrt + +public fun blockPosOf(x: Int, y: Int, z: Int): BlockPos = BlockPos(x, y, z) +public fun locationPosOf(x: Double, y: Double, z: Double, yaw: Float = 0f, pitch: Float = 0f): LocationPos = LocationPos(x, y, z, yaw, pitch) +public fun chunkPosOf(x: Int, z: Int): ChunkPos = ChunkPos(x, z) + +public fun Location.asPos(): LocationPos = LocationPos(x, y, z, yaw, pitch) +public fun LocationPos.asBukkitBlock(world: World): Block = world.getBlockAt(x.toInt(), y.toInt(), z.toInt()) +public fun LocationPos.asBukkitLocation(world: World): Location = Location(world, x, y, z) +public fun LocationPos.asBlockPos(): BlockPos = BlockPos(x.toInt(), y.toInt(), z.toInt()) + +public fun Block.asPos(): BlockPos = BlockPos(x, y, z) +public fun Location.asBlockPos(): BlockPos = BlockPos(blockX, blockY, blockZ) +public fun BlockPos.asBukkitBlock(world: World): Block = world.getBlockAt(x, y, z) +public fun BlockPos.asBukkitLocation(world: World?): Location = Location(world, x.toDouble(), y.toDouble(), z.toDouble()) +public fun BlockPos.asLocationPos(): LocationPos = LocationPos(x.toDouble(), y.toDouble(), z.toDouble(), 0f, 0f) +public fun BlockPos.asChunkPos(): ChunkPos = ChunkPos(x shr 4, z shr 4) + +public fun Chunk.asPos(): ChunkPos = ChunkPos(x, z) +public fun ChunkPos.asBukkitChunk(world: World): Chunk = world.getChunkAt(x, z) + +public data class BlockPos( + var x: Int, + var y: Int, + var z: Int, +) : VectorComparable { + override fun axis(): DoubleArray = doubleArrayOf(x.toDouble(), y.toDouble(), z.toDouble()) + override fun factor(axis: IntArray): BlockPos = BlockPos(axis[0], axis[1], axis[2]) +} + +public data class LocationPos( + var x: Double, + var y: Double, + var z: Double, + val yaw: Float, + val pitch: Float, +) : VectorComparable { + override fun axis(): DoubleArray = doubleArrayOf(x, y, z) + override fun factor(axis: IntArray): LocationPos = LocationPos(axis[0].toDouble(), axis[1].toDouble(), axis[2].toDouble(), yaw, pitch) +} + +public data class ChunkPos( + var x: Int, + var z: Int, +) : VectorComparable { + override fun axis(): DoubleArray = doubleArrayOf(x.toDouble(), z.toDouble()) + override fun factor(axis: IntArray): ChunkPos = ChunkPos(axis[0], axis[1]) +} + +public interface VectorComparable> : Comparable { + public fun axis(): DoubleArray + public fun factor(axis: IntArray): T + + public operator fun rangeTo(other: T): PosRange { + return PosRange(this as T, other) { PosRangeIterator(this, other, ::factor) } + } + + override fun compareTo(other: T): Int { + val selfAxis = axis() + val otherAxis = other.axis() + val pairAxis = selfAxis.mapIndexed { index, axis -> axis to otherAxis[index] } + val (d1, d2) = calculatePythagoras(*pairAxis.toTypedArray()) + return d1.compareTo(d2) + } +} + +private fun Pair.toDouble() = first.toDouble() to second.toDouble() +private fun Pair.toInt() = first.toInt() to second.toInt() +private fun calculatePythagoras(vararg positions: Pair): Pair { + val pow = positions.map { (x1, x2) -> (x1 * x1) to (x2 * x2) } + + val x1Sum = pow.sumOf { (x, _) -> x } + val x2Sum = pow.sumOf { (_, x) -> x } + + val d1 = sqrt(x1Sum) + val d2 = sqrt(x2Sum) + + return d1 to d2 +} diff --git a/utility/src/main/kotlin/tech/carcadex/kotlinbukkitkit/utility/types/TimeFormat.kt b/utility/src/main/kotlin/tech/carcadex/kotlinbukkitkit/utility/types/TimeFormat.kt new file mode 100644 index 0000000..a90e692 --- /dev/null +++ b/utility/src/main/kotlin/tech/carcadex/kotlinbukkitkit/utility/types/TimeFormat.kt @@ -0,0 +1,114 @@ +package tech.carcadex.kotlinbukkitkit.utility.types + +import kotlinx.serialization.Serializable + +public const val DEFAULT_SPACER: Char = ' ' +public const val DEFAULT_FORMAT_STYLE: String = "%YEAR %MONTH %WEEK %DAY %HOUR %MIN %SEC" + +public val Long.formatter: TimeFormat get() = TimeFormat(this) +public val Int.formatter: TimeFormat get() = TimeFormat(this.toLong()) + +public val TimeFormat.PTBR: String get() = format(PTBRFormat) +public val TimeFormat.EN: String get() = format(ENFormat) + +@Serializable +public data class FormatLang( + val second: String, + val seconds: String, + val minute: String, + val minutes: String, + val hour: String, + val hours: String, + val day: String, + val days: String, + val week: String, + val weeks: String, + val month: String, + val months: String, + val year: String, + val years: String, +) + +/** + * @param time in second + */ +@JvmInline +public value class TimeFormat(private val time: Long) { + public fun format( + lang: FormatLang, + formatStyle: String = DEFAULT_FORMAT_STYLE, + formatSpacer: Char = DEFAULT_SPACER, + ): String { + val seconds = time % 60 + val minutes = time / 60 % 60 + val hours = time / 3600 % 24 + val days = time / 86400 % 7 + val weeks = time / 604800 % 4 + val months = time / 2419200 % 12 + val years = time / 29030400 + + var formated = formatStyle.replace("%SEC", if (seconds > 0) "$seconds ${if (seconds > 1) lang.seconds else lang.second}" else "", true) + + formated = formated.replace("%MIN", if (minutes > 0) "$minutes ${if (minutes > 1) lang.minutes else lang.minute}" else "", true) + + formated = formated.replace("%HOUR", if (hours > 0) "$hours ${if (hours > 1) lang.hours else lang.hour}" else "", true) + + formated = formated.replace("%DAY", if (days > 0) "$days ${if (days > 1) lang.days else lang.day}" else "", true) + + formated = formated.replace("%WEEK", if (weeks > 0) "$weeks ${if (weeks > 1) lang.weeks else lang.week}" else "", true) + + formated = formated.replace("%MONTH", if (months > 0) "$months ${if (months > 1) lang.months else lang.month}" else "", true) + + formated = formated.replace("%YEAR", if (years > 0) "$years ${if (years > 1) lang.years else lang.year}" else "", true) + + formated = formated.replace("$formatSpacer$formatSpacer", "$formatSpacer") + + return formated.trim { it == formatSpacer } + } +} + +public val PTBRFormat: FormatLang = FormatLang( + second = "segundo", + seconds = "segundos", + + minute = "minuto", + minutes = "minutos", + + hour = "hora", + hours = "horas", + + day = "dia", + days = "dias", + + week = "semana", + weeks = "semanas", + + month = "mes", + months = "meses", + + year = "ano", + years = "anos", +) + +public val ENFormat: FormatLang = FormatLang( + second = "second", + seconds = "seconds", + + minute = "minute", + minutes = "minutes", + + hour = "hour", + hours = "hours", + + day = "day", + days = "days", + + week = "week", + weeks = "weeks", + + month = "month", + months = "months", + + year = "year", + years = "years", +) diff --git a/utility/src/main/kotlin/tech/carcadex/kotlinbukkitkit/utility/utils/ComparatorUtils.kt b/utility/src/main/kotlin/tech/carcadex/kotlinbukkitkit/utility/utils/ComparatorUtils.kt new file mode 100644 index 0000000..d921361 --- /dev/null +++ b/utility/src/main/kotlin/tech/carcadex/kotlinbukkitkit/utility/utils/ComparatorUtils.kt @@ -0,0 +1,25 @@ +package tech.carcadex.kotlinbukkitkit.utility.utils + +import org.bukkit.entity.Player +import org.bukkit.plugin.Plugin +import kotlin.reflect.KClass + +public class HashcodeComparator : Comparator { + override fun compare(p0: T, p1: T): Int { + return p0.hashCode().compareTo(p1.hashCode()) + } +} + +public object PluginComparator : Comparator { + override fun compare(p0: Plugin, p1: Plugin): Int { + return p0.name.compareTo(p1.name) + } +} + +public object PlayerComparator : Comparator { + override fun compare(p0: Player, p1: Player): Int { + return p0.uniqueId.compareTo(p1.uniqueId) + } +} + +public val KClassComparator: HashcodeComparator> = HashcodeComparator>()