├── src ├── main │ ├── kotlin │ │ └── me │ │ │ └── ebonjaeger │ │ │ └── perworldinventory │ │ │ ├── permission │ │ │ ├── PermissionNode.kt │ │ │ └── PlayerPermission.kt │ │ │ ├── initialization │ │ │ ├── PluginFolder.kt │ │ │ ├── DataDirectory.kt │ │ │ ├── InjectorBuilder.kt │ │ │ └── Injector.kt │ │ │ ├── serialization │ │ │ ├── EconomySerializer.kt │ │ │ ├── LocationSerializer.kt │ │ │ ├── StatSerializer.kt │ │ │ ├── PlayerSerializer.kt │ │ │ ├── InventoryHelper.kt │ │ │ ├── PotionSerializer.kt │ │ │ └── ItemSerializer.kt │ │ │ ├── UpdateTimeoutsTask.kt │ │ │ ├── data │ │ │ ├── ProfileFactory.kt │ │ │ ├── DataSourceProvider.kt │ │ │ ├── PlayerDefaults.kt │ │ │ ├── ProfileKey.kt │ │ │ ├── DataSource.kt │ │ │ ├── migration │ │ │ │ ├── MigrationService.kt │ │ │ │ └── MigrationTask.kt │ │ │ ├── PlayerProperty.kt │ │ │ ├── FlatFile.kt │ │ │ └── PlayerProfile.kt │ │ │ ├── LogLevel.kt │ │ │ ├── listener │ │ │ ├── player │ │ │ │ ├── InventoryCreativeListener.kt │ │ │ │ ├── PlayerQuitListener.kt │ │ │ │ ├── PlayerDeathListener.kt │ │ │ │ ├── PlayerTeleportListener.kt │ │ │ │ ├── PlayerRespawnListener.kt │ │ │ │ ├── PlayerSpawnLocationListener.kt │ │ │ │ ├── PlayerGameModeChangeListener.kt │ │ │ │ └── PlayerChangedWorldListener.kt │ │ │ └── entity │ │ │ │ └── EntityPortalEventListener.kt │ │ │ ├── configuration │ │ │ ├── MetricsSettings.kt │ │ │ ├── PwiMigrationService.kt │ │ │ ├── Settings.kt │ │ │ ├── PluginSettings.kt │ │ │ └── PlayerSettings.kt │ │ │ ├── event │ │ │ ├── InventoryLoadCompleteEvent.kt │ │ │ └── InventoryLoadEvent.kt │ │ │ ├── command │ │ │ ├── MigrateCommand.kt │ │ │ ├── ReloadCommand.kt │ │ │ ├── HelpCommand.kt │ │ │ └── ConvertCommand.kt │ │ │ ├── service │ │ │ ├── BukkitService.kt │ │ │ └── EconomyService.kt │ │ │ ├── api │ │ │ └── PerWorldInventoryAPI.kt │ │ │ ├── Utils.kt │ │ │ ├── ConsoleLogger.kt │ │ │ ├── Group.kt │ │ │ ├── conversion │ │ │ └── ConvertService.kt │ │ │ ├── GroupManager.kt │ │ │ └── PerWorldInventory.kt │ └── resources │ │ ├── worlds.yml │ │ ├── plugin.yml │ │ └── config.yml └── test │ └── kotlin │ └── me │ └── ebonjaeger │ └── perworldinventory │ ├── GroupTest.kt │ ├── listener │ └── ListenerConsistencyTest.kt │ ├── ReflectionUtils.kt │ ├── configuration │ └── SettingsConsistencyTest.kt │ ├── serialization │ ├── PotionSerializerTest.kt │ ├── LocationSerializerTest.kt │ ├── ItemSerializerTest.kt │ ├── ItemMetaImpl.kt │ └── PlayerSerializerTest.kt │ ├── UtilsTest.kt │ ├── event │ └── EventConsistencyTest.kt │ ├── TestHelper.kt │ ├── GroupManagerTest.kt │ ├── service │ └── EconomyServiceTest.kt │ └── command │ └── GroupCommandsTest.kt ├── .codeclimate.yml ├── .github └── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── LICENSE ├── .circleci └── config.yml ├── README.md └── .gitignore /src/main/kotlin/me/ebonjaeger/perworldinventory/permission/PermissionNode.kt: -------------------------------------------------------------------------------- 1 | package me.ebonjaeger.perworldinventory.permission 2 | 3 | interface PermissionNode 4 | { 5 | 6 | fun getNode(): String 7 | } 8 | -------------------------------------------------------------------------------- /src/main/kotlin/me/ebonjaeger/perworldinventory/initialization/PluginFolder.kt: -------------------------------------------------------------------------------- 1 | package me.ebonjaeger.perworldinventory.initialization 2 | 3 | /** 4 | * Annotation used to identify the plugin's data folder for injection. 5 | */ 6 | @Target(AnnotationTarget.FIELD, AnnotationTarget.VALUE_PARAMETER) 7 | @Retention(AnnotationRetention.RUNTIME) 8 | annotation class PluginFolder -------------------------------------------------------------------------------- /src/main/kotlin/me/ebonjaeger/perworldinventory/initialization/DataDirectory.kt: -------------------------------------------------------------------------------- 1 | package me.ebonjaeger.perworldinventory.initialization 2 | 3 | /** 4 | * Annotation used to identify the data directory for injection. Not to confuse with 5 | * [PluginFolder], which Bukkit refers to as "data folder." 6 | */ 7 | @Target(AnnotationTarget.FIELD, AnnotationTarget.VALUE_PARAMETER) 8 | @Retention(AnnotationRetention.RUNTIME) 9 | annotation class DataDirectory -------------------------------------------------------------------------------- /src/main/kotlin/me/ebonjaeger/perworldinventory/permission/PlayerPermission.kt: -------------------------------------------------------------------------------- 1 | package me.ebonjaeger.perworldinventory.permission 2 | 3 | enum class PlayerPermission (private val node: String) : PermissionNode 4 | { 5 | 6 | BYPASS_WORLDS("perworldinventory.bypass.world"), 7 | 8 | BYPASS_GAMEMODE("perworldinventory.bypass.gamemode"), 9 | 10 | BYPASS_ENFORCE_GAMEMODE("perworldinventory.bypass.enforcegamemode"); 11 | 12 | override fun getNode(): String 13 | { 14 | return node 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | version: '2' # required to adjust maintainability checks 2 | 3 | checks: 4 | file-lines: 5 | enabled: false 6 | method-lines: 7 | enabled: false 8 | 9 | exclude_patterns: 10 | # Exclude settings objects 11 | - 'src/main/kotlin/me/ebonjaeger/perworldinventory/configuration/*Settings.kt' 12 | # Exclude player profile class since it just complains about the .equals function 13 | - 'src/main/kotlin/me/ebonjaeger/perworldinventory/data/PlayerProfile.kt' 14 | # Don't check test classes 15 | - 'src/test/kotlin/**/*Test.java' -------------------------------------------------------------------------------- /src/main/kotlin/me/ebonjaeger/perworldinventory/initialization/InjectorBuilder.kt: -------------------------------------------------------------------------------- 1 | package me.ebonjaeger.perworldinventory.initialization 2 | 3 | /** 4 | * Kotlin wrapper for [ch.jalu.injector.InjectorBuilder], creating a Kotlin-type [Injector]. 5 | */ 6 | class InjectorBuilder { 7 | 8 | val jaluInjectorBuilder: ch.jalu.injector.InjectorBuilder = ch.jalu.injector.InjectorBuilder() 9 | 10 | fun addDefaultHandlers(rootPackage: String): InjectorBuilder { 11 | jaluInjectorBuilder.addDefaultHandlers(rootPackage) 12 | return this 13 | } 14 | 15 | fun create(): Injector { 16 | return Injector(jaluInjectorBuilder.create()) 17 | } 18 | } -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: 'Type: Enhancement' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context about the feature request here. 21 | -------------------------------------------------------------------------------- /src/main/kotlin/me/ebonjaeger/perworldinventory/serialization/EconomySerializer.kt: -------------------------------------------------------------------------------- 1 | package me.ebonjaeger.perworldinventory.serialization 2 | 3 | import me.ebonjaeger.perworldinventory.data.PlayerProfile 4 | import net.minidev.json.JSONObject 5 | import org.bukkit.util.NumberConversions 6 | 7 | object EconomySerializer 8 | { 9 | 10 | /** 11 | * Get a player's currency amount. 12 | * 13 | * @param data The JsonObject with the balance data 14 | */ 15 | fun deserialize(data: JSONObject): Double 16 | { 17 | if (data.containsKey("balance")) 18 | { 19 | return NumberConversions.toDouble(data["balance"]) 20 | } 21 | 22 | return 0.0 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/kotlin/me/ebonjaeger/perworldinventory/UpdateTimeoutsTask.kt: -------------------------------------------------------------------------------- 1 | package me.ebonjaeger.perworldinventory 2 | 3 | class UpdateTimeoutsTask(private val plugin: PerWorldInventory) : Runnable 4 | { 5 | 6 | override fun run() 7 | { 8 | if (plugin.timeouts.isEmpty()) 9 | { 10 | return 11 | } 12 | 13 | val iter = plugin.timeouts.entries.iterator() 14 | while (iter.hasNext()) 15 | { 16 | val e = iter.next() 17 | val value = e.value - 1 18 | if (value > 0) 19 | { 20 | e.setValue(value) 21 | } else 22 | { 23 | iter.remove() 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/resources/worlds.yml: -------------------------------------------------------------------------------- 1 | # In this file you define your groups and the worlds in them. 2 | # Follow this format: 3 | # groups: 4 | # default: 5 | # worlds: 6 | # - world 7 | # - world_nether 8 | # - world_the_end 9 | # default-gamemode: SURVIVAL 10 | # creative: 11 | # worlds: 12 | # - creative 13 | # default-gamemode: CREATIVE 14 | # 15 | # 'default' and 'creative' are the names of the groups 16 | # worlds: is a list of all worlds in the group 17 | # If you have 'manage-gamemodes' set to true in the main config, the server 18 | # will use the 'default-gamemode' here to know what gamemode to put users in. 19 | groups: 20 | default: 21 | worlds: 22 | - world 23 | - world_nether 24 | - world_the_end 25 | default-gamemode: SURVIVAL 26 | respawnWorld: world 27 | -------------------------------------------------------------------------------- /src/main/kotlin/me/ebonjaeger/perworldinventory/data/ProfileFactory.kt: -------------------------------------------------------------------------------- 1 | package me.ebonjaeger.perworldinventory.data 2 | 3 | import me.ebonjaeger.perworldinventory.service.BukkitService 4 | import me.ebonjaeger.perworldinventory.service.EconomyService 5 | import org.bukkit.entity.Player 6 | import javax.inject.Inject 7 | 8 | /** 9 | * Factory for creating PlayerProfile objects. 10 | * 11 | * @param bukkitService [BukkitService] instance 12 | * @param economyService [EconomyService] instance 13 | */ 14 | class ProfileFactory @Inject constructor(private val bukkitService: BukkitService, 15 | private val economyService: EconomyService) 16 | { 17 | 18 | fun create(player: Player): PlayerProfile 19 | { 20 | val balance = economyService.getBalance(player) 21 | return PlayerProfile(player, balance) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/kotlin/me/ebonjaeger/perworldinventory/LogLevel.kt: -------------------------------------------------------------------------------- 1 | package me.ebonjaeger.perworldinventory 2 | 3 | /** 4 | * Log level. 5 | * 6 | * @param value the log level; the higher the number the more "important" the level. 7 | * A log level enables its number and all above. 8 | */ 9 | enum class LogLevel(private val value: Int) 10 | { 11 | 12 | /** Info: General messages. */ 13 | INFO(3), 14 | 15 | /** Fine: More detailed messages that may still be interesting to plugin users. */ 16 | FINE(2), 17 | 18 | /** Debug: Very detailed messages for debugging. */ 19 | DEBUG(1); 20 | 21 | /** 22 | * Return whether the current log level includes the given log level. 23 | * 24 | * @param level the level to process 25 | * @return true if the level is enabled, false otherwise 26 | */ 27 | fun includes(level: LogLevel): Boolean 28 | = value <= level.value 29 | } 30 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve. Failure to fill out the template could 4 | result in the issue being closed. 5 | title: '' 6 | labels: '' 7 | assignees: '' 8 | 9 | --- 10 | 11 | **Describe the bug** 12 | A clear and concise description of what the bug is. Errors and logs go down below, not here! 13 | 14 | **To Reproduce** 15 | Steps to reproduce the behavior: 16 | 1. Do '...' 17 | 2. Go '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Plugin version** 24 | - [ ] Using the latest released version of the plugin 25 | - [ ] Using the `legacy` jar 26 | 27 | **Server version** 28 | The Minecraft version your server is running. Bonus points for the full version including the hash. 29 | 30 | **Additional context and logs** 31 | Add any other context about the problem here. Also post error messages and logs (use a service like Hastebin for logs, please!) in this area. 32 | -------------------------------------------------------------------------------- /src/main/kotlin/me/ebonjaeger/perworldinventory/data/DataSourceProvider.kt: -------------------------------------------------------------------------------- 1 | package me.ebonjaeger.perworldinventory.data 2 | 3 | import ch.jalu.injector.factory.SingletonStore 4 | import me.ebonjaeger.perworldinventory.ConsoleLogger 5 | import javax.inject.Inject 6 | import javax.inject.Provider 7 | 8 | class DataSourceProvider @Inject constructor (private val dataSourceStore: SingletonStore) : Provider 9 | { 10 | 11 | override fun get(): DataSource 12 | { 13 | try 14 | { 15 | return createDataSource() 16 | } catch (ex: Exception) 17 | { 18 | ConsoleLogger.severe("Unable to create data source:", ex) 19 | throw IllegalStateException("Error during initialization of data source", ex) 20 | } 21 | } 22 | 23 | private fun createDataSource(): DataSource 24 | { 25 | // Later on we will have logic here to differentiate between flatfile and MySQL. 26 | return dataSourceStore.getSingleton(FlatFile::class.java) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/kotlin/me/ebonjaeger/perworldinventory/listener/player/InventoryCreativeListener.kt: -------------------------------------------------------------------------------- 1 | package me.ebonjaeger.perworldinventory.listener.player 2 | 3 | import me.ebonjaeger.perworldinventory.PerWorldInventory 4 | import org.bukkit.entity.Player 5 | import org.bukkit.event.Event 6 | import org.bukkit.event.EventHandler 7 | import org.bukkit.event.EventPriority 8 | import org.bukkit.event.Listener 9 | import org.bukkit.event.inventory.InventoryCreativeEvent 10 | import javax.inject.Inject 11 | 12 | class InventoryCreativeListener @Inject constructor(private val plugin: PerWorldInventory) : Listener 13 | { 14 | 15 | @EventHandler(priority = EventPriority.LOWEST) 16 | fun onCreativeSlotChange(event: InventoryCreativeEvent) 17 | { 18 | if (!plugin.timeouts.isEmpty()) 19 | { 20 | return 21 | } 22 | 23 | val holder = event.inventory.holder 24 | if (holder is Player && plugin.timeouts.containsKey(holder.uniqueId)) 25 | { 26 | event.result = Event.Result.DENY 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 Evan Maddock 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /src/main/kotlin/me/ebonjaeger/perworldinventory/configuration/MetricsSettings.kt: -------------------------------------------------------------------------------- 1 | package me.ebonjaeger.perworldinventory.configuration 2 | 3 | import ch.jalu.configme.Comment 4 | import ch.jalu.configme.SettingsHolder 5 | import ch.jalu.configme.properties.Property 6 | import ch.jalu.configme.properties.PropertyInitializer.newProperty 7 | 8 | /** 9 | * Object to hold settings for sending plugin metrics. 10 | */ 11 | object MetricsSettings : SettingsHolder 12 | { 13 | 14 | @JvmField 15 | @Comment( 16 | "Choose whether or not to enable metrics sending.", 17 | "See https://bstats.org/getting-started for details." 18 | ) 19 | val ENABLE_METRICS: Property = newProperty("metrics.enable", true) 20 | 21 | @JvmField 22 | @Comment( 23 | "Send the number of configured groups.", 24 | "No group names will be sent!" 25 | ) 26 | val SEND_NUM_GROUPS: Property? = newProperty("metrics.send-number-of-groups", true) 27 | 28 | @JvmField 29 | @Comment("Send the total number of worlds on the server.") 30 | val SEND_NUM_WORLDS: Property? = newProperty("metrics.send-number-of-worlds", true) 31 | } 32 | -------------------------------------------------------------------------------- /src/main/kotlin/me/ebonjaeger/perworldinventory/data/PlayerDefaults.kt: -------------------------------------------------------------------------------- 1 | package me.ebonjaeger.perworldinventory.data 2 | 3 | /** 4 | * A collection of default stats for a Player. 5 | */ 6 | object PlayerDefaults 7 | { 8 | 9 | /** 10 | * Default health value. 11 | */ 12 | val HEALTH = 20.0 13 | 14 | /** 15 | * Default experience value. 16 | */ 17 | val EXPERIENCE = 0F 18 | 19 | /** 20 | * Default level value. 21 | */ 22 | val LEVEL = 0 23 | 24 | /** 25 | * Default food level value. 26 | */ 27 | val FOOD_LEVEL = 20 28 | 29 | /** 30 | * Default exhaustion value. 31 | */ 32 | val EXHAUSTION = 0.0F 33 | 34 | /** 35 | * Default saturation value. 36 | */ 37 | val SATURATION = 5F 38 | 39 | /** 40 | * Default fall distance value. 41 | */ 42 | val FALL_DISTANCE = 0F 43 | 44 | /** 45 | * Default fire ticks value. 46 | */ 47 | val FIRE_TICKS = 0 48 | 49 | /** 50 | * Default remaining air value. 51 | */ 52 | val REMAINING_AIR = 300 53 | 54 | /** 55 | * Default maximum air value. 56 | */ 57 | val MAXIMUM_AIR = 300 58 | } 59 | -------------------------------------------------------------------------------- /src/main/kotlin/me/ebonjaeger/perworldinventory/data/ProfileKey.kt: -------------------------------------------------------------------------------- 1 | package me.ebonjaeger.perworldinventory.data 2 | 3 | import me.ebonjaeger.perworldinventory.Group 4 | import org.bukkit.GameMode 5 | import java.util.* 6 | 7 | class ProfileKey(val uuid: UUID, 8 | val group: Group, 9 | val gameMode: GameMode) 10 | { 11 | 12 | override fun equals(other: Any?): Boolean 13 | { 14 | if (this === other) return true 15 | if (other !is ProfileKey) return false 16 | 17 | return Objects.equals(uuid, other.uuid) && 18 | Objects.equals(group, other.group) && 19 | Objects.equals(gameMode, other.gameMode) 20 | } 21 | 22 | override fun hashCode(): Int 23 | { 24 | var result = uuid.hashCode() 25 | result = 31 * result + group.hashCode() 26 | result = 31 * result + gameMode.hashCode() 27 | return result 28 | } 29 | 30 | override fun toString(): String 31 | { 32 | return "ProfileKey{" + 33 | "uuid='$uuid'" + 34 | ", group='${group.name}'" + 35 | ", gameMode='${gameMode.toString().toLowerCase()}'" + 36 | "}" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/kotlin/me/ebonjaeger/perworldinventory/event/InventoryLoadCompleteEvent.kt: -------------------------------------------------------------------------------- 1 | package me.ebonjaeger.perworldinventory.event 2 | 3 | import me.ebonjaeger.perworldinventory.Group 4 | import org.bukkit.GameMode 5 | import org.bukkit.entity.Player 6 | import org.bukkit.event.Event 7 | import org.bukkit.event.HandlerList 8 | 9 | /** 10 | * Event fired after inventory data has been applied to a player. 11 | * If the plugin is not configured to separate inventories by GameMode, 12 | * then [GameMode.SURVIVAL] will be returned by this event. 13 | * 14 | * @property player The [Player] that was effected. 15 | * @property group The [Group] the data was loaded for. 16 | * @property gameMode The [GameMode] the used to find the data. 17 | */ 18 | class InventoryLoadCompleteEvent(val player: Player, 19 | val group: Group, 20 | val gameMode: GameMode) : Event() 21 | { 22 | 23 | companion object { 24 | private val HANDLERS = HandlerList() 25 | 26 | @JvmStatic 27 | fun getHandlerList() = HANDLERS 28 | } 29 | 30 | override fun getHandlers(): HandlerList 31 | { 32 | return InventoryLoadCompleteEvent.HANDLERS 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/kotlin/me/ebonjaeger/perworldinventory/command/MigrateCommand.kt: -------------------------------------------------------------------------------- 1 | package me.ebonjaeger.perworldinventory.command 2 | 3 | import co.aikar.commands.BaseCommand 4 | import co.aikar.commands.annotation.CommandAlias 5 | import co.aikar.commands.annotation.CommandPermission 6 | import co.aikar.commands.annotation.Description 7 | import co.aikar.commands.annotation.Subcommand 8 | import me.ebonjaeger.perworldinventory.data.migration.MigrationService 9 | import org.bukkit.ChatColor 10 | import org.bukkit.command.CommandSender 11 | import javax.inject.Inject 12 | 13 | @CommandAlias("perworldinventory|pwi") 14 | class MigrateCommand @Inject constructor(private val migrationService: MigrationService) : BaseCommand() { 15 | 16 | @Subcommand("migrate") 17 | @CommandPermission("perworldinventory.command.migrate") 18 | @Description("Migrate old data to the latest data format") 19 | fun onMigrate(sender: CommandSender) { 20 | if (migrationService.isMigrating()) { 21 | sender.sendMessage("${ChatColor.DARK_RED}» ${ChatColor.GRAY}A data migration is already in progress!") 22 | return 23 | } 24 | 25 | sender.sendMessage("${ChatColor.BLUE}» ${ChatColor.GRAY}Beginning data migration to new format!") 26 | migrationService.beginMigration(sender) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/test/kotlin/me/ebonjaeger/perworldinventory/GroupTest.kt: -------------------------------------------------------------------------------- 1 | package me.ebonjaeger.perworldinventory 2 | 3 | import com.natpryce.hamkrest.assertion.assertThat 4 | import com.natpryce.hamkrest.equalTo 5 | import org.bukkit.GameMode 6 | import org.junit.jupiter.api.Test 7 | 8 | /** 9 | * Test for [Group]. 10 | */ 11 | class GroupTest { 12 | 13 | @Test 14 | fun shouldCreateNewGroup() { 15 | // given 16 | val group = Group("test", mutableSetOf("world1", "world2"), GameMode.SURVIVAL, null) 17 | 18 | // when / then 19 | assertThat(group.containsWorld("world2"), equalTo(true)) 20 | assertThat(group.containsWorld("other"), equalTo(false)) 21 | } 22 | 23 | @Test 24 | fun shouldAddNewWorlds() { 25 | // given 26 | val group = Group("my_group", mutableSetOf("world1"), GameMode.ADVENTURE, null) 27 | 28 | // when 29 | group.addWorld("other") 30 | group.addWorlds(setOf("one", "two")) 31 | 32 | // then 33 | val existingWorlds = setOf("world1", "other", "one", "two") 34 | existingWorlds.forEach { world -> 35 | assertThat("World $world should be included", group.containsWorld(world), equalTo(true)) 36 | } 37 | assertThat(group.containsWorld("bogus"), equalTo(false)) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/test/kotlin/me/ebonjaeger/perworldinventory/listener/ListenerConsistencyTest.kt: -------------------------------------------------------------------------------- 1 | package me.ebonjaeger.perworldinventory.listener 2 | 3 | import me.ebonjaeger.perworldinventory.TestHelper 4 | import org.bukkit.event.EventHandler 5 | import org.bukkit.event.Listener 6 | import org.junit.jupiter.api.Assertions.fail 7 | import org.junit.jupiter.api.Test 8 | import org.reflections.Reflections 9 | import org.reflections.scanners.SubTypesScanner 10 | 11 | /** 12 | * Consistency test for [Listener] implementations. 13 | */ 14 | class ListenerConsistencyTest { 15 | 16 | @Test 17 | fun shouldOnlyHaveEventHandlerMethods() { 18 | // given 19 | val reflections = Reflections(TestHelper.PROJECT_PACKAGE, SubTypesScanner()) 20 | val listeners = reflections.getSubTypesOf(Listener::class.java) 21 | if (listeners.isEmpty()) { 22 | throw IllegalStateException("Did not find any Listener implementations. Is the package correct?") 23 | } 24 | 25 | // when / then 26 | for (listener in listeners) { 27 | listener.methods 28 | .filter { it.declaringClass != Object::class.java } 29 | .filter { !it.isAnnotationPresent(EventHandler::class.java) } 30 | .forEach { 31 | fail("Expected method " + it.declaringClass.simpleName + "#" + it.name 32 | + " to be annotated with @EventHandler") 33 | } 34 | } 35 | } 36 | 37 | } -------------------------------------------------------------------------------- /src/main/kotlin/me/ebonjaeger/perworldinventory/listener/player/PlayerQuitListener.kt: -------------------------------------------------------------------------------- 1 | package me.ebonjaeger.perworldinventory.listener.player 2 | 3 | import me.ebonjaeger.perworldinventory.PerWorldInventory 4 | import me.ebonjaeger.perworldinventory.configuration.PluginSettings 5 | import me.ebonjaeger.perworldinventory.configuration.Settings 6 | import me.ebonjaeger.perworldinventory.data.DataSource 7 | import org.bukkit.event.EventHandler 8 | import org.bukkit.event.EventPriority 9 | import org.bukkit.event.Listener 10 | import org.bukkit.event.player.PlayerKickEvent 11 | import org.bukkit.event.player.PlayerQuitEvent 12 | import javax.inject.Inject 13 | 14 | class PlayerQuitListener @Inject constructor(private val plugin: PerWorldInventory, 15 | private val dataSource: DataSource, 16 | private val settings: Settings) : Listener { 17 | 18 | @EventHandler(priority = EventPriority.MONITOR) 19 | fun onPlayerQuit(event: PlayerQuitEvent) { 20 | plugin.timeouts.remove(event.player.uniqueId) 21 | if (settings.getProperty(PluginSettings.LOAD_DATA_ON_JOIN)) { 22 | dataSource.saveLogout(event.player) 23 | } 24 | } 25 | 26 | @EventHandler(priority = EventPriority.MONITOR) 27 | fun onPlayerKick(event: PlayerKickEvent) { 28 | plugin.timeouts.remove(event.player.uniqueId) 29 | if (settings.getProperty(PluginSettings.LOAD_DATA_ON_JOIN)) { 30 | dataSource.saveLogout(event.player) 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/kotlin/me/ebonjaeger/perworldinventory/command/ReloadCommand.kt: -------------------------------------------------------------------------------- 1 | package me.ebonjaeger.perworldinventory.command 2 | 3 | import co.aikar.commands.BaseCommand 4 | import co.aikar.commands.annotation.CommandAlias 5 | import co.aikar.commands.annotation.CommandPermission 6 | import co.aikar.commands.annotation.Description 7 | import co.aikar.commands.annotation.Subcommand 8 | import me.ebonjaeger.perworldinventory.ConsoleLogger 9 | import me.ebonjaeger.perworldinventory.GroupManager 10 | import me.ebonjaeger.perworldinventory.configuration.PluginSettings 11 | import me.ebonjaeger.perworldinventory.configuration.Settings 12 | import me.ebonjaeger.perworldinventory.data.ProfileManager 13 | import org.bukkit.ChatColor 14 | import org.bukkit.command.CommandSender 15 | import javax.inject.Inject 16 | 17 | @CommandAlias("perworldinventory|pwi") 18 | class ReloadCommand @Inject constructor(private val groupManager: GroupManager, 19 | private val profileManager: ProfileManager, 20 | private val settings: Settings) : BaseCommand() 21 | { 22 | @Subcommand("reload") 23 | @CommandPermission("perworldinventory.reload") 24 | @Description("Reload Configurations") 25 | fun onReload(sender: CommandSender) 26 | { 27 | settings.reload() 28 | ConsoleLogger.setLogLevel(settings.getProperty(PluginSettings.LOGGING_LEVEL)) 29 | groupManager.loadGroups() 30 | profileManager.invalidateCache() 31 | 32 | sender.sendMessage("${ChatColor.BLUE}» ${ChatColor.GRAY}Configuration files reloaded!") 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/kotlin/me/ebonjaeger/perworldinventory/configuration/PwiMigrationService.kt: -------------------------------------------------------------------------------- 1 | package me.ebonjaeger.perworldinventory.configuration 2 | 3 | import ch.jalu.configme.configurationdata.ConfigurationData 4 | import ch.jalu.configme.migration.MigrationService 5 | import ch.jalu.configme.migration.PlainMigrationService 6 | import ch.jalu.configme.resource.PropertyReader 7 | import me.ebonjaeger.perworldinventory.LogLevel 8 | 9 | class PwiMigrationService : PlainMigrationService() 10 | { 11 | 12 | override fun performMigrations(reader: PropertyReader, configurationData: ConfigurationData): Boolean 13 | { 14 | return migrateDebugLevels(reader, configurationData) 15 | } 16 | 17 | /** 18 | * Migrate the old simple on-off debug mode to the new multi-leveled debug mode. 19 | * 20 | * @param reader The property reader 21 | * @param configurationData The configuration data 22 | * @return True if the configuration has changed, false otherwise 23 | */ 24 | private fun migrateDebugLevels(reader: PropertyReader, configurationData: ConfigurationData) : Boolean 25 | { 26 | val oldPath = "debug-mode" 27 | val newSetting = PluginSettings.LOGGING_LEVEL 28 | 29 | if (!newSetting!!.isPresent(reader) && reader.contains(oldPath)) 30 | { 31 | val oldValue = reader.getBoolean(oldPath) ?: return false 32 | val level = if (oldValue) LogLevel.FINE else LogLevel.INFO 33 | 34 | configurationData.setValue(newSetting, level) 35 | return MigrationService.MIGRATION_REQUIRED 36 | } 37 | 38 | return MigrationService.NO_MIGRATION_NEEDED 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Java Maven CircleCI 2.0 configuration file 2 | # 3 | # Check https://circleci.com/docs/2.0/language-java/ for more details 4 | # 5 | version: 2.1 6 | orbs: 7 | codecov: codecov/codecov@1.0.5 8 | jobs: 9 | build: 10 | docker: 11 | # specify the version you desire here 12 | - image: circleci/openjdk:11-jdk 13 | 14 | # Specify service dependencies here if necessary 15 | # CircleCI maintains a library of pre-built images 16 | # documented at https://circleci.com/docs/2.0/circleci-images/ 17 | # - image: circleci/postgres:9.4 18 | 19 | working_directory: ~/repo 20 | 21 | environment: 22 | # Customize the JVM maximum heap limit 23 | MAVEN_OPTS: -Xmx3200m 24 | 25 | steps: 26 | - checkout 27 | 28 | # Download and cache dependencies 29 | - restore_cache: 30 | keys: 31 | - v1-dependencies-{{ checksum "pom.xml" }} 32 | # fallback to using the latest cache if no exact match is found 33 | - v1-dependencies- 34 | 35 | - run: mvn dependency:go-offline 36 | 37 | - save_cache: 38 | paths: 39 | - ~/.m2 40 | key: v1-dependencies-{{ checksum "pom.xml" }} 41 | 42 | # run tests! 43 | - run: mvn package 44 | 45 | - store_test_results: # uploads the test metadata from the `target/surefire-reports` directory so that it can show up in the CircleCI dashboard. 46 | # Upload test results for display in Test Summary: https://circleci.com/docs/2.0/collect-test-data/ 47 | path: target/surefire-reports 48 | 49 | - codecov/upload: 50 | file: target/surefire-reports/*.xml 51 | -------------------------------------------------------------------------------- /src/test/kotlin/me/ebonjaeger/perworldinventory/ReflectionUtils.kt: -------------------------------------------------------------------------------- 1 | package me.ebonjaeger.perworldinventory 2 | 3 | import java.lang.String.format 4 | import java.lang.reflect.Field 5 | import kotlin.reflect.KClass 6 | 7 | object ReflectionUtils 8 | { 9 | 10 | /** 11 | * Set the field of a given object to a new value with reflection. 12 | * 13 | * @param clazz The class of the object. 14 | * @param instance The instance to modify (null for static fields). 15 | * @param fieldName The name of the field to modify. 16 | * @param value The value to give the field. 17 | */ 18 | fun setField(clazz: KClass, instance: T?, fieldName: String, value: Any) 19 | { 20 | try 21 | { 22 | val field = getField(clazz, instance, fieldName) 23 | field.set(instance, value) 24 | } catch (ex: UnsupportedOperationException) 25 | { 26 | throw UnsupportedOperationException(format("Could not set field '%s' for instance '%s' of class '%s'.", 27 | fieldName, instance, clazz.simpleName), ex) 28 | } 29 | } 30 | 31 | private fun getField(clazz: KClass, instance: T?, fieldName: String): Field 32 | { 33 | try 34 | { 35 | val field = clazz.java.getDeclaredField(fieldName) 36 | field.isAccessible = true 37 | return field 38 | } catch (ex: NoSuchFieldException) 39 | { 40 | throw UnsupportedOperationException(format("Could not get field '%s' for instance '%s' of class '%s'.", 41 | fieldName, instance, clazz.simpleName), ex) 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/kotlin/me/ebonjaeger/perworldinventory/listener/entity/EntityPortalEventListener.kt: -------------------------------------------------------------------------------- 1 | package me.ebonjaeger.perworldinventory.listener.entity 2 | 3 | import me.ebonjaeger.perworldinventory.ConsoleLogger 4 | import me.ebonjaeger.perworldinventory.GroupManager 5 | import org.bukkit.entity.Item 6 | import org.bukkit.event.EventHandler 7 | import org.bukkit.event.EventPriority 8 | import org.bukkit.event.Listener 9 | import org.bukkit.event.entity.EntityPortalEvent 10 | import javax.inject.Inject 11 | 12 | class EntityPortalEventListener @Inject constructor(private val groupManager: GroupManager) : Listener 13 | { 14 | 15 | @EventHandler (priority = EventPriority.NORMAL) 16 | fun onEntityPortalTeleport(event: EntityPortalEvent) 17 | { 18 | if (event.entity !is Item) 19 | return 20 | 21 | ConsoleLogger.fine("[EntityPortalEvent] A '${event.entity.name}' is going through a portal") 22 | 23 | val worldFrom = event.from.world 24 | val locationTo = event.to ?: return // A destination location is not guaranteed to exist 25 | val worldTo = locationTo.world 26 | 27 | if (worldFrom == null || worldTo == null) { 28 | ConsoleLogger.fine("[EntityPortalEvent] One of the worlds was null, returning") 29 | return 30 | } 31 | 32 | val from = groupManager.getGroupFromWorld(worldFrom.name) 33 | val to = groupManager.getGroupFromWorld(worldTo.name) 34 | 35 | // If the groups are different, cancel the event 36 | if (from != to) 37 | { 38 | ConsoleLogger.debug("[EntityPortalEvent] Group '${from.name}' and group '${to.name}' are different! Canceling event!") 39 | event.isCancelled = true 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/kotlin/me/ebonjaeger/perworldinventory/service/BukkitService.kt: -------------------------------------------------------------------------------- 1 | package me.ebonjaeger.perworldinventory.service 2 | 3 | import me.ebonjaeger.perworldinventory.PerWorldInventory 4 | import me.ebonjaeger.perworldinventory.Utils 5 | import org.bukkit.Bukkit 6 | import org.bukkit.OfflinePlayer 7 | import org.bukkit.scheduler.BukkitTask 8 | import javax.inject.Inject 9 | 10 | /** 11 | * Service for functions around the server the plugin is running on (scheduling tasks, etc.). 12 | * 13 | * @param plugin the plugin instance 14 | */ 15 | class BukkitService @Inject constructor(private val plugin: PerWorldInventory) 16 | { 17 | 18 | private val scheduler = plugin.server.scheduler 19 | 20 | fun isShuttingDown() = 21 | plugin.isShuttingDown 22 | 23 | fun getOfflinePlayers(): Array = 24 | Bukkit.getOfflinePlayers() 25 | 26 | fun runRepeatingTaskAsynchronously(task: Runnable, delay: Long, period: Long): BukkitTask = 27 | scheduler.runTaskTimerAsynchronously(plugin, task, delay, period) 28 | 29 | fun runTaskAsynchronously(task: () -> Unit): BukkitTask = 30 | scheduler.runTaskAsynchronously(plugin, task) 31 | 32 | /** 33 | * Run a task that may or may not be asynchronous depending on the 34 | * parameter passed to this function. 35 | * 36 | * @param task The task to run 37 | * @param async If the task should be run asynchronously 38 | */ 39 | fun runTaskOptionallyAsynchronously(task: () -> Unit, async: Boolean): BukkitTask = 40 | if (async) { scheduler.runTaskAsynchronously(plugin, task) } else { scheduler.runTask(plugin, task) } 41 | 42 | fun runTask(task: () -> Unit): BukkitTask = 43 | scheduler.runTask(plugin, task) 44 | } 45 | -------------------------------------------------------------------------------- /src/main/kotlin/me/ebonjaeger/perworldinventory/command/HelpCommand.kt: -------------------------------------------------------------------------------- 1 | package me.ebonjaeger.perworldinventory.command 2 | 3 | import co.aikar.commands.BaseCommand 4 | import co.aikar.commands.CommandHelp 5 | import co.aikar.commands.annotation.* 6 | import co.aikar.commands.annotation.HelpCommand 7 | import me.ebonjaeger.perworldinventory.PerWorldInventory 8 | import org.bukkit.ChatColor 9 | import org.bukkit.command.CommandSender 10 | 11 | @CommandAlias("perworldinventory|pwi") 12 | class HelpCommand (private val plugin: PerWorldInventory) : BaseCommand() 13 | { 14 | 15 | @HelpCommand 16 | fun onHelp(sender: CommandSender, help: CommandHelp) 17 | { 18 | sender.sendMessage("${ChatColor.DARK_GRAY}${ChatColor.STRIKETHROUGH}-----------------------------------------------------") 19 | sender.sendMessage("${ChatColor.DARK_GRAY} [ ${ChatColor.BLUE}PerWorldInventoryCommands ${ChatColor.DARK_GRAY}]") 20 | help.showHelp() 21 | sender.sendMessage("${ChatColor.DARK_GRAY}${ChatColor.STRIKETHROUGH}-----------------------------------------------------") 22 | } 23 | 24 | @Subcommand("version") 25 | @CommandPermission("perworldinventory.version") 26 | @Description("View the installed version of PerWorldInventory") 27 | fun onVersion(sender: CommandSender) 28 | { 29 | val version = plugin.description.version 30 | val authors = plugin.description.authors 31 | .toString() 32 | .replace("[", "") 33 | .replace("]", "") 34 | 35 | sender.sendMessage("${ChatColor.BLUE}» ${ChatColor.GRAY}Version: ${ChatColor.BLUE}$version") 36 | sender.sendMessage("${ChatColor.BLUE}» ${ChatColor.GRAY}Authors: ${ChatColor.BLUE}$authors") 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/kotlin/me/ebonjaeger/perworldinventory/listener/player/PlayerDeathListener.kt: -------------------------------------------------------------------------------- 1 | package me.ebonjaeger.perworldinventory.listener.player 2 | 3 | import me.ebonjaeger.perworldinventory.GroupManager 4 | import me.ebonjaeger.perworldinventory.data.PlayerDefaults 5 | import me.ebonjaeger.perworldinventory.data.ProfileManager 6 | import org.bukkit.event.EventHandler 7 | import org.bukkit.event.EventPriority 8 | import org.bukkit.event.Listener 9 | import org.bukkit.event.entity.PlayerDeathEvent 10 | import javax.inject.Inject 11 | 12 | /** 13 | * Listens for [PlayerDeathEvent]s to prevent inventory 14 | * duplication when the player re-enters the world. 15 | */ 16 | class PlayerDeathListener @Inject constructor(private val groupManager: GroupManager, 17 | private val profileManager: ProfileManager) : Listener 18 | { 19 | 20 | @EventHandler(priority = EventPriority.MONITOR) 21 | fun onPlayerDeath(event: PlayerDeathEvent) 22 | { 23 | val player = event.entity 24 | val location = player.location 25 | val group = groupManager.getGroupFromWorld(location.world!!.name) 26 | 27 | if (!event.keepInventory) 28 | { 29 | player.inventory.clear() 30 | player.totalExperience = event.newExp 31 | player.level = event.newLevel 32 | } 33 | 34 | player.foodLevel = PlayerDefaults.FOOD_LEVEL 35 | player.saturation = PlayerDefaults.SATURATION 36 | player.exhaustion = PlayerDefaults.EXHAUSTION 37 | player.fallDistance = PlayerDefaults.FALL_DISTANCE 38 | player.fireTicks = PlayerDefaults.FIRE_TICKS 39 | player.activePotionEffects.forEach { player.removePotionEffect(it.type) } 40 | 41 | profileManager.addPlayerProfile(player, group, player.gameMode) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/resources/plugin.yml: -------------------------------------------------------------------------------- 1 | main: maven-main 2 | version: maven-version 3 | name: maven-name 4 | author: maven-authors 5 | softdepend: [Vault] 6 | api-version: 1.13 7 | 8 | commands: 9 | pwi: 10 | description: Commands for PerWorldInventory 11 | aliases: [perworldinventory] 12 | 13 | permissions: 14 | perworldinventory.admin: 15 | default: op 16 | children: 17 | perworldinventory.command.convert: true 18 | perworldinventory.command.help: true 19 | perworldinventory.command.reload: true 20 | perworldinventory.command.version: true 21 | perworldinventory.command.groups.list: true 22 | perworldinventory.command.groups.info: true 23 | perworldinventory.command.groups.add: true 24 | perworldinventory.command.groups.modify: true 25 | perworldinventory.command.groups.remove: true 26 | perworldinventory.command.migrate: true 27 | perworldinventory.bypass.*: 28 | default: false 29 | children: 30 | perworldinventory.bypass.gamemode: true 31 | perworldinventory.bypass.world: true 32 | perworldinventory.command.convert: 33 | default: false 34 | perworldinventory.command.help: 35 | default: false 36 | perworldinventory.command.reload: 37 | default: false 38 | perworldinventory.command.version: 39 | default: false 40 | perworldinventory.command.groups.list: 41 | default: false 42 | perworldinventory.command.groups.info: 43 | default: false 44 | perworldinventory.command.groups.add: 45 | default: false 46 | perworldinventory.command.groups.modify: 47 | default: false 48 | perworldinventory.command.groups.remove: 49 | default: false 50 | perworldinventory.command.migrate: 51 | default: false 52 | perworldinventory.bypass.gamemode: 53 | default: false 54 | perworldinventory.bypass.world: 55 | default: false 56 | -------------------------------------------------------------------------------- /src/main/kotlin/me/ebonjaeger/perworldinventory/serialization/LocationSerializer.kt: -------------------------------------------------------------------------------- 1 | package me.ebonjaeger.perworldinventory.serialization 2 | 3 | import com.dumptruckman.bukkit.configuration.util.SerializationHelper 4 | import net.minidev.json.JSONObject 5 | import org.bukkit.Bukkit 6 | import org.bukkit.Location 7 | import org.bukkit.util.NumberConversions 8 | 9 | object LocationSerializer 10 | { 11 | 12 | /** 13 | * Serialize a [Location] into a JsonObject. 14 | * 15 | * @param location The location to serialize 16 | * @return A JsonObject with the world name and location properties 17 | */ 18 | @Suppress("UNCHECKED_CAST") // We know that #serialize will give us a Map for a ConfigurationSerializable object 19 | fun serialize(location: Location): JSONObject 20 | { 21 | val map = SerializationHelper.serialize(location) as Map 22 | return JSONObject(map) 23 | } 24 | 25 | /** 26 | * Deserialize a [Location] from a JsonObject. 27 | * 28 | * @param obj The JsonObject to deserialize 29 | * @return A new location from the properties of the JsonObject 30 | */ 31 | fun deserialize(obj: JSONObject): Location 32 | { 33 | return if (obj.containsKey("==")) { // Location was serialized using ConfigurationSerializable method 34 | SerializationHelper.deserialize(obj as Map) as Location 35 | } else { // Location was serialized by hand 36 | val world = Bukkit.getWorld(obj["world"] as String) 37 | val x = NumberConversions.toDouble(obj["x"]) 38 | val y = NumberConversions.toDouble(obj["y"]) 39 | val z = NumberConversions.toDouble(obj["z"]) 40 | val pitch = NumberConversions.toFloat(obj["pitch"]) 41 | val yaw = NumberConversions.toFloat(obj["yaw"]) 42 | 43 | Location(world, x, y, z, yaw, pitch) 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/kotlin/me/ebonjaeger/perworldinventory/event/InventoryLoadEvent.kt: -------------------------------------------------------------------------------- 1 | package me.ebonjaeger.perworldinventory.event 2 | 3 | import me.ebonjaeger.perworldinventory.Group 4 | import org.bukkit.GameMode 5 | import org.bukkit.entity.Player 6 | import org.bukkit.event.Cancellable 7 | import org.bukkit.event.Event 8 | import org.bukkit.event.HandlerList 9 | 10 | /** 11 | * The cause of this loading event. 12 | */ 13 | enum class Cause 14 | { 15 | /** 16 | * The player changed worlds. 17 | */ 18 | WORLD_CHANGE, 19 | 20 | /** 21 | * The player changed their GameMode. 22 | */ 23 | GAMEMODE_CHANGE 24 | } 25 | 26 | /** 27 | * Event called when a player's new inventory is about to be 28 | * loaded. If the event is cancelled, the inventory will not 29 | * be loaded. 30 | * 31 | * @property player The [Player] that caused this event. 32 | * @property cause The [Cause] of the event. 33 | * @property oldGameMode The player's old [GameMode]. 34 | * @property newGameMode The player's new [GameMode]. 35 | * @property group The [Group] that the player is going to. 36 | */ 37 | class InventoryLoadEvent(val player: Player, 38 | val cause: Cause, 39 | val oldGameMode: GameMode, 40 | val newGameMode: GameMode, 41 | val group: Group) : Event(), Cancellable 42 | { 43 | companion object { 44 | private val HANDLERS = HandlerList() 45 | 46 | @JvmStatic 47 | fun getHandlerList() = HANDLERS 48 | } 49 | 50 | private var isEventCancelled = false 51 | 52 | override fun getHandlers(): HandlerList 53 | { 54 | return HANDLERS 55 | } 56 | 57 | override fun isCancelled(): Boolean 58 | { 59 | return isEventCancelled 60 | } 61 | 62 | override fun setCancelled(cancelled: Boolean) 63 | { 64 | isEventCancelled = cancelled 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/main/kotlin/me/ebonjaeger/perworldinventory/command/ConvertCommand.kt: -------------------------------------------------------------------------------- 1 | package me.ebonjaeger.perworldinventory.command 2 | 3 | import co.aikar.commands.BaseCommand 4 | import co.aikar.commands.annotation.CommandAlias 5 | import co.aikar.commands.annotation.CommandPermission 6 | import co.aikar.commands.annotation.Description 7 | import co.aikar.commands.annotation.Subcommand 8 | import com.onarandombox.multiverseinventories.MultiverseInventories 9 | import me.ebonjaeger.perworldinventory.conversion.ConvertService 10 | import org.bukkit.ChatColor 11 | import org.bukkit.command.CommandSender 12 | import org.bukkit.plugin.PluginManager 13 | import javax.inject.Inject 14 | 15 | @CommandAlias("perworldinventory|pwi") 16 | class ConvertCommand @Inject constructor(private val pluginManager: PluginManager, 17 | private val convertService: ConvertService) : BaseCommand() { 18 | 19 | @Subcommand("convert") 20 | @CommandPermission("perworldinventory.convert") 21 | @Description("Convert inventory data from another plugin") 22 | fun onConvert(sender: CommandSender) { 23 | if (convertService.isConverting()) { 24 | sender.sendMessage("${ChatColor.DARK_RED}» ${ChatColor.GRAY}A data conversion is already in progress!") 25 | return 26 | } 27 | 28 | if (!pluginManager.isPluginEnabled("MultiVerse-Inventories")) { 29 | sender.sendMessage("${ChatColor.DARK_RED}» ${ChatColor.GRAY}MultiVerse-Inventories is not installed! Cannot convert data!") 30 | return 31 | } 32 | 33 | val mvi = pluginManager.getPlugin("MultiVerse-Inventories") 34 | if (mvi == null) { 35 | sender.sendMessage("${ChatColor.DARK_RED}» ${ChatColor.GRAY}Unable to get MultiVerse-Inventories instance!") 36 | return 37 | } 38 | 39 | convertService.beginConverting(sender, mvi as MultiverseInventories) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/test/kotlin/me/ebonjaeger/perworldinventory/configuration/SettingsConsistencyTest.kt: -------------------------------------------------------------------------------- 1 | package me.ebonjaeger.perworldinventory.configuration 2 | 3 | import ch.jalu.configme.configurationdata.ConfigurationDataBuilder 4 | import ch.jalu.configme.resource.YamlFileReader 5 | import com.google.common.collect.Sets 6 | import com.natpryce.hamkrest.assertion.assertThat 7 | import com.natpryce.hamkrest.equalTo 8 | import com.natpryce.hamkrest.isEmpty 9 | import me.ebonjaeger.perworldinventory.TestHelper.getFromJar 10 | import org.junit.jupiter.api.Test 11 | 12 | /** 13 | * Tests that the config.yml file corresponds with the settings holder classes in the code. 14 | */ 15 | class SettingsConsistencyTest 16 | { 17 | 18 | private val configData = ConfigurationDataBuilder.createConfiguration( 19 | PluginSettings::class.java, 20 | PlayerSettings::class.java, 21 | MetricsSettings::class.java) 22 | 23 | private val yamlReader = YamlFileReader(getFromJar("/config.yml")) 24 | 25 | @Test 26 | fun shouldContainAllPropertiesWithSameDefaultValue() 27 | { 28 | // given / when / then 29 | configData.properties.forEach { 30 | assertThat("config.yml does not have property for $it", 31 | it.isPresent(yamlReader), equalTo(true)) 32 | assertThat("config.yml does not have same default value for $it", 33 | it.determineValue(yamlReader), equalTo(it.defaultValue)) 34 | } 35 | } 36 | 37 | @Test 38 | fun shouldNotHaveUnknownProperties() 39 | { 40 | // given 41 | val keysInYaml = yamlReader.getKeys(true) 42 | 43 | val keysInCode = configData.properties.map { it.path }.toSet() 44 | 45 | // when / then 46 | val difference = Sets.difference(keysInYaml, keysInCode) 47 | assertThat("config.yml has unknown properties", 48 | difference, isEmpty) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/test/kotlin/me/ebonjaeger/perworldinventory/serialization/PotionSerializerTest.kt: -------------------------------------------------------------------------------- 1 | package me.ebonjaeger.perworldinventory.serialization 2 | 3 | import com.natpryce.hamkrest.assertion.assertThat 4 | import com.natpryce.hamkrest.equalTo 5 | import org.bukkit.potion.PotionEffect 6 | import org.bukkit.potion.PotionEffectType 7 | import org.junit.jupiter.api.BeforeEach 8 | import org.junit.jupiter.api.Disabled 9 | import org.junit.jupiter.api.Test 10 | 11 | @Disabled 12 | class PotionSerializerTest 13 | { 14 | 15 | @BeforeEach 16 | fun registerPotionEffects() 17 | { 18 | PotionEffectType.registerPotionEffectType(PotionEffectType.ABSORPTION) 19 | PotionEffectType.registerPotionEffectType(PotionEffectType.GLOWING) 20 | } 21 | 22 | @Test 23 | fun verifySerializedPotions() 24 | { 25 | // given 26 | val effects = mutableListOf() 27 | print(PotionEffectType.values()) 28 | 29 | val effect1 = PotionEffect(PotionEffectType.ABSORPTION, 5, 2, true, false) 30 | val effect2 = PotionEffect(PotionEffectType.GLOWING, 27, 1) 31 | 32 | effects.add(effect1) 33 | effects.add(effect2) 34 | 35 | // when 36 | val json = PotionSerializer.serialize(effects) 37 | 38 | // then 39 | /*val result = PotionSerializer.deserialize(json) 40 | assertHasSameProperties(result.first(), effect1) 41 | assertHasSameProperties(result.last(), effect2)*/ 42 | } 43 | 44 | private fun assertHasSameProperties(given: PotionEffect, expected: PotionEffect) 45 | { 46 | assertThat(given.type, equalTo(expected.type)) 47 | assertThat(given.duration, equalTo(expected.duration)) 48 | assertThat(given.amplifier, equalTo(expected.amplifier)) 49 | assertThat(given.isAmbient, equalTo(expected.isAmbient)) 50 | assertThat(given.hasParticles(), equalTo(expected.hasParticles())) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![PerWorldInventory](https://i.imgur.com/FA8b4OY.png) 2 | 3 | PerWorldInventory Is a multiworld inventory plugin for CraftBukkit and Spigot that supports UUIDs. 4 | This version 2.0.0 of the plugin is a full, from-scratch rewrite using the 5 | Kotlin programming language. 6 | 7 | *** 8 | 9 | ## Installation and Usage 10 | 11 | [![Build Status](https://ci.codemc.org/job/EbonJaeger/job/PerWorldInventory-KT/badge/icon)](https://ci.codemc.org/job/EbonJaeger/job/PerWorldInventory-KT/) 12 | [![Dependency Status](https://www.versioneye.com/user/projects/5aea27cb0fb24f5450e028a7/badge.svg?style=flat-square)](https://www.versioneye.com/user/projects/5aea27cb0fb24f5450e028a7) 13 | [![codecov](https://codecov.io/gh/EbonJaeger/perworldinventory-kt/branch/master/graph/badge.svg)](https://codecov.io/gh/EbonJaeger/perworldinventory-kt) 14 | ## Building Prerequisites 15 | * JDK 8 16 | * Maven 17 | 18 | ## Building 19 | 20 | If you want to build PerWorldInventory yourself, you will need Maven. 21 | 22 | 1) Clone the PerWorldInventory project: ```git clone https://github.com/Gnat008/perworldinventory-kt.git``` 23 | 24 | 2) Run ```mvn clean install``` 25 | 26 | That is all! 27 | 28 | This should give you a copy of PerWorldInventory.jar under the target/ directory. 29 | 30 | ## License 31 | 32 | PerWorldInventory by EbonJaeger is licensed under the [MIT License]. 33 | 34 | ## Contributing 35 | Are you a talented programmer looking to contribute some code? I'd love the 36 | help! 37 | Open a pull request with your changes. 38 | * Please conform to the project code conventions: 39 | * Opening braces on new lines 40 | * Four (4) spaces for indentations; no tabs! 41 | * Individual lines of code should usually not be over 80 characters long 42 | * One pull request should only tackle one issue 43 | 44 | Please note that not all pull requests will be accepted, and I may require 45 | changes before they are accepted! 46 | 47 | Readme file made by @Jaryn-R, edited and updated by EbonJaeger 48 | -------------------------------------------------------------------------------- /src/main/kotlin/me/ebonjaeger/perworldinventory/configuration/Settings.kt: -------------------------------------------------------------------------------- 1 | package me.ebonjaeger.perworldinventory.configuration 2 | 3 | import ch.jalu.configme.SettingsHolder 4 | import ch.jalu.configme.SettingsManagerImpl 5 | import ch.jalu.configme.configurationdata.ConfigurationData 6 | import ch.jalu.configme.configurationdata.ConfigurationDataBuilder 7 | import ch.jalu.configme.migration.MigrationService 8 | import ch.jalu.configme.resource.YamlFileResource 9 | import java.io.File 10 | 11 | /** 12 | * Settings class for PWI settings. 13 | * 14 | * @param resource The property resource to read from and write to 15 | * @param migrater The configuration migrater to use to add new config options 16 | * @param settingsHolders Classes that hold the actual properties 17 | */ 18 | class Settings private constructor(resource: YamlFileResource, 19 | configurationData: ConfigurationData, 20 | migrater: MigrationService) : 21 | SettingsManagerImpl(resource, configurationData, migrater) 22 | { 23 | 24 | companion object { 25 | 26 | /** All [SettingsHolder] classes of PerWorldInventory. */ 27 | private val PROPERTY_HOLDERS = arrayOf( 28 | PluginSettings::class.java, 29 | MetricsSettings::class.java, 30 | PlayerSettings::class.java) 31 | 32 | /** 33 | * Creates a [Settings] instance, using the given file as config file. 34 | * 35 | * @param file the config file to load 36 | * @return settings instance for the file 37 | */ 38 | fun create(file: File): Settings { 39 | val fileResource = YamlFileResource(file) 40 | val configurationData = ConfigurationDataBuilder.createConfiguration(*PROPERTY_HOLDERS) 41 | val migrater = PwiMigrationService() 42 | 43 | return Settings(fileResource, configurationData, migrater) 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/kotlin/me/ebonjaeger/perworldinventory/listener/player/PlayerTeleportListener.kt: -------------------------------------------------------------------------------- 1 | package me.ebonjaeger.perworldinventory.listener.player 2 | 3 | import me.ebonjaeger.perworldinventory.ConsoleLogger 4 | import me.ebonjaeger.perworldinventory.GroupManager 5 | import me.ebonjaeger.perworldinventory.data.ProfileManager 6 | import org.bukkit.event.EventHandler 7 | import org.bukkit.event.EventPriority 8 | import org.bukkit.event.Listener 9 | import org.bukkit.event.player.PlayerTeleportEvent 10 | import javax.inject.Inject 11 | 12 | class PlayerTeleportListener @Inject constructor(private val groupManager: GroupManager, 13 | private val profileManager: ProfileManager) : Listener 14 | { 15 | 16 | @EventHandler(priority = EventPriority.MONITOR) 17 | fun onPlayerTeleport(event: PlayerTeleportEvent) 18 | { 19 | val destination = event.to ?: return // Why is it even possible for the destination to be null? 20 | 21 | if (event.isCancelled || event.from.world == destination.world) 22 | { 23 | return 24 | } 25 | 26 | val player = event.player 27 | val worldFromName = event.from.world!!.name // The server will never provide a null world in a Location 28 | val worldToName = destination.world!!.name // The server will never provide a null world in a Location 29 | val groupFrom = groupManager.getGroupFromWorld(worldFromName) 30 | val groupTo = groupManager.getGroupFromWorld(worldToName) 31 | 32 | ConsoleLogger.fine("onPlayerTeleport: '${event.player.name}' going teleporting to another world") 33 | ConsoleLogger.debug("onPlayerTeleport: worldFrom='$worldFromName', worldTo='$worldToName'") 34 | 35 | if (groupFrom == groupTo) 36 | { 37 | return 38 | } 39 | 40 | profileManager.addPlayerProfile(player, groupFrom, player.gameMode) 41 | 42 | // TODO: Save the player's last location 43 | 44 | // Possibly prevents item duping exploit 45 | player.closeInventory() 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/kotlin/me/ebonjaeger/perworldinventory/serialization/StatSerializer.kt: -------------------------------------------------------------------------------- 1 | package me.ebonjaeger.perworldinventory.serialization 2 | 3 | import me.ebonjaeger.perworldinventory.data.PlayerDefaults 4 | import me.ebonjaeger.perworldinventory.data.PlayerProfile 5 | import net.minidev.json.JSONObject 6 | import org.bukkit.GameMode 7 | 8 | object StatSerializer 9 | { 10 | 11 | /** 12 | * Validate data by making sure that all stats are present. 13 | * If something is missing, add it to the object with sane 14 | * defaults, in most cases using the [PlayerDefaults] object. 15 | * 16 | * @param data The data to validate 17 | * @param playerName The name of the player. Necessary in case 18 | * display name is not stored in the data 19 | * @return The data with all stats present 20 | */ 21 | fun validateStats(data: JSONObject, playerName: String): JSONObject 22 | { 23 | if (!data.containsKey("can-fly")) data["can-fly"] = false 24 | if (!data.containsKey("display-name")) data["display-name"] = playerName 25 | if (!data.containsKey("exhaustion")) data["exhaustion"] = PlayerDefaults.EXHAUSTION 26 | if (!data.containsKey("exp")) data["exp"] = PlayerDefaults.EXPERIENCE 27 | if (!data.containsKey("flying")) data["flying"] = false 28 | if (!data.containsKey("food")) data["food"] = PlayerDefaults.FOOD_LEVEL 29 | if (!data.containsKey("gamemode")) data["gamemode"] = GameMode.SURVIVAL.toString() 30 | if (!data.containsKey("max-health")) data["max-health"] = PlayerDefaults.HEALTH 31 | if (!data.containsKey("health")) data["health"] = PlayerDefaults.HEALTH 32 | if (!data.containsKey("level")) data["level"] = PlayerDefaults.LEVEL 33 | if (!data.containsKey("saturation")) data["saturation"] = PlayerDefaults.SATURATION 34 | if (!data.containsKey("fallDistance")) data["fallDistance"] = PlayerDefaults.FALL_DISTANCE 35 | if (!data.containsKey("fireTicks")) data["fireTicks"] = PlayerDefaults.FIRE_TICKS 36 | if (!data.containsKey("maxAir")) data["maxAir"] = PlayerDefaults.MAXIMUM_AIR 37 | if (!data.containsKey("remainingAir")) data["remainingAir"] = PlayerDefaults.REMAINING_AIR 38 | 39 | return data 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/kotlin/me/ebonjaeger/perworldinventory/data/DataSource.kt: -------------------------------------------------------------------------------- 1 | package me.ebonjaeger.perworldinventory.data 2 | 3 | import com.google.gson.JsonObject 4 | import org.bukkit.Location 5 | import org.bukkit.entity.Player 6 | 7 | interface DataSource 8 | { 9 | 10 | /** 11 | * Saves a player's information to the database. 12 | * 13 | * @param key The [ProfileKey] for this profile 14 | * @param player The [PlayerProfile] with the data 15 | */ 16 | fun savePlayer(key: ProfileKey, player: PlayerProfile) 17 | 18 | /** 19 | * Save the location of a player when they log out or are kicked from the 20 | * server. 21 | * 22 | * @param player The player who logged out 23 | */ 24 | fun saveLogout(player: Player) 25 | 26 | /** 27 | * Save the location of a player when they teleport to a different world. 28 | * 29 | * @param player The player that teleported 30 | * @param location The location of the player on teleport 31 | */ 32 | fun saveLocation(player: Player, location: Location) 33 | 34 | /** 35 | * Retrieves a player's data from the database. 36 | * 37 | * @param key The [ProfileKey] to get the data for 38 | * @param player The [Player] that the data will be applied to 39 | * @return A [JsonObject] with all of the player's information 40 | */ 41 | fun getPlayer(key: ProfileKey, player: Player): PlayerProfile? 42 | 43 | /** 44 | * Get the name of the world that a player logged out in. 45 | * If this is their first time logging in, this method will return null 46 | * instead of a location. 47 | * 48 | * @param player The player to get the last logout for 49 | * @return The location of the player when they last logged out 50 | */ 51 | fun getLogout(player: Player): Location? 52 | 53 | /** 54 | * Get a player's last location in a world. If a player has never been to 55 | * the world before, this method will return null. 56 | * 57 | * @param player The player to get the last location of 58 | * @param world The name of the world the player is going to 59 | * @return The last location in the world where the player was standing 60 | */ 61 | fun getLocation(player: Player, world: String): Location? 62 | } 63 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Artwork ### 2 | logos/ 3 | 4 | ### Java ### 5 | *.class 6 | 7 | # Mobile Tools for Java (J2ME) 8 | .mtj.tmp/ 9 | 10 | # Package Files # 11 | #*.jar 12 | *.war 13 | *.ear 14 | 15 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 16 | hs_err_pid*Land 17 | 18 | 19 | ### Maven ### 20 | target/ 21 | pom.xml.tag 22 | pom.xml.releaseBackup 23 | pom.xml.versionsBackup 24 | pom.xml.next 25 | release.properties 26 | dependency-reduced-pom.xml 27 | buildNumber.properties 28 | 29 | 30 | ### Intellij ### 31 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm 32 | 33 | *.iml 34 | 35 | ## Directory-based project format: 36 | .idea/ 37 | # if you remove the above rule, at least ignore the following: 38 | 39 | # User-specific stuff: 40 | # .idea/workspace.xml 41 | # .idea/tasks.xml 42 | # .idea/dictionaries 43 | 44 | # Sensitive or high-churn files: 45 | # .idea/dataSources.ids 46 | # .idea/dataSources.xml 47 | # .idea/sqlDataSources.xml 48 | # .idea/dynamic.xml 49 | # .idea/uiDesigner.xml 50 | 51 | # Gradle: 52 | # .idea/gradle.xml 53 | # .idea/libraries 54 | 55 | # Mongo Explorer plugin: 56 | # .idea/mongoSettings.xml 57 | 58 | ## File-based project format: 59 | *.ipr 60 | *.iws 61 | 62 | ## Plugin-specific files: 63 | 64 | # IntelliJ 65 | /out/ 66 | 67 | # mpeltonen/sbt-idea plugin 68 | .idea_modules/ 69 | 70 | # JIRA plugin 71 | atlassian-ide-plugin.xml 72 | 73 | # Crashlytics plugin (for Android Studio and IntelliJ) 74 | com_crashlytics_export_strings.xml 75 | crashlytics.properties 76 | crashlytics-build.properties 77 | 78 | 79 | ### Eclipse ### 80 | *.pydevproject 81 | .metadata 82 | .gradle 83 | bin/ 84 | tmp/ 85 | *.tmp 86 | *.bak 87 | *.swp 88 | *~.nib 89 | local.properties 90 | .settings/ 91 | .loadpath 92 | 93 | # Eclipse Core 94 | .project 95 | 96 | # External tool builders 97 | .externalToolBuilders/ 98 | 99 | # Locally stored "Eclipse launch configurations" 100 | *.launch 101 | 102 | # CDT-specific 103 | .cproject 104 | 105 | # JDT-specific (Eclipse Java Development Tools) 106 | .classpath 107 | 108 | # PDT-specific 109 | .buildpath 110 | 111 | # sbteclipse plugin 112 | .target 113 | 114 | # TeXlipse plugin 115 | .texlipse 116 | -------------------------------------------------------------------------------- /src/test/kotlin/me/ebonjaeger/perworldinventory/UtilsTest.kt: -------------------------------------------------------------------------------- 1 | package me.ebonjaeger.perworldinventory 2 | 3 | import com.natpryce.hamkrest.assertion.assertThat 4 | import com.natpryce.hamkrest.equalTo 5 | import org.junit.jupiter.api.Test 6 | 7 | /** 8 | * Tests for [Utils]. 9 | */ 10 | class UtilsTest 11 | { 12 | 13 | val VERSION_1_8_8 = "git-Spigot-8a048fe-3c19fef (MC: 1.8.8)" 14 | val VERSION_1_9 = "git-Spigot-8a048fe-3c19fef (MC: 1.9)" 15 | val VERSION_1_9_2 = "git-Spigot-8a048fe-3c19fef (MC: 1.9.2)" 16 | val VERSION_1_9_4 = "git-Spigot-8a048fe-3c19fef (MC: 1.9.4)" 17 | val VERSION_1_10 = "git-Spigot-8a048fe-3c19fef (MC: 1.10)" 18 | val VERSION_1_10_2 = "git-Spigot-8a048fe-3c19fef (MC: 1.10.2)" 19 | 20 | @Test 21 | fun shouldReturnTrueSameMinorVersion() 22 | { 23 | // given/when 24 | val result = Utils.checkServerVersion(VERSION_1_9, 1, 9, 0) 25 | 26 | // then 27 | assertThat(result, equalTo(true)) 28 | } 29 | 30 | @Test 31 | fun shouldReturnTrueSameMinorSamePatchVersion() 32 | { 33 | // given/when 34 | val result = Utils.checkServerVersion(VERSION_1_9_2, 1, 9, 2) 35 | 36 | // then 37 | assertThat(result, equalTo(true)) 38 | } 39 | 40 | @Test 41 | fun shouldReturnTrueSameMinorGreatorPatchVersion() 42 | { 43 | // given/when 44 | val result = Utils.checkServerVersion(VERSION_1_9_4, 1, 9, 2) 45 | 46 | // then 47 | assertThat(result, equalTo(true)) 48 | } 49 | 50 | @Test 51 | fun shouldReturnTrueHigherMinorVersion() 52 | { 53 | // given/when 54 | val result = Utils.checkServerVersion(VERSION_1_10, 1, 9, 2) 55 | 56 | // then 57 | assertThat(result, equalTo(true)) 58 | } 59 | 60 | @Test 61 | fun shouldReturnFalseLowerMinorVersion() 62 | { 63 | // given/when 64 | val result = Utils.checkServerVersion(VERSION_1_8_8, 1, 9, 2) 65 | 66 | // then 67 | assertThat(result, equalTo(false)) 68 | } 69 | 70 | @Test 71 | fun shouldReturnFalseSameMinorLowerPatchVersion() 72 | { 73 | // given/when 74 | val result = Utils.checkServerVersion(VERSION_1_9_2, 1, 9, 4) 75 | 76 | // then 77 | assertThat(result, equalTo(false)) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/main/kotlin/me/ebonjaeger/perworldinventory/listener/player/PlayerRespawnListener.kt: -------------------------------------------------------------------------------- 1 | package me.ebonjaeger.perworldinventory.listener.player 2 | 3 | import me.ebonjaeger.perworldinventory.ConsoleLogger 4 | import me.ebonjaeger.perworldinventory.GroupManager 5 | import me.ebonjaeger.perworldinventory.configuration.PluginSettings 6 | import me.ebonjaeger.perworldinventory.configuration.Settings 7 | import org.bukkit.Bukkit 8 | import org.bukkit.event.EventHandler 9 | import org.bukkit.event.EventPriority 10 | import org.bukkit.event.Listener 11 | import org.bukkit.event.player.PlayerRespawnEvent 12 | import javax.inject.Inject 13 | 14 | /** 15 | * Listener for [PlayerRespawnEvent]. 16 | */ 17 | class PlayerRespawnListener @Inject constructor(private val groupManager: GroupManager, 18 | private val settings: Settings) : Listener 19 | { 20 | 21 | @EventHandler(priority = EventPriority.LOW) 22 | fun onPlayerRespawn(event: PlayerRespawnEvent) { 23 | // Do nothing if managing respawns is disabled in the config 24 | if (!settings.getProperty(PluginSettings.MANAGE_DEATH_RESPAWN)) { 25 | return 26 | } 27 | 28 | val group = groupManager.getGroupFromWorld(event.player.location.world!!.name) // The server will never provide a null world in a Location 29 | 30 | // Check for a bed location in the group 31 | val bedLocation = event.player.bedSpawnLocation 32 | if (bedLocation != null) { 33 | // Set the spawn location to the bed and return if it's in the same group 34 | if (group.containsWorld(bedLocation.world!!.name)) { 35 | event.respawnLocation = bedLocation 36 | return 37 | } 38 | } 39 | 40 | // Set the respawn location to the world set in the config 41 | val respawnWorld = group.respawnWorld 42 | if (respawnWorld != null && group.containsWorld(respawnWorld)) { 43 | val world = Bukkit.getWorld(respawnWorld) 44 | 45 | if (world == null) { 46 | ConsoleLogger.warning("Unable to set respawn location: World '$respawnWorld' doesn't exist!") 47 | return 48 | } 49 | 50 | event.respawnLocation = world.spawnLocation 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/main/kotlin/me/ebonjaeger/perworldinventory/data/migration/MigrationService.kt: -------------------------------------------------------------------------------- 1 | package me.ebonjaeger.perworldinventory.data.migration 2 | 3 | import me.ebonjaeger.perworldinventory.ConsoleLogger 4 | import me.ebonjaeger.perworldinventory.GroupManager 5 | import me.ebonjaeger.perworldinventory.PerWorldInventory 6 | import me.ebonjaeger.perworldinventory.initialization.DataDirectory 7 | import org.bukkit.Bukkit 8 | import org.bukkit.ChatColor 9 | import org.bukkit.command.CommandSender 10 | import org.bukkit.command.ConsoleCommandSender 11 | import org.bukkit.entity.Player 12 | import java.io.File 13 | import javax.inject.Inject 14 | 15 | class MigrationService @Inject constructor(private val groupManager: GroupManager, 16 | private val plugin: PerWorldInventory, 17 | @DataDirectory private val dataDirectory: File) { 18 | 19 | private var migrating = false 20 | private var sender: CommandSender? = null 21 | 22 | fun isMigrating(): Boolean { 23 | return migrating 24 | } 25 | 26 | fun beginMigration(sender: CommandSender) { 27 | val offlinePlayers = Bukkit.getOfflinePlayers() 28 | 29 | if (migrating) { 30 | return 31 | } 32 | 33 | this.sender = sender 34 | 35 | if (sender !is ConsoleCommandSender) { // No need to send a message to console when console did the command 36 | ConsoleLogger.info("Beginning data migration to new format.") 37 | } 38 | 39 | migrating = true 40 | 41 | val task = MigrationTask(this, offlinePlayers, dataDirectory, groupManager.groups.values) 42 | task.runTaskTimerAsynchronously(plugin, 0, 20) 43 | } 44 | 45 | /** 46 | * Alerts that the migration completed. 47 | * 48 | * @param numMigrated The number of profiles migrated 49 | */ 50 | fun finishMigration(numMigrated: Int) { 51 | migrating = false 52 | ConsoleLogger.info("Data migration has been completed! Migrated $numMigrated profiles.") 53 | if (sender != null && sender is Player) { 54 | if ((sender as Player).isOnline) { 55 | (sender as Player).sendMessage("${ChatColor.GREEN}» ${ChatColor.GRAY}Data migration has been completed!") 56 | } 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/test/kotlin/me/ebonjaeger/perworldinventory/event/EventConsistencyTest.kt: -------------------------------------------------------------------------------- 1 | package me.ebonjaeger.perworldinventory.event 2 | 3 | import org.bukkit.event.Event 4 | import org.bukkit.event.HandlerList 5 | import org.junit.jupiter.api.Test 6 | import org.reflections.Reflections 7 | import org.reflections.scanners.SubTypesScanner 8 | import java.lang.reflect.Method 9 | import java.lang.reflect.Modifier 10 | import kotlin.test.assertNotNull 11 | 12 | /** 13 | * Tests that events are set up to conform to Bukkit's requirements. 14 | * 15 | * 16 | * This test is implemented in Java as to ensure that required static members are really 17 | * visible as such inside Java. 18 | */ 19 | class EventConsistencyTest { 20 | 21 | /** 22 | * Bukkit requires a static getHandlerList() method on all event classes, see [Event]. 23 | * This test checks that such a method is present and that it returns the same instance as the 24 | * method on the [Event] interface. 25 | */ 26 | @Test 27 | @Throws(Exception::class) 28 | fun shouldHaveStaticEventHandlerMethod() { 29 | // given 30 | val reflections = Reflections("me.ebonjaeger.perworldinventory", SubTypesScanner()) 31 | val eventClasses = reflections.getSubTypesOf(Event::class.java) 32 | 33 | check(eventClasses.isNotEmpty()) { "Failed to collect any Event classes. Is the package correct?" } 34 | 35 | for (clazz in eventClasses) { 36 | val staticHandlerList = getHandlerListFromStaticMethod(clazz) 37 | assertNotNull(staticHandlerList, "Handler list should not be null for class $clazz") 38 | } 39 | } 40 | 41 | companion object { 42 | 43 | @Throws(Exception::class) 44 | private fun getHandlerListFromStaticMethod(clz: Class<*>): HandlerList { 45 | val staticHandlerListMethod = getStaticHandlerListMethod(clz) 46 | return staticHandlerListMethod.invoke(null) as HandlerList 47 | } 48 | 49 | private fun getStaticHandlerListMethod(clz: Class<*>): Method { 50 | var method: Method? = null 51 | try { 52 | method = clz.getMethod("getHandlerList") 53 | } catch (e: NoSuchMethodException) { // swallow 54 | } 55 | 56 | checkNotNull(method) { "No public getHandlerList() method on $clz" } 57 | check(Modifier.isStatic(method.modifiers)) { "Method getHandlerList() must be static on $clz" } 58 | 59 | return method 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/main/kotlin/me/ebonjaeger/perworldinventory/serialization/PlayerSerializer.kt: -------------------------------------------------------------------------------- 1 | package me.ebonjaeger.perworldinventory.serialization 2 | 3 | import me.ebonjaeger.perworldinventory.data.PlayerProfile 4 | import net.minidev.json.JSONArray 5 | import net.minidev.json.JSONObject 6 | import org.bukkit.GameMode 7 | import org.bukkit.util.NumberConversions 8 | 9 | object PlayerSerializer 10 | { 11 | 12 | fun deserialize(data: JSONObject, playerName: String, inventorySize: Int, eChestSize: Int): PlayerProfile 13 | { 14 | // Get the data format being used 15 | var format = 3 16 | if (data.containsKey("data-format")) 17 | { 18 | format = data["data-format"] as Int 19 | } 20 | 21 | val inventory = data["inventory"] as JSONObject 22 | val items = InventoryHelper.deserialize(inventory["inventory"] as JSONArray, 23 | inventorySize, 24 | format) 25 | val armor = InventoryHelper.deserialize(inventory["armor"] as JSONArray, 4, format) 26 | val enderChest = InventoryHelper.deserialize(data["ender-chest"] as JSONArray, 27 | eChestSize, 28 | format) 29 | val stats = StatSerializer.validateStats(data["stats"] as JSONObject, playerName) 30 | val potionEffects = PotionSerializer.deserialize(stats["potion-effects"] as JSONArray) 31 | val balance = if (data.containsKey("economy")) 32 | { 33 | EconomySerializer.deserialize(data["economy"] as JSONObject) 34 | } else 35 | { 36 | 0.0 37 | } 38 | 39 | return PlayerProfile(armor, 40 | enderChest, 41 | items, 42 | stats["can-fly"] as Boolean, 43 | stats["display-name"] as String, 44 | stats["exhaustion"] as Float, 45 | stats["exp"] as Float, 46 | stats["flying"] as Boolean, 47 | stats["food"] as Int, 48 | NumberConversions.toDouble(stats["max-health"]), 49 | NumberConversions.toDouble(stats["health"]), 50 | GameMode.valueOf(stats["gamemode"] as String), 51 | stats["level"] as Int, 52 | stats["saturation"] as Float, 53 | potionEffects, 54 | stats["fallDistance"] as Float, 55 | stats["fireTicks"] as Int, 56 | stats["maxAir"] as Int, 57 | stats["remainingAir"] as Int, 58 | balance) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/test/kotlin/me/ebonjaeger/perworldinventory/serialization/LocationSerializerTest.kt: -------------------------------------------------------------------------------- 1 | package me.ebonjaeger.perworldinventory.serialization 2 | 3 | import com.natpryce.hamkrest.assertion.assertThat 4 | import com.natpryce.hamkrest.equalTo 5 | import io.mockk.every 6 | import io.mockk.mockkClass 7 | import me.ebonjaeger.perworldinventory.TestHelper 8 | import net.minidev.json.JSONObject 9 | import org.bukkit.Bukkit 10 | import org.bukkit.Location 11 | import org.bukkit.World 12 | import org.junit.jupiter.api.BeforeEach 13 | import org.junit.jupiter.api.Test 14 | 15 | class LocationSerializerTest 16 | { 17 | 18 | @BeforeEach 19 | fun createMocks() 20 | { 21 | TestHelper.mockBukkit() 22 | } 23 | 24 | @Test 25 | fun verifySerializedLocation() { 26 | // given 27 | val world = mockkClass(World::class) 28 | every { Bukkit.getWorld("test") } returns world 29 | every { world.name } returns "test" 30 | 31 | val loc = Location(world, 134.523, 64.0, -3876.26437, 432.67F, 32.63413F) 32 | 33 | // when 34 | val json = LocationSerializer.serialize(loc) 35 | 36 | // then 37 | val result = LocationSerializer.deserialize(json) 38 | assertHasSameProperties(result, loc) 39 | } 40 | 41 | @Test 42 | fun deserializedOldDataCorrectly() { 43 | // given 44 | val world = mockkClass(World::class) 45 | every { Bukkit.getWorld("test") } returns world 46 | every { world.name } returns "test" 47 | 48 | val expected = Location(world, 14521.3, 14.0, -2352.121, 123.3F, -2352.532F) 49 | 50 | val obj = JSONObject() 51 | obj["world"] = "test" 52 | obj["x"] = 14521.3 53 | obj["y"] = 14.0 54 | obj["z"] = -2352.121 55 | obj["yaw"] = 123.3F 56 | obj["pitch"] = -2352.532F 57 | 58 | // when 59 | val actual = LocationSerializer.deserialize(obj) 60 | 61 | // then 62 | assertHasSameProperties(actual, expected) 63 | } 64 | 65 | private fun assertHasSameProperties(given: Location, expected: Location) 66 | { 67 | assertThat(given.world!!.name, equalTo(expected.world!!.name)) // We created them, we know they're not null! 68 | assertThat(given.x.toFloat(), equalTo(expected.x.toFloat())) 69 | assertThat(given.y.toFloat(), equalTo(expected.y.toFloat())) 70 | assertThat(given.z.toFloat(), equalTo(expected.z.toFloat())) 71 | assertThat(given.pitch, equalTo(expected.pitch)) 72 | assertThat(given.yaw, equalTo(expected.yaw)) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/main/kotlin/me/ebonjaeger/perworldinventory/serialization/InventoryHelper.kt: -------------------------------------------------------------------------------- 1 | package me.ebonjaeger.perworldinventory.serialization 2 | 3 | import me.ebonjaeger.perworldinventory.ConsoleLogger 4 | import me.ebonjaeger.perworldinventory.data.PlayerProfile 5 | import net.minidev.json.JSONArray 6 | import net.minidev.json.JSONObject 7 | import org.bukkit.Material 8 | import org.bukkit.inventory.ItemStack 9 | 10 | object InventoryHelper 11 | { 12 | 13 | /** 14 | * Serialize an inventory's contents. 15 | * 16 | * @param contents The items in the inventory 17 | * @return A JsonArray containing the inventory contents 18 | */ 19 | fun serializeInventory(contents: Array): List> 20 | { 21 | val inventory = mutableListOf>() 22 | 23 | contents.indices 24 | .map { ItemSerializer.serialize(contents[it], it) } 25 | .forEach { inventory.add(it) } 26 | 27 | return inventory 28 | } 29 | 30 | fun listToInventory(items: List<*>): Array { 31 | val contents = mutableListOf() 32 | val iter = items.listIterator() 33 | while (iter.hasNext()) { 34 | val next = iter.next() as Map<*, *> 35 | val item = next["item"] as ItemStack 36 | contents.add(item) 37 | } 38 | 39 | return contents.toTypedArray() 40 | } 41 | 42 | /** 43 | * Gets an ItemStack array from serialized inventory contents. 44 | * 45 | * @param array The array of items to deserialize 46 | * @param size The expected size of the inventory; can be greater than expected 47 | * @param format The data format being used 48 | * @return An array of ItemStacks 49 | */ 50 | fun deserialize(array: JSONArray, size: Int, format: Int): Array 51 | { 52 | val contents = Array(size) { ItemStack(Material.AIR) } 53 | 54 | for (i in 0 until array.size) 55 | { 56 | // We don't want to risk failing to deserialize a players inventory. 57 | // Try your best to deserialize as much as possible. 58 | try 59 | { 60 | val obj = array[i] as JSONObject 61 | val index = obj["index"] as Int 62 | val item = ItemSerializer.deserialize(obj, format) 63 | 64 | contents[index] = item 65 | } catch (ex: Exception) 66 | { 67 | ConsoleLogger.warning("Failed to deserialize item in inventory:", ex) 68 | } 69 | } 70 | 71 | return contents 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/main/kotlin/me/ebonjaeger/perworldinventory/listener/player/PlayerSpawnLocationListener.kt: -------------------------------------------------------------------------------- 1 | package me.ebonjaeger.perworldinventory.listener.player 2 | 3 | import me.ebonjaeger.perworldinventory.ConsoleLogger 4 | import me.ebonjaeger.perworldinventory.GroupManager 5 | import me.ebonjaeger.perworldinventory.configuration.PluginSettings 6 | import me.ebonjaeger.perworldinventory.configuration.Settings 7 | import me.ebonjaeger.perworldinventory.data.DataSource 8 | import me.ebonjaeger.perworldinventory.data.ProfileManager 9 | import org.bukkit.event.EventHandler 10 | import org.bukkit.event.EventPriority 11 | import org.bukkit.event.Listener 12 | import org.spigotmc.event.player.PlayerSpawnLocationEvent 13 | import javax.inject.Inject 14 | 15 | 16 | class PlayerSpawnLocationListener @Inject constructor(private val dataSource: DataSource, 17 | private val groupManager: GroupManager, 18 | private val settings: Settings, 19 | private val profileManager: ProfileManager) : Listener 20 | { 21 | @EventHandler(priority = EventPriority.MONITOR) 22 | fun onPlayerSpawn(event: PlayerSpawnLocationEvent) 23 | { 24 | if (!settings.getProperty(PluginSettings.LOAD_DATA_ON_JOIN)) 25 | return 26 | 27 | val player = event.player 28 | val spawnWorld = event.spawnLocation.world!!.name // The server will never provide a null world in a Location 29 | ConsoleLogger.fine("onPlayerSpawn: '${player.name}' joining in world '$spawnWorld'") 30 | 31 | val location = dataSource.getLogout(player) 32 | 33 | if (location == null || location.world == null) { // No valid location found 34 | return 35 | } 36 | 37 | ConsoleLogger.debug("onPlayerSpawn: Logout location found for player '${player.name}': $location") 38 | 39 | val world = location.world 40 | if (world!!.name != spawnWorld) { // We saved this Location, so we can assume it has a world 41 | val spawnGroup = groupManager.getGroupFromWorld(spawnWorld) 42 | val logoutGroup = groupManager.getGroupFromWorld(world.name) 43 | 44 | if (spawnGroup != logoutGroup) { 45 | ConsoleLogger.fine("onPlayerSpawn: Current group does not match logout group for '${player.name}'") 46 | ConsoleLogger.debug("onPlayerSpawn: spawnGroup=$spawnGroup, logoutGroup=$logoutGroup") 47 | 48 | profileManager.addPlayerProfile(player, logoutGroup, player.gameMode) 49 | profileManager.getPlayerData(player, spawnGroup, player.gameMode) 50 | } 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/main/kotlin/me/ebonjaeger/perworldinventory/data/PlayerProperty.kt: -------------------------------------------------------------------------------- 1 | package me.ebonjaeger.perworldinventory.data 2 | 3 | import ch.jalu.configme.properties.Property 4 | import me.ebonjaeger.perworldinventory.configuration.PlayerSettings 5 | import me.ebonjaeger.perworldinventory.configuration.Settings 6 | import org.bukkit.entity.Player 7 | 8 | /** 9 | * Properties that can be conditionally transferred from a [PlayerProfile] to a [Player], 10 | * depending on a configurable setting. 11 | */ 12 | enum class PlayerProperty(private val property: Property?, 13 | private val accessors: ValueAccessors) { 14 | 15 | ALLOW_FLIGHT(PlayerSettings.LOAD_ALLOW_FLIGHT, 16 | ValueAccessors(Player::setAllowFlight, PlayerProfile::allowFlight)), 17 | 18 | DISPLAY_NAME(PlayerSettings.LOAD_DISPLAY_NAME, 19 | ValueAccessors(Player::setDisplayName, PlayerProfile::displayName)), 20 | 21 | EXHAUSTION(PlayerSettings.LOAD_EXHAUSTION, 22 | ValueAccessors(Player::setExhaustion, PlayerProfile::exhaustion)), 23 | 24 | EXPERIENCE(PlayerSettings.LOAD_EXP, 25 | ValueAccessors(Player::setExp, PlayerProfile::experience)), 26 | 27 | FOOD_LEVEL(PlayerSettings.LOAD_HUNGER, 28 | ValueAccessors(Player::setFoodLevel, PlayerProfile::foodLevel)), 29 | 30 | LEVEL(PlayerSettings.LOAD_LEVEL, 31 | ValueAccessors(Player::setLevel, PlayerProfile::level)), 32 | 33 | SATURATION(PlayerSettings.LOAD_SATURATION, 34 | ValueAccessors(Player::setSaturation, PlayerProfile::saturation)), 35 | 36 | FALL_DISTANCE(PlayerSettings.LOAD_FALL_DISTANCE, 37 | ValueAccessors(Player::setFallDistance, PlayerProfile::fallDistance)), 38 | 39 | FIRE_TICKS(PlayerSettings.LOAD_FIRE_TICKS, 40 | ValueAccessors(Player::setFireTicks, PlayerProfile::fireTicks)), 41 | 42 | MAXIMUM_AIR(PlayerSettings.LOAD_MAX_AIR, 43 | ValueAccessors(Player::setMaximumAir, PlayerProfile::maximumAir)), 44 | 45 | REMAINING_AIR(PlayerSettings.LOAD_REMAINING_AIR, 46 | ValueAccessors(Player::setRemainingAir, PlayerProfile::remainingAir)); 47 | 48 | 49 | fun applyFromProfileToPlayerIfConfigured(profile: PlayerProfile, player: Player, settings: Settings) { 50 | if (settings.getProperty(property)) { 51 | accessors.applyFromProfileToPlayer(profile, player) 52 | } 53 | } 54 | 55 | private class ValueAccessors(val playerSetter: (Player, T) -> Unit, 56 | val profileGetter: (PlayerProfile) -> T) { 57 | 58 | fun applyFromProfileToPlayer(profile: PlayerProfile, player: Player) { 59 | val value = profileGetter(profile) 60 | playerSetter(player, value) 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/main/kotlin/me/ebonjaeger/perworldinventory/listener/player/PlayerGameModeChangeListener.kt: -------------------------------------------------------------------------------- 1 | package me.ebonjaeger.perworldinventory.listener.player 2 | 3 | import me.ebonjaeger.perworldinventory.ConsoleLogger 4 | import me.ebonjaeger.perworldinventory.GroupManager 5 | import me.ebonjaeger.perworldinventory.configuration.PluginSettings 6 | import me.ebonjaeger.perworldinventory.configuration.Settings 7 | import me.ebonjaeger.perworldinventory.data.ProfileManager 8 | import me.ebonjaeger.perworldinventory.event.Cause 9 | import me.ebonjaeger.perworldinventory.event.InventoryLoadEvent 10 | import me.ebonjaeger.perworldinventory.permission.PlayerPermission 11 | import org.bukkit.Bukkit 12 | import org.bukkit.event.EventHandler 13 | import org.bukkit.event.EventPriority 14 | import org.bukkit.event.Listener 15 | import org.bukkit.event.player.PlayerGameModeChangeEvent 16 | import javax.inject.Inject 17 | 18 | class PlayerGameModeChangeListener @Inject constructor(private val groupManager: GroupManager, 19 | private val profileManager: ProfileManager, 20 | private val settings: Settings) : Listener { 21 | 22 | @EventHandler(priority = EventPriority.MONITOR) 23 | fun onPlayerChangedGameMode(event: PlayerGameModeChangeEvent) { 24 | if (event.isCancelled || !settings.getProperty(PluginSettings.SEPARATE_GM_INVENTORIES)) { 25 | return 26 | } 27 | 28 | val player = event.player 29 | val group = groupManager.getGroupFromWorld(player.world.name) 30 | 31 | ConsoleLogger.fine("onPlayerChangedGameMode: '${player.name}' changing GameModes") 32 | ConsoleLogger.debug("onPlayerChangedGameMode: newGameMode: ${event.newGameMode}, group: $group") 33 | 34 | // Save the current profile 35 | profileManager.addPlayerProfile(player, group, player.gameMode) 36 | 37 | // Check if the player can bypass the inventory switch 38 | if (!settings.getProperty(PluginSettings.DISABLE_BYPASS) && 39 | player.hasPermission(PlayerPermission.BYPASS_GAMEMODE.getNode())) { 40 | ConsoleLogger.debug("onPlayerChangedGameMode: '${player.name}' is bypassing the inventory switch") 41 | return 42 | } 43 | 44 | // Create and call an InventoryLoadEvent. If it isn't cancelled, load 45 | // the player's new profile 46 | val loadEvent = InventoryLoadEvent(player, Cause.GAMEMODE_CHANGE, player.gameMode, event.newGameMode, group) 47 | Bukkit.getPluginManager().callEvent(loadEvent) 48 | if (!loadEvent.isCancelled) { 49 | ConsoleLogger.fine("onPlayerChangedGameMode: Loading profile for '${player.name}'") 50 | profileManager.getPlayerData(player, group, event.newGameMode) 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/main/kotlin/me/ebonjaeger/perworldinventory/api/PerWorldInventoryAPI.kt: -------------------------------------------------------------------------------- 1 | package me.ebonjaeger.perworldinventory.api 2 | 3 | import me.ebonjaeger.perworldinventory.Group 4 | import me.ebonjaeger.perworldinventory.GroupManager 5 | import me.ebonjaeger.perworldinventory.PerWorldInventory 6 | import me.ebonjaeger.perworldinventory.configuration.PluginSettings 7 | import me.ebonjaeger.perworldinventory.configuration.Settings 8 | import me.ebonjaeger.perworldinventory.data.ProfileManager 9 | import javax.inject.Inject 10 | 11 | /** 12 | * This class is for other plugins to interact with some parts of PWI. 13 | */ 14 | class PerWorldInventoryAPI @Inject constructor(private val plugin: PerWorldInventory, 15 | private val groupManager: GroupManager, 16 | private val profileManager: ProfileManager, 17 | private val settings: Settings) 18 | { 19 | 20 | /** 21 | * Check if two worlds are a part of the same [Group] and thus can share 22 | * inventories. If one of the groups is not configured in the worlds.yml, 23 | * this method will return true if the option for sharing inventories 24 | * between non-configured worlds is true in the config.yml file. 25 | * 26 | * @param first The name of the first world 27 | * @param second The name of the second world 28 | * @return True if both worlds are in the same group, or one group is not 29 | * configured and the option to share is set to true. 30 | */ 31 | fun canWorldsShare(first: String, second: String): Boolean 32 | { 33 | val firstGroup = groupManager.getGroupFromWorld(first) 34 | val secondGroup = groupManager.getGroupFromWorld(second) 35 | 36 | return if (!firstGroup.configured || !secondGroup.configured) 37 | { 38 | firstGroup.containsWorld(second) || settings.getProperty(PluginSettings.SHARE_IF_UNCONFIGURED) 39 | } else 40 | { 41 | firstGroup.containsWorld(second) 42 | } 43 | } 44 | 45 | /** 46 | * Get a [Group] by name. If no group with that name exists, the method 47 | * will return null. See [GroupManager.getGroup] 48 | * 49 | * @param name The name of the group 50 | * @return The group if it exists, or null 51 | */ 52 | fun getGroup(name: String) = 53 | groupManager.getGroup(name) 54 | 55 | /** 56 | * Get the [Group] that a given world is in. This method will always return 57 | * a group, even if a new one has to be created. 58 | * 59 | * See [GroupManager.getGroupFromWorld] 60 | * 61 | * @param worldName The name of the world 62 | * @return The group the world is in 63 | */ 64 | fun getGroupFromWorld(worldName: String) = 65 | groupManager.getGroupFromWorld(worldName) 66 | } 67 | -------------------------------------------------------------------------------- /src/main/kotlin/me/ebonjaeger/perworldinventory/Utils.kt: -------------------------------------------------------------------------------- 1 | package me.ebonjaeger.perworldinventory 2 | 3 | /** 4 | * Object that holds utility methods. 5 | */ 6 | object Utils 7 | { 8 | 9 | /** 10 | * Check if a server's version is the same as a given version or higher. 11 | * 12 | * @param version The server's version 13 | * @param major The major version number 14 | * @param minor The minor version number 15 | * @param patch The patch version number 16 | * @return True if the server is running the same version or newer 17 | */ 18 | fun checkServerVersion(version: String, major: Int, minor: Int, patch: Int): Boolean 19 | { 20 | val versionNum = version.substring(version.indexOf('.') - 1, 21 | version.length - 1).trim() 22 | val parts = versionNum.split(".") 23 | 24 | try 25 | { 26 | if (parts[0].toInt() >= major) 27 | { 28 | return if (parts[1].toInt() == minor) 29 | { 30 | if (parts.size == 2) 31 | { 32 | true 33 | } else 34 | { 35 | parts[2].toInt() >= patch 36 | } 37 | } else 38 | { 39 | parts[1].toInt() > minor 40 | } 41 | } 42 | } catch (ex: NumberFormatException) 43 | { 44 | return false 45 | } 46 | 47 | return false 48 | } 49 | 50 | /** 51 | * Get the header to display at the top of the `worlds.yml` 52 | * configuration file. This is so we can easily save the header 53 | * when the file is modified using in-game commands without having 54 | * to clutter up another class or method. 55 | */ 56 | fun getWorldsConfigHeader(): String 57 | { 58 | return "# In this file you define your groups and the worlds in them.\n" + 59 | "# Follow this format:\n" + 60 | "# groups:\n" + 61 | "# default:\n" + 62 | "# worlds:\n" + 63 | "# - world\n" + 64 | "# - world_nether\n" + 65 | "# - world_the_end\n" + 66 | "# default-gamemode: SURVIVAL\n" + 67 | "# creative:\n" + 68 | "# worlds:\n" + 69 | "# - creative\n" + 70 | "# default-gamemode: CREATIVE\n" + 71 | "#\n" + 72 | "# 'default' and 'creative' are the names of the groups\n" + 73 | "# worlds: is a list of all worlds in the group\n" + 74 | "# If you have 'manage-gamemodes' set to true in the main config, the server\n" + 75 | "# will use the 'default-gamemode' here to know what gamemode to put users in." 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/main/kotlin/me/ebonjaeger/perworldinventory/configuration/PluginSettings.kt: -------------------------------------------------------------------------------- 1 | package me.ebonjaeger.perworldinventory.configuration 2 | 3 | import ch.jalu.configme.Comment 4 | import ch.jalu.configme.SettingsHolder 5 | import ch.jalu.configme.properties.Property 6 | import ch.jalu.configme.properties.PropertyInitializer.newProperty 7 | import me.ebonjaeger.perworldinventory.LogLevel 8 | 9 | /** 10 | * Object to hold settings for general plugin operation. 11 | */ 12 | object PluginSettings : SettingsHolder 13 | { 14 | 15 | @JvmField 16 | @Comment("Set the level of debug messages shown by PWI.", 17 | "INFO: Print general messages", 18 | "FINE: Print more detailed messages about what the plugin is doing", 19 | "DEBUG: Print detailed messages about everything") 20 | val LOGGING_LEVEL: Property? = newProperty(LogLevel::class.java, "logging-level", LogLevel.INFO) 21 | 22 | @JvmField 23 | @Comment( 24 | "If true, the server will change player's gamemodes when entering a world", 25 | "The gamemode set is configured in the worlds.yml file") 26 | val MANAGE_GAMEMODES: Property? = newProperty("manage-gamemodes", false) 27 | 28 | @JvmField 29 | @Comment("If true, players will have different inventories for each gamemode") 30 | val SEPARATE_GM_INVENTORIES: Property? = newProperty("separate-gamemode-inventories", true) 31 | 32 | @JvmField 33 | @Comment("If true, any worlds that are not in the worlds.yml configuration file will share the same inventory") 34 | val SHARE_IF_UNCONFIGURED: Property? = newProperty("share-if-unconfigured", false) 35 | 36 | @JvmField 37 | @Comment("True if PWI should set the respawn world when a player dies") 38 | val MANAGE_DEATH_RESPAWN: Property? = newProperty("manage-death-respawn", false) 39 | 40 | @JvmField 41 | @Comment( 42 | "Attempt to figure out which world a player last logged off in", 43 | "and save/load the correct data if that world is different.", 44 | "REQUIRES MC 1.9.2 OR NEWER") 45 | val LOAD_DATA_ON_JOIN: Property? = newProperty("load-data-on-join", false) 46 | 47 | @JvmField 48 | @Comment( 49 | "Disables bypass regardless of permission", 50 | "Defaults to false") 51 | val DISABLE_BYPASS: Property? = newProperty("disable-bypass", false) 52 | 53 | @JvmField 54 | @Comment("Set the duration in minutes for player profile information to be cached") 55 | val CACHE_DURATION: Property? = newProperty("cache-duration", 10) 56 | 57 | @JvmField 58 | @Comment("Set the maximum number of player profiles that can be cached at any given time") 59 | val CACHE_MAX_LIMIT: Property? = newProperty("cache-maximum-limit", 1000) 60 | 61 | @JvmField 62 | @Comment("Disables the nagging message when a world is created on the fly", 63 | "Intended for users who know what they're doing, and don't need to have worlds configured") 64 | val DISABLE_NAG: Property? = newProperty("disable-nag-message", false) 65 | } 66 | -------------------------------------------------------------------------------- /src/main/kotlin/me/ebonjaeger/perworldinventory/ConsoleLogger.kt: -------------------------------------------------------------------------------- 1 | package me.ebonjaeger.perworldinventory 2 | 3 | import java.util.logging.Logger 4 | 5 | /** 6 | * Static logger. 7 | */ 8 | object ConsoleLogger 9 | { 10 | 11 | private var logger: Logger? = null 12 | private var logLevel: LogLevel = LogLevel.INFO 13 | 14 | fun setLogger(logger: Logger) 15 | { 16 | ConsoleLogger.logger = logger 17 | } 18 | 19 | fun setLogLevel(logLevel: LogLevel) 20 | { 21 | ConsoleLogger.logLevel = logLevel 22 | } 23 | 24 | /** 25 | * Log a SEVERE message. 26 | * 27 | * @param message The message to log 28 | */ 29 | fun severe(message: String) 30 | { 31 | logger?.severe(message) 32 | } 33 | 34 | /** 35 | * Log a SEVERE message with the cause of an error. 36 | * 37 | * @param message The message to log 38 | * @param cause The cause of an error 39 | */ 40 | fun severe(message: String, cause: Throwable) 41 | { 42 | logger?.severe(message + " " + formatThrowable(cause)) 43 | cause.printStackTrace() 44 | } 45 | 46 | /** 47 | * Log a WARN message. 48 | * 49 | * @param message The message to log 50 | */ 51 | fun warning(message: String) 52 | { 53 | logger?.warning(message) 54 | } 55 | 56 | /** 57 | * Log a WARN message with the cause of an error. 58 | * 59 | * @param message The message to log 60 | * @param cause The cause of an error 61 | */ 62 | fun warning(message: String, cause: Throwable) 63 | { 64 | logger?.warning(message + " " + formatThrowable(cause)) 65 | cause.printStackTrace() 66 | } 67 | 68 | /** 69 | * Log an INFO message. 70 | * 71 | * @param message The message to log 72 | */ 73 | fun info(message: String) 74 | { 75 | logger?.info(message) 76 | } 77 | 78 | /** 79 | * Log a FINE message if enabled. 80 | *

81 | * Implementation note: this logs a message on INFO level because 82 | * levels below INFO are disabled by Bukkit/Spigot. 83 | * 84 | * @param message The message to log 85 | */ 86 | fun fine(message: String) 87 | { 88 | if (logLevel.includes(LogLevel.FINE)) 89 | { 90 | logger?.info("[FINE] $message") 91 | } 92 | } 93 | 94 | /** 95 | * Log a DEBUG message if enabled. 96 | *

97 | * Implementation note: this logs a message on INFO level and prefixes it with "DEBUG" because 98 | * levels below INFO are disabled by Bukkit/Spigot. 99 | * 100 | * @param message The message to log 101 | */ 102 | fun debug(message: String) 103 | { 104 | if (logLevel.includes(LogLevel.DEBUG)) 105 | { 106 | logger?.info("[DEBUG] $message") 107 | } 108 | } 109 | 110 | private fun formatThrowable(throwable: Throwable): String 111 | { 112 | return "[" + throwable.javaClass.simpleName + "] " + throwable.message 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/main/kotlin/me/ebonjaeger/perworldinventory/serialization/PotionSerializer.kt: -------------------------------------------------------------------------------- 1 | package me.ebonjaeger.perworldinventory.serialization 2 | 3 | import com.dumptruckman.bukkit.configuration.util.SerializationHelper 4 | import me.ebonjaeger.perworldinventory.ConsoleLogger 5 | import net.minidev.json.JSONArray 6 | import net.minidev.json.JSONObject 7 | import org.bukkit.potion.PotionEffect 8 | import org.bukkit.potion.PotionEffectType 9 | 10 | object PotionSerializer 11 | { 12 | 13 | /** 14 | * Serialize a Collection of PotionEffects. The 15 | * effects are serialized using their ConfigurationSerialization method. 16 | * 17 | * @param effects The PotionEffects to serialize 18 | * @return The serialized PotionEffects 19 | */ 20 | @Suppress("UNCHECKED_CAST") // We know that #serialize will give us a Map for a ConfigurationSerializable object 21 | fun serialize(effects: MutableCollection): List> 22 | { 23 | val list = mutableListOf>() 24 | 25 | effects.forEach { effect -> 26 | val map = SerializationHelper.serialize(effect) as Map 27 | list.add(map) 28 | } 29 | 30 | return list 31 | } 32 | 33 | /** 34 | * Return a Collection of PotionEffects from a given JsonArray. 35 | * 36 | * @param array The serialized PotionEffects 37 | * @return The PotionEffects 38 | */ 39 | fun deserialize(array: JSONArray): MutableCollection 40 | { 41 | val effects = mutableListOf() 42 | 43 | for (i in 0 until array.size) 44 | { 45 | val obj = array[i] as JSONObject 46 | 47 | val effect = if (obj.containsKey("==")) { // Object contains the classname as a key 48 | val map = obj as Map 49 | SerializationHelper.deserialize(map) as PotionEffect 50 | } else { // Likely older data, try to use the old long way 51 | deserializeLongWay(obj) 52 | } 53 | 54 | if (effect != null) { // Potion effect de-serialized correctly 55 | effects.add(effect) 56 | } 57 | } 58 | 59 | return effects 60 | } 61 | 62 | @Deprecated("Kept for backwards compatibility, and may be removed in a later MC version") 63 | private fun deserializeLongWay(obj: JSONObject): PotionEffect? { 64 | val type = PotionEffectType.getByName(obj["type"] as String) 65 | 66 | if (type == null) { 67 | ConsoleLogger.warning("Unable to get potion effect for type: ${obj["type"]}") 68 | return null 69 | } 70 | 71 | val amplifier = obj["amp"] as Int 72 | val duration = obj["duration"] as Int 73 | val ambient = obj["ambient"] as Boolean 74 | val particles = obj["particles"] as Boolean 75 | 76 | // Randomly in 1.13, color stopped being a thing, and now icon is. Yay. 77 | val hasIcon = if (obj.containsKey("hasIcon")) obj["hasIcon"] as Boolean else false 78 | 79 | return PotionEffect(type, duration, amplifier, ambient, particles, hasIcon) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/main/resources/config.yml: -------------------------------------------------------------------------------- 1 | ########################################## 2 | # PerWorldInventory configuration file # 3 | ########################################## 4 | 5 | # If true, the server will change player's gamemodes when entering a world 6 | # The gamemode set is configured in the worlds.yml file 7 | manage-gamemodes: false 8 | 9 | # If true, players will have different inventories for each gamemode 10 | separate-gamemode-inventories: true 11 | 12 | # If true, any worlds that are not in the worlds.yml configuration file will share the same inventory 13 | share-if-unconfigured: false 14 | 15 | # True if PWI should set the respawn world when a player dies 16 | manage-death-respawn: false 17 | 18 | # Disables the nagging message when a world is created on the fly 19 | # Intended for users who know what they're doing, and don't need to have worlds configured 20 | disable-nag-message: false 21 | 22 | metrics: 23 | # Choose whether or not to enable metrics sending. 24 | # See https://bstats.org/getting-started for details. 25 | enable: true 26 | # Send the number of configured groups. 27 | # No group names will be sent! 28 | send-number-of-groups: true 29 | # Send the total number of worlds on the server. 30 | send-number-of-worlds: true 31 | 32 | # All settings for players are here: 33 | player: 34 | # Save and Load players' economy balances. Requires Vault! 35 | economy: false 36 | # Load players' ender chests 37 | ender-chest: true 38 | # Load players' inventory 39 | inventory: true 40 | # All options for player stats are here: 41 | stats: 42 | # Load if a player is able to fly 43 | can-fly: true 44 | # Load the player's display name 45 | display-name: false 46 | # Load a player's exhaustion level 47 | exhaustion: true 48 | # Load how much exp a player has 49 | exp: true 50 | # Load a player's hunger level 51 | food: true 52 | # Load if a player is flying 53 | flying: true 54 | # Load how much health a player has 55 | health: true 56 | # Load what level the player is 57 | level: true 58 | # Load all the potion effects of the player 59 | potion-effects: true 60 | # Load the saturation level of the player 61 | saturation: true 62 | # Load a player's fall distance 63 | fall-distance: true 64 | # Load the fire ticks a player has 65 | fire-ticks: true 66 | # Load the maximum amount of air a player can have 67 | max-air: true 68 | # Load the current remaining air a player has 69 | remaining-air: true 70 | 71 | # Attempt to figure out which world a player last logged off in 72 | # and save/load the correct data if that world is different. 73 | # REQUIRES MC 1.9.2 OR NEWER 74 | load-data-on-join: false 75 | 76 | # Set the level of debug messages shown by PWI. 77 | # INFO: Print general messages 78 | # FINE: Print more detailed messages about what the plugin is doing 79 | # DEBUG: Print detailed messages about everything 80 | logging-level: 'INFO' 81 | 82 | # Disables bypass regardless of permission 83 | # Defaults to false 84 | disable-bypass: false 85 | 86 | # Set the duration in minutes for player profile information to be cached 87 | cache-duration: 10 88 | 89 | # Set the maximum number of player profiles that can be cached at any given time 90 | cache-maximum-limit: 1000 91 | -------------------------------------------------------------------------------- /src/main/kotlin/me/ebonjaeger/perworldinventory/Group.kt: -------------------------------------------------------------------------------- 1 | package me.ebonjaeger.perworldinventory 2 | 3 | import org.bukkit.GameMode 4 | import java.util.* 5 | 6 | /** 7 | * A group of worlds, typically defined in the worlds.yml file. 8 | * Each Group has a name, and should have a list of worlds in that group, as well as 9 | * a default GameMode. 10 | * 11 | * This data class can be serialized/deserialized to and from Json directly. 12 | * 13 | * @property name The name of the group 14 | * @property worlds A Set of world names 15 | * @property defaultGameMode The default GameMode for players in this group 16 | * @property respawnWorld The world that players will spawn in when they die 17 | */ 18 | data class Group(val name: String, 19 | val worlds: MutableSet, 20 | val defaultGameMode: GameMode, 21 | var respawnWorld: String?) 22 | { 23 | 24 | /** 25 | * If this is true, then this group was configured in the `worlds.yml` 26 | * file. If the group was created on the fly due to a world not being in 27 | * a group, then this will be false. 28 | */ 29 | var configured: Boolean = false 30 | 31 | /** 32 | * Get whether this group contains a world with a given name. 33 | * 34 | * @param toCheck The name of the world to check for 35 | * 36 | * @return True if the world is in this group 37 | */ 38 | fun containsWorld(toCheck: String): Boolean 39 | = worlds.contains(toCheck) 40 | 41 | /** 42 | * Add a list of worlds to this group. 43 | * 44 | * @param worlds A list of the worlds to add 45 | */ 46 | fun addWorlds(worlds: Collection) 47 | { 48 | this.worlds.addAll(worlds) 49 | } 50 | 51 | /** 52 | * Add a world to this group. 53 | * 54 | * @param world The name of the world to add 55 | */ 56 | fun addWorld(world: String) 57 | { 58 | worlds.add(world) 59 | } 60 | 61 | /** 62 | * Remove a world from this group. 63 | * 64 | * @param world The name of the world to remove 65 | */ 66 | fun removeWorld(world: String) 67 | { 68 | worlds.remove(world) 69 | } 70 | 71 | override fun equals(other: Any?): Boolean 72 | { 73 | if (this === other) return true 74 | if (other !is Group) return false 75 | 76 | return Objects.equals(name, other.name) && 77 | Objects.equals(worlds, other.worlds) && 78 | Objects.equals(defaultGameMode, other.defaultGameMode) && 79 | Objects.equals(respawnWorld, other.respawnWorld) 80 | } 81 | 82 | override fun hashCode(): Int 83 | { 84 | var result = name.hashCode() 85 | result = 31 * result + defaultGameMode.hashCode() 86 | return result 87 | } 88 | 89 | override fun toString(): String 90 | { 91 | return "Group{" + 92 | "name='$name'" + 93 | ", worlds=$worlds" + 94 | ", defaultGameMode='${defaultGameMode.toString().toLowerCase()}'" + 95 | ", respawnWorld='$respawnWorld'" + 96 | ", isConfigured='$configured'" + 97 | "}" 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/main/kotlin/me/ebonjaeger/perworldinventory/conversion/ConvertService.kt: -------------------------------------------------------------------------------- 1 | package me.ebonjaeger.perworldinventory.conversion 2 | 3 | import ch.jalu.injector.annotations.NoMethodScan 4 | import com.onarandombox.multiverseinventories.MultiverseInventories 5 | import com.onarandombox.multiverseinventories.WorldGroup 6 | import me.ebonjaeger.perworldinventory.ConsoleLogger 7 | import me.ebonjaeger.perworldinventory.GroupManager 8 | import me.ebonjaeger.perworldinventory.PerWorldInventory 9 | import me.ebonjaeger.perworldinventory.initialization.DataDirectory 10 | import me.ebonjaeger.perworldinventory.service.BukkitService 11 | import org.bukkit.ChatColor 12 | import org.bukkit.GameMode 13 | import org.bukkit.command.CommandSender 14 | import org.bukkit.command.ConsoleCommandSender 15 | import org.bukkit.entity.Player 16 | import org.bukkit.plugin.PluginManager 17 | import java.io.File 18 | import javax.inject.Inject 19 | 20 | /** 21 | * Initiates conversion tasks. 22 | */ 23 | @NoMethodScan 24 | class ConvertService @Inject constructor(private val plugin: PerWorldInventory, 25 | private val bukkitService: BukkitService, 26 | private val groupManager: GroupManager, 27 | private val pluginManager: PluginManager, 28 | @DataDirectory private val dataDirectory: File) { 29 | 30 | private var converting = false 31 | private var sender: CommandSender? = null 32 | 33 | fun isConverting(): Boolean { 34 | return converting 35 | } 36 | 37 | fun beginConverting(sender: CommandSender, mvInventory: MultiverseInventories) { 38 | val offlinePlayers = bukkitService.getOfflinePlayers() 39 | 40 | if (isConverting()) { 41 | return 42 | } 43 | 44 | this.sender = sender 45 | 46 | if (sender !is ConsoleCommandSender) { // No need to send a message to console when console did the command 47 | ConsoleLogger.info("Beginning conversion from MultiVerse-Inventories.") 48 | } 49 | 50 | converting = true 51 | 52 | val groups = mvInventory.groupManager.groups 53 | convertGroups(groups) 54 | 55 | val task = ConvertTask(this, groupManager, sender, offlinePlayers, groups, dataDirectory) 56 | task.runTaskTimerAsynchronously(plugin, 0, 20) 57 | } 58 | 59 | fun finishConversion(converted: Int) { 60 | converting = false 61 | 62 | val mvInventory = pluginManager.getPlugin("Multiverse-Inventories") 63 | if (mvInventory != null && pluginManager.isPluginEnabled(mvInventory)) { 64 | pluginManager.disablePlugin(mvInventory) 65 | } 66 | 67 | ConsoleLogger.info("Data conversion has been completed! Converted $converted profiles.") 68 | if (sender != null && sender is Player) { 69 | if ((sender as Player).isOnline) { 70 | (sender as Player).sendMessage("${ChatColor.GREEN}» ${ChatColor.GRAY}Data conversion has been completed!") 71 | } 72 | } 73 | } 74 | 75 | private fun convertGroups(groups: List) { 76 | groups.forEach { group -> 77 | // Ensure that the group exists first, otherwise you get nulls down the road 78 | val pwiGroup = groupManager.getGroup(group.name) 79 | val worlds = group.worlds 80 | 81 | if (pwiGroup == null) { 82 | groupManager.addGroup(group.name, worlds, GameMode.SURVIVAL, true) 83 | } else { 84 | pwiGroup.addWorlds(worlds) 85 | } 86 | } 87 | 88 | groupManager.saveGroups() 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/main/kotlin/me/ebonjaeger/perworldinventory/configuration/PlayerSettings.kt: -------------------------------------------------------------------------------- 1 | package me.ebonjaeger.perworldinventory.configuration 2 | 3 | import ch.jalu.configme.Comment 4 | import ch.jalu.configme.SettingsHolder 5 | import ch.jalu.configme.configurationdata.CommentsConfiguration 6 | import ch.jalu.configme.properties.Property 7 | import ch.jalu.configme.properties.PropertyInitializer.newProperty 8 | 9 | /** 10 | * Object to hold settings about what to change on a player when they 11 | * teleport to a different world group. 12 | */ 13 | object PlayerSettings : SettingsHolder 14 | { 15 | 16 | @JvmField 17 | @Comment("Save and load players' economy balances. Requires Vault!") 18 | val USE_ECONOMY: Property? = newProperty("player.economy", false) 19 | 20 | @JvmField 21 | @Comment("Load players' ender chests") 22 | val LOAD_ENDER_CHEST: Property? = newProperty("player.ender-chest", true) 23 | 24 | @JvmField 25 | @Comment("Load players' inventory") 26 | val LOAD_INVENTORY: Property? = newProperty("player.inventory", true) 27 | 28 | @JvmField 29 | @Comment("Load if a player is able to fly") 30 | val LOAD_ALLOW_FLIGHT: Property? = newProperty("player.stats.can-fly", true) 31 | 32 | @JvmField 33 | @Comment("Load the player's display name") 34 | val LOAD_DISPLAY_NAME: Property? = newProperty("player.stats.display-name", false) 35 | 36 | @JvmField 37 | @Comment("Load a player's exhaustion level") 38 | val LOAD_EXHAUSTION: Property? = newProperty("player.stats.exhaustion", true) 39 | 40 | @JvmField 41 | @Comment("Load how much exp a player has") 42 | val LOAD_EXP: Property? = newProperty("player.stats.exp", true) 43 | 44 | @JvmField 45 | @Comment("Load a player's hunger level") 46 | val LOAD_HUNGER: Property? = newProperty("player.stats.food", true) 47 | 48 | @JvmField 49 | @Comment("Load if a player is flying") 50 | val LOAD_FLYING: Property? = newProperty("player.stats.flying", true) 51 | 52 | @JvmField 53 | @Comment("Load how much health a player has") 54 | val LOAD_HEALTH: Property? = newProperty("player.stats.health", true) 55 | 56 | @JvmField 57 | @Comment("Load what level the player is") 58 | val LOAD_LEVEL: Property? = newProperty("player.stats.level", true) 59 | 60 | @JvmField 61 | @Comment("Load all the potion effects of the player") 62 | val LOAD_POTION_EFFECTS: Property? = newProperty("player.stats.potion-effects", true) 63 | 64 | @JvmField 65 | @Comment("Load the saturation level of the player") 66 | val LOAD_SATURATION: Property? = newProperty("player.stats.saturation", true) 67 | 68 | @JvmField 69 | @Comment("Load a player's fall distance") 70 | val LOAD_FALL_DISTANCE: Property? = newProperty("player.stats.fall-distance", true) 71 | 72 | @JvmField 73 | @Comment("Load the fire ticks a player has") 74 | val LOAD_FIRE_TICKS: Property? = newProperty("player.stats.fire-ticks", true) 75 | 76 | @JvmField 77 | @Comment("Load the maximum amount of air a player can have") 78 | val LOAD_MAX_AIR: Property? = newProperty("player.stats.max-air", true) 79 | 80 | @JvmField 81 | @Comment("Load the current remaining air a player has") 82 | val LOAD_REMAINING_AIR: Property? = newProperty("player.stats.remaining-air", true) 83 | 84 | override fun registerComments(conf: CommentsConfiguration) { 85 | conf.setComment("player", "All settings for players are here:") 86 | conf.setComment("player.stats", "All options for player stats are here:") 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/main/kotlin/me/ebonjaeger/perworldinventory/service/EconomyService.kt: -------------------------------------------------------------------------------- 1 | package me.ebonjaeger.perworldinventory.service 2 | 3 | import ch.jalu.injector.annotations.NoFieldScan 4 | import me.ebonjaeger.perworldinventory.ConsoleLogger 5 | import me.ebonjaeger.perworldinventory.configuration.PlayerSettings 6 | import me.ebonjaeger.perworldinventory.configuration.Settings 7 | import net.milkbowl.vault.economy.Economy 8 | import org.bukkit.Server 9 | import org.bukkit.entity.Player 10 | import javax.annotation.PostConstruct 11 | import javax.inject.Inject 12 | 13 | @NoFieldScan 14 | class EconomyService @Inject constructor(private val server: Server, 15 | private val settings: Settings) { 16 | 17 | /** 18 | * Economy from Vault, if Vault is present on the server. Null if Vault is not 19 | * installed, we failed to hook into it, or PWI is configured not to use economy. 20 | */ 21 | private var economy: Economy? = null 22 | 23 | fun getBalance(player: Player): Double { 24 | return if (economy != null) { 25 | economy!!.getBalance(player) 26 | } else { 27 | 0.0 28 | } 29 | } 30 | 31 | /** 32 | * Set a player's balance by calculating the difference of the old and new amounts. 33 | * This avoids having to set a player's balance to 0 each time. If for some reason 34 | * the difference ends up being a negative number and the economy plugin in use 35 | * does not permit negative balances, the player's balance will be set to 0. 36 | * 37 | * @param player The [Player] in the transaction. 38 | * @param newBalance The end balance that the player should end up with. 39 | */ 40 | fun setNewBalance(player: Player, newBalance: Double) { 41 | val econ = economy 42 | if (econ != null) 43 | { 44 | val oldBalance = econ.getBalance(player) 45 | 46 | if (newBalance < oldBalance) 47 | { 48 | // If the new bal is less than old bal, withdraw the difference 49 | val response = econ.withdrawPlayer(player, (oldBalance - newBalance)) 50 | ConsoleLogger.debug("EconomyService: Withdrawing $${(oldBalance - newBalance)} from '${player.name}'") 51 | 52 | if (!response.transactionSuccess()) 53 | { 54 | if (response.errorMessage.equals("Loan was not permitted", true)) 55 | { 56 | ConsoleLogger.warning("[ECON] Negative balances are not permitted. Setting balance for '${player.name}'" + 57 | " to 0 in '${player.location.world?.name}'") 58 | econ.withdrawPlayer(player, oldBalance) 59 | } else 60 | { 61 | ConsoleLogger.debug("EconomyService: Withdrawing funds from ${player.name} failed: ${response.errorMessage}") 62 | } 63 | } 64 | } else 65 | { 66 | // If the new bal is greater than old bal, deposit the difference 67 | econ.depositPlayer(player, (newBalance - oldBalance)) 68 | ConsoleLogger.debug("EconomyService: Depositing $${(newBalance - oldBalance)} from '${player.name}'") 69 | } 70 | } 71 | } 72 | 73 | fun withdrawFromPlayer(player: Player) { 74 | if (economy != null) { 75 | val amount = economy?.getBalance(player) ?: 0.0 76 | economy?.withdrawPlayer(player, amount) 77 | 78 | ConsoleLogger.fine("EconomyService: Withdrew $$amount from '${player.name}'") 79 | } 80 | } 81 | 82 | @PostConstruct 83 | fun tryLoadEconomy() { 84 | if (settings.getProperty(PlayerSettings.USE_ECONOMY) && server.pluginManager.getPlugin("Vault") != null) { 85 | ConsoleLogger.info("Vault found! Hooking into it...") 86 | 87 | val rsp = server.servicesManager.getRegistration(Economy::class.java) 88 | if (rsp != null) { 89 | economy = rsp.provider 90 | ConsoleLogger.info("Hooked into Vault!") 91 | } else { 92 | ConsoleLogger.warning("Unable to hook into Vault!") 93 | } 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/main/kotlin/me/ebonjaeger/perworldinventory/serialization/ItemSerializer.kt: -------------------------------------------------------------------------------- 1 | package me.ebonjaeger.perworldinventory.serialization 2 | 3 | import com.dumptruckman.bukkit.configuration.util.SerializationHelper 4 | import me.ebonjaeger.perworldinventory.ConsoleLogger 5 | import net.minidev.json.JSONObject 6 | import org.bukkit.Bukkit 7 | import org.bukkit.Material 8 | import org.bukkit.inventory.ItemStack 9 | import org.bukkit.inventory.meta.SkullMeta 10 | import org.bukkit.util.io.BukkitObjectInputStream 11 | import org.yaml.snakeyaml.external.biz.base64Coder.Base64Coder 12 | import java.io.ByteArrayInputStream 13 | import java.io.IOException 14 | 15 | object ItemSerializer 16 | { 17 | 18 | private const val AIR = "AIR" 19 | 20 | /** 21 | * Serialize an ItemStack to a JsonObject. 22 | * 23 | * @param item The item to serialize 24 | * @param index The position in the inventory 25 | * @return A JsonObject with the item and its index 26 | */ 27 | fun serialize(item: ItemStack?, index: Int): Map 28 | { 29 | val obj = linkedMapOf() 30 | obj["index"] = index 31 | 32 | // Items in inventories can be null. Because why wouldn't they be. 33 | var checkedItem = item 34 | if (checkedItem == null) { 35 | checkedItem = ItemStack(Material.AIR) 36 | } 37 | 38 | /* 39 | * Check to see if the item is a skull with a null owner. 40 | * This is because some people are getting skulls with null owners, which causes Spigot to throw an error 41 | * when it tries to serialize the item. 42 | */ 43 | if (checkedItem.type == Material.PLAYER_HEAD) 44 | { 45 | val meta = checkedItem.itemMeta as SkullMeta 46 | if (meta.hasOwner() && (meta.owningPlayer == null)) 47 | { 48 | checkedItem.itemMeta = Bukkit.getServer().itemFactory.getItemMeta(Material.PLAYER_HEAD) 49 | } 50 | } 51 | 52 | obj["item"] = SerializationHelper.serialize(checkedItem) 53 | 54 | return obj 55 | } 56 | 57 | /** 58 | * Get an ItemStack from a JsonObject. 59 | * 60 | * @param obj The Json to read 61 | * @param format The data format being used. Refer to {@link PlayerSerializer#serialize(PWIPlayer)} 62 | * @return The deserialized item stack 63 | */ 64 | @Suppress("UNCHECKED_CAST") // Reading a map we created; it's safe to assume the Map types 65 | fun deserialize(obj: JSONObject, format: Int): ItemStack 66 | { 67 | when (format) 68 | { 69 | 0 -> throw IllegalArgumentException("Old data format is not supported!") 70 | 1, 2 -> 71 | { 72 | val encoded = obj["item"] as String 73 | return decodeItem(encoded) 74 | } 75 | 3 -> 76 | { 77 | return if (obj["item"] is Map<*, *>) 78 | { 79 | val item = obj["item"] as Map 80 | SerializationHelper.deserialize(item) as ItemStack 81 | } else 82 | { 83 | ItemStack(Material.AIR) 84 | } 85 | } 86 | else -> 87 | { 88 | throw IllegalArgumentException("Unknown data format '$format'.") 89 | } 90 | } 91 | } 92 | 93 | private fun decodeItem(encoded: String): ItemStack 94 | { 95 | if (encoded == AIR) 96 | { 97 | return ItemStack(Material.AIR) 98 | } 99 | 100 | try 101 | { 102 | ByteArrayInputStream(Base64Coder.decodeLines(encoded)).use { byteStream -> 103 | BukkitObjectInputStream(byteStream).use { return it.readObject() as ItemStack } 104 | } 105 | } catch (ex: IOException) 106 | { 107 | ConsoleLogger.severe("Unable to deserialize an item:", ex) 108 | return ItemStack(Material.AIR) 109 | } catch (ex: ClassNotFoundException) 110 | { 111 | ConsoleLogger.severe("Unable to deserialize an item:", ex) 112 | return ItemStack(Material.AIR) 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/test/kotlin/me/ebonjaeger/perworldinventory/TestHelper.kt: -------------------------------------------------------------------------------- 1 | package me.ebonjaeger.perworldinventory 2 | 3 | import io.mockk.every 4 | import io.mockk.mockkClass 5 | import io.mockk.mockkStatic 6 | import io.mockk.slot 7 | import me.ebonjaeger.perworldinventory.serialization.ItemMetaTestImpl 8 | import org.bukkit.Bukkit 9 | import org.bukkit.GameMode 10 | import org.bukkit.Material 11 | import org.bukkit.Server 12 | import org.bukkit.inventory.ItemFactory 13 | import org.bukkit.inventory.meta.ItemMeta 14 | import java.io.File 15 | import java.net.URI 16 | import java.net.URISyntaxException 17 | 18 | /** 19 | * Test utilities. 20 | */ 21 | object TestHelper 22 | { 23 | 24 | val PROJECT_PACKAGE = "me.ebonjaeger.perworldinventory" 25 | 26 | /** 27 | * Return a [File] to a file in the JAR's resources (main or test). 28 | * 29 | * @param path The absolute path to the file 30 | * @return The project file 31 | */ 32 | fun getFromJar(path: String): File 33 | { 34 | val uri = getUriOrThrow(path) 35 | return File(uri.path) 36 | } 37 | 38 | private fun getUriOrThrow(path: String): URI 39 | { 40 | val url = TestHelper.javaClass.getResource(path) ?: 41 | throw IllegalStateException("File '$path' could not be loaded") 42 | 43 | try 44 | { 45 | return URI(url.toString()) 46 | } catch (ex: URISyntaxException) 47 | { 48 | throw IllegalStateException("File '$path' cannot be converted to URI") 49 | } 50 | } 51 | 52 | /** 53 | * Make a new [Group] for testing. 54 | * 55 | * @param name The name of the group 56 | * @return A new group with the given name 57 | */ 58 | fun mockGroup(name: String): Group 59 | { 60 | val worlds = mutableSetOf(name, "${name}_nether", "${name}_the_end") 61 | return mockGroup(name, worlds) 62 | } 63 | 64 | /** 65 | * Make a new [Group] for testing, with a provided list of worlds. 66 | * 67 | * @param name The name of the group 68 | * @param worlds The world names in the group 69 | * @return A new group with the given name and worlds 70 | */ 71 | fun mockGroup(name: String, worlds: MutableSet): Group 72 | { 73 | return mockGroup(name, worlds, GameMode.SURVIVAL) 74 | } 75 | 76 | /** 77 | * Make a new [Group] for testing, with a provided list of worlds and a default GameMode. 78 | * 79 | * @param name The name of the group 80 | * @param worlds The world names in the group 81 | * @param gameMode The default GameMode 82 | * @return A new group with the given name, worlds and default GameMode 83 | */ 84 | fun mockGroup(name: String, worlds: MutableSet, gameMode: GameMode): Group { 85 | return Group(name, worlds, gameMode, null) 86 | } 87 | 88 | fun mockBukkit() { 89 | mockkStatic(Bukkit::class) 90 | 91 | @Suppress("SENSELESS_COMPARISON") // There is exactly one time where this is not false: the first time 92 | if (Bukkit.getServer() == null) { 93 | val server = mockkClass(Server::class, relaxed = true) 94 | Bukkit.setServer(server) 95 | } 96 | } 97 | 98 | fun mockItemFactory(): ItemFactory { 99 | val itemFactory = mockkClass(ItemFactory::class) 100 | 101 | // No implementation of the ItemMeta interface readily available, so we return our own 102 | every { itemFactory.getItemMeta(any()) } answers { ItemMetaTestImpl() } 103 | 104 | // This is required for ItemStack#setItemMeta to be successful 105 | val itemMeta = ItemMetaTestImpl() 106 | val metaArg = slot() 107 | val materialArg = slot() 108 | every { itemFactory.getItemMeta(any()) } answers { itemMeta } 109 | every { itemFactory.equals(any(), isNull()) } returns false 110 | every { itemFactory.isApplicable(ofType(ItemMeta::class), ofType(Material::class)) } returns true 111 | every { itemFactory.asMetaFor(capture(metaArg), ofType(Material::class)) } answers { metaArg.captured } 112 | every { itemFactory.updateMaterial(ofType(ItemMeta::class), capture(materialArg)) } answers { materialArg.captured } 113 | 114 | return itemFactory 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/test/kotlin/me/ebonjaeger/perworldinventory/GroupManagerTest.kt: -------------------------------------------------------------------------------- 1 | package me.ebonjaeger.perworldinventory 2 | 3 | import com.natpryce.hamkrest.absent 4 | import com.natpryce.hamkrest.assertion.assertThat 5 | import com.natpryce.hamkrest.equalTo 6 | import io.mockk.every 7 | import io.mockk.mockkClass 8 | import me.ebonjaeger.perworldinventory.TestHelper.mockGroup 9 | import me.ebonjaeger.perworldinventory.configuration.PluginSettings 10 | import me.ebonjaeger.perworldinventory.configuration.Settings 11 | import me.ebonjaeger.perworldinventory.service.BukkitService 12 | import org.bukkit.GameMode 13 | import org.junit.jupiter.api.Assertions.assertNotNull 14 | import org.junit.jupiter.api.Test 15 | import java.io.File 16 | 17 | /** 18 | * Test for [GroupManager]. 19 | */ 20 | class GroupManagerTest { 21 | 22 | private val bukkitService = mockkClass(BukkitService::class) 23 | 24 | private val settings = mockkClass(Settings::class) 25 | 26 | private val groupManager = GroupManager(File(""), bukkitService, settings) 27 | 28 | @Test 29 | fun shouldReturnAbsentValueForNonExistentGroup() { 30 | assertThat(groupManager.getGroup("test"), absent()) 31 | } 32 | 33 | @Test 34 | fun addGroupWithLowercaseName() 35 | { 36 | // given 37 | groupManager.groups.clear() 38 | val name = "test" 39 | val worlds = mutableSetOf(name) 40 | val gameMode = GameMode.SURVIVAL 41 | 42 | // when 43 | groupManager.addGroup(name, worlds, gameMode, true) 44 | 45 | // then 46 | val expected = mockGroup(name, worlds, gameMode) 47 | val actual = groupManager.getGroup(name) 48 | 49 | assertThat(actual, equalTo(expected)) 50 | } 51 | 52 | @Test 53 | fun addGroupWithUppercaseName() 54 | { 55 | // given 56 | groupManager.groups.clear() 57 | val name = "TeSt" 58 | val worlds = mutableSetOf(name) 59 | val gameMode = GameMode.SURVIVAL 60 | 61 | // when 62 | groupManager.addGroup(name, worlds, gameMode, true) 63 | 64 | // then 65 | val expected = mockGroup(name, worlds, gameMode) 66 | val actual = groupManager.getGroup(name) 67 | 68 | assertThat(actual, equalTo(expected)) 69 | } 70 | 71 | @Test 72 | fun addGroupWithUppercaseNameLowercaseGet() 73 | { 74 | // given 75 | groupManager.groups.clear() 76 | val name = "TeSt" 77 | val worlds = mutableSetOf(name) 78 | val gameMode = GameMode.SURVIVAL 79 | 80 | // when 81 | groupManager.addGroup(name, worlds, gameMode, true) 82 | 83 | // then 84 | val expected = mockGroup(name, worlds, gameMode) 85 | val actual = groupManager.getGroup(name.toLowerCase()) 86 | 87 | assertThat(actual, equalTo(expected)) 88 | } 89 | 90 | @Test 91 | fun getGroupFromWorldWhereExists() 92 | { 93 | // given 94 | groupManager.groups.clear() 95 | val name = "test" 96 | val worlds = mutableSetOf(name) 97 | val gameMode = GameMode.SURVIVAL 98 | groupManager.addGroup(name, worlds, gameMode, true) 99 | 100 | // when 101 | val result = groupManager.getGroupFromWorld(name) 102 | 103 | // then 104 | val expected = mockGroup(name, worlds, gameMode) 105 | 106 | assertThat(result, equalTo(expected)) 107 | } 108 | 109 | @Test 110 | fun getGroupFromWorldWhereNotExists() 111 | { 112 | // given 113 | every { settings.getProperty(PluginSettings.DISABLE_NAG) } returns false 114 | groupManager.groups.clear() 115 | val name = "test" 116 | val worlds = mutableSetOf(name, "${name}_nether", "${name}_the_end") 117 | val gameMode = GameMode.SURVIVAL 118 | 119 | // when 120 | val result = groupManager.getGroupFromWorld(name) 121 | 122 | // then 123 | val expected = mockGroup(name, worlds, gameMode) 124 | 125 | assertNotNull(result) 126 | assertThat(result, equalTo(expected)) 127 | } 128 | 129 | @Test 130 | fun getGroupAfterCreatedFromGroupFromWorldMethod() 131 | { 132 | // given 133 | every { settings.getProperty(PluginSettings.DISABLE_NAG) } returns false 134 | groupManager.groups.clear() 135 | val name = "test" 136 | val expected = groupManager.getGroupFromWorld(name) 137 | 138 | // when 139 | val result = groupManager.getGroup(name) 140 | 141 | // then 142 | assertNotNull(result) 143 | assertThat(result, equalTo(expected)) 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/test/kotlin/me/ebonjaeger/perworldinventory/serialization/ItemSerializerTest.kt: -------------------------------------------------------------------------------- 1 | package me.ebonjaeger.perworldinventory.serialization 2 | 3 | import io.mockk.every 4 | import io.mockk.mockkClass 5 | import me.ebonjaeger.perworldinventory.TestHelper 6 | import net.minidev.json.JSONObject 7 | import org.bukkit.Bukkit 8 | import org.bukkit.Material 9 | import org.bukkit.UnsafeValues 10 | import org.bukkit.configuration.serialization.ConfigurationSerialization 11 | import org.bukkit.inventory.ItemStack 12 | import org.bukkit.inventory.meta.ItemMeta 13 | import org.bukkit.util.io.BukkitObjectOutputStream 14 | import org.junit.jupiter.api.BeforeAll 15 | import org.junit.jupiter.api.Test 16 | import org.junit.jupiter.api.TestInstance 17 | import org.junit.jupiter.api.fail 18 | import org.yaml.snakeyaml.external.biz.base64Coder.Base64Coder 19 | import java.io.ByteArrayOutputStream 20 | import kotlin.test.assertEquals 21 | 22 | /** 23 | * Tests for [ItemSerializer]. 24 | */ 25 | @TestInstance(TestInstance.Lifecycle.PER_CLASS) 26 | class ItemSerializerTest { 27 | 28 | private lateinit var unsafe: UnsafeValues 29 | 30 | @BeforeAll 31 | fun writeMocksAndInitializeSerializer() { 32 | TestHelper.mockBukkit() 33 | val itemFactory = TestHelper.mockItemFactory() 34 | every { Bukkit.getItemFactory() } returns itemFactory 35 | 36 | // Bukkit's serializer needs to know about our test implementation of ItemMeta or it will fail 37 | ConfigurationSerialization.registerClass(ItemMetaTestImpl::class.java) 38 | 39 | // As of 1.13, Bukkit has a compatibility layer, and serializing an item 40 | // now checks the data version to see what Material name to use. 41 | unsafe = mockkClass(UnsafeValues::class, relaxed = true) 42 | every { Bukkit.getUnsafe() } returns unsafe 43 | every { unsafe.dataVersion } returns 1513 44 | every { unsafe.getMaterial("APPLE", 1513) } returns Material.APPLE 45 | every { unsafe.getMaterial("TORCH", 1513) } returns Material.TORCH 46 | } 47 | 48 | @Test 49 | fun verifySerializedItem() { 50 | // given 51 | val item = ItemStack(Material.APPLE, 5) 52 | 53 | // when 54 | val json = ItemSerializer.serialize(item, 1) 55 | 56 | // then 57 | val result = ItemSerializer.deserialize(JSONObject(json), 3) 58 | assertHasSameProperties(item, result) 59 | } 60 | 61 | @Test 62 | fun verifySerializedItemWithMeta() { 63 | // given 64 | val item = ItemStack(Material.APPLE, 3) 65 | val meta = ItemMetaTestImpl(mutableMapOf(Pair("test", "test"), Pair("values", 5))) 66 | setItemMetaOrFail(item, meta) 67 | 68 | // when 69 | val json = ItemSerializer.serialize(item, 0) 70 | 71 | // then 72 | // deserialize item and test for match 73 | val result = ItemSerializer.deserialize(JSONObject(json), 3) 74 | assertHasSameProperties(item, result) 75 | assertItemMetaMapsAreEqual(result, item) 76 | } 77 | 78 | @Test 79 | fun verifyEncodedDeserialization() 80 | { 81 | // given 82 | val expected = ItemStack(Material.TORCH, 16) 83 | 84 | val obj = JSONObject() 85 | ByteArrayOutputStream().use { os -> 86 | BukkitObjectOutputStream(os).use { 87 | it.writeObject(expected) 88 | } 89 | 90 | val encoded = Base64Coder.encodeLines(os.toByteArray()) 91 | obj["item"] = encoded 92 | } 93 | 94 | // when 95 | val actual = ItemSerializer.deserialize(obj, 2) 96 | 97 | // then 98 | assertHasSameProperties(expected, actual) 99 | } 100 | 101 | private fun assertHasSameProperties(expected: ItemStack, actual: ItemStack) { 102 | assertEquals(expected.type, actual.type) 103 | assertEquals(expected.amount, actual.amount) 104 | assertEquals(expected.data, actual.data) 105 | } 106 | 107 | /** 108 | * Sets the given ItemMeta to the provided ItemStack object, and fails if the setter 109 | * method reports the action as unsuccessful. 110 | */ 111 | private fun setItemMetaOrFail(item: ItemStack, meta: ItemMeta) { 112 | val isSuccessful = item.setItemMeta(meta) 113 | assertEquals(isSuccessful, true, "Setting ItemMeta to ItemStack was unsuccessful; this is likely a missing mock behavior") 114 | } 115 | 116 | private fun assertItemMetaMapsAreEqual(given: ItemStack, expected: ItemStack) { 117 | if (given.itemMeta is ItemMetaTestImpl) { 118 | val givenItemMeta = given.itemMeta as ItemMetaTestImpl 119 | val expectedItemMeta = expected.itemMeta as ItemMetaTestImpl 120 | if (expectedItemMeta.providedMap.isEmpty()) { 121 | fail("Map is empty!") 122 | } else { 123 | for (entry in expectedItemMeta.providedMap.entries) { 124 | assertEquals(givenItemMeta.providedMap[entry.key], entry.value) 125 | } 126 | } 127 | } else { 128 | fail("Expected given.itemMeta to be of test impl, but was ${given.itemMeta!!::class}") 129 | } 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/main/kotlin/me/ebonjaeger/perworldinventory/initialization/Injector.kt: -------------------------------------------------------------------------------- 1 | package me.ebonjaeger.perworldinventory.initialization 2 | 3 | import javax.inject.Provider 4 | import kotlin.reflect.KClass 5 | 6 | /** 7 | * Kotlin wrapper for [ch.jalu.injector.Injector]. 8 | */ 9 | class Injector(private val jaluInjector: ch.jalu.injector.Injector) { 10 | 11 | /** 12 | * Registers an object as the singleton of the given class. Throws an exception if a singleton is already 13 | * available for the class. 14 | * 15 | * @param clazz the class to register the object for 16 | * @param singleton the object 17 | * @param T the type to register the object for 18 | */ 19 | fun register(clazz: KClass, singleton: T) { 20 | jaluInjector.register(clazz.java, singleton) 21 | } 22 | 23 | /** 24 | * Registers a provider for the given class. The provider is used whenever the class needs to be instantiated. 25 | * 26 | * @param clazz the class to register the provider for 27 | * @param provider the provider 28 | * @param T the class' type 29 | */ 30 | fun registerProvider(clazz: KClass, provider: Provider) { 31 | jaluInjector.registerProvider(clazz.java, provider) 32 | } 33 | 34 | /** 35 | * Registers the provider class to instantiate a given class. The first time the {@code clazz} has to 36 | * be instantiated, the {@code providerClass} will be instantiated. 37 | * 38 | * @param clazz the class to register the provider for 39 | * @param providerClass the class of the provider 40 | * @param T the class' type 41 | * @param P the provider's type 42 | */ 43 | fun > registerProvider(clazz: KClass, providerClass: KClass

) { 44 | jaluInjector.registerProvider(clazz.java, providerClass.java) 45 | } 46 | 47 | /** 48 | * Processes an annotation with an associated object. The actual behavior of this method depends on the 49 | * configured handlers of the injector. By default it register the given object for the annotation such 50 | * that it may be later injected with the annotation as identifier. 51 | * 52 | * @param annotation the annotation 53 | * @param value the object 54 | */ 55 | fun provide(annotation: KClass, value: Any) { 56 | jaluInjector.provide(annotation.java, value) 57 | } 58 | 59 | /** 60 | * Retrieves or instantiates an object of the given type (singleton scope). 61 | * 62 | * @param clazz the class to retrieve the value for 63 | * @param T the class' type 64 | * @return object of the class' type 65 | */ 66 | fun getSingleton(clazz: KClass): T { 67 | return jaluInjector.getSingleton(clazz.java) 68 | } 69 | 70 | /** 71 | * Request-scoped method to instantiate a new object of the given class. The injector does not keep track 72 | * of it afterwards; it will always return a new instance and forget about it. 73 | * 74 | * @param clazz the class to instantiate 75 | * @param T the class' type 76 | * @return new instance of class T 77 | */ 78 | fun newInstance(clazz: KClass): T { 79 | return jaluInjector.newInstance(clazz.java) 80 | } 81 | 82 | /** 83 | * Returns the singleton of the given class if available. This simply returns the instance if present, and 84 | * otherwise {@code null}. Calling this method will never create any new objects. 85 | * 86 | * @param clazz the class to retrieve the instance for 87 | * @param T the class' type 88 | * @return instance or null if not available 89 | */ 90 | fun getIfAvailable(clazz: KClass): T? { 91 | return jaluInjector.getIfAvailable(clazz.java) 92 | } 93 | 94 | /** 95 | * Creates an instance of the given class if all of its dependencies are available. A new instance 96 | * is returned each time and the created object is not stored in the injector. 97 | *

98 | * Note: Currently, all dependencies of the class need to be registered singletons for a new 99 | * instance to be created. This limitation may be lifted in future versions. 100 | * 101 | * @param clazz the class to construct if possible 102 | * @param T the class' type 103 | * @return instance of the class, or {@code null} if any dependency does not already exist 104 | */ 105 | fun createIfHasDependencies(clazz: KClass): T? { 106 | return jaluInjector.createIfHasDependencies(clazz.java) 107 | } 108 | 109 | /** 110 | * Returns all known singletons of the given type. Typically used 111 | * with interfaces in order to perform an action without knowing its concrete implementors. 112 | * Trivially, using {@link Object} as {@code clazz} will return all known singletons. 113 | * 114 | * @param clazz the class to retrieve singletons of 115 | * @param T the class' type 116 | * @return list of singletons of the given type 117 | */ 118 | fun retrieveAllOfType(clazz: KClass): MutableCollection { 119 | return jaluInjector.retrieveAllOfType(clazz.java) 120 | } 121 | } -------------------------------------------------------------------------------- /src/main/kotlin/me/ebonjaeger/perworldinventory/listener/player/PlayerChangedWorldListener.kt: -------------------------------------------------------------------------------- 1 | package me.ebonjaeger.perworldinventory.listener.player 2 | 3 | import me.ebonjaeger.perworldinventory.ConsoleLogger 4 | import me.ebonjaeger.perworldinventory.GroupManager 5 | import me.ebonjaeger.perworldinventory.PerWorldInventory 6 | import me.ebonjaeger.perworldinventory.configuration.PluginSettings 7 | import me.ebonjaeger.perworldinventory.configuration.Settings 8 | import me.ebonjaeger.perworldinventory.data.ProfileManager 9 | import me.ebonjaeger.perworldinventory.event.Cause 10 | import me.ebonjaeger.perworldinventory.event.InventoryLoadEvent 11 | import me.ebonjaeger.perworldinventory.permission.PlayerPermission 12 | import org.bukkit.Bukkit 13 | import org.bukkit.event.EventHandler 14 | import org.bukkit.event.EventPriority 15 | import org.bukkit.event.Listener 16 | import org.bukkit.event.player.PlayerChangedWorldEvent 17 | import javax.inject.Inject 18 | 19 | class PlayerChangedWorldListener @Inject constructor(private val plugin: PerWorldInventory, 20 | private val groupManager: GroupManager, 21 | private val profileManager: ProfileManager, 22 | private val settings: Settings) : Listener 23 | { 24 | 25 | @EventHandler(priority = EventPriority.MONITOR) 26 | fun onPlayerChangedWorld(event: PlayerChangedWorldEvent) 27 | { 28 | val player = event.player 29 | val worldFrom = event.from 30 | val worldTo = player.world 31 | val groupFrom = groupManager.getGroupFromWorld(worldFrom.name) 32 | val groupTo = groupManager.getGroupFromWorld(worldTo.name) 33 | val startingGameMode = player.gameMode 34 | 35 | ConsoleLogger.fine("onPlayerChangedWorld: ${player.name} changed worlds") 36 | 37 | // Check if the FROM group is configured 38 | if (!groupFrom.configured && settings.getProperty(PluginSettings.SHARE_IF_UNCONFIGURED)) 39 | { 40 | ConsoleLogger.debug("onPlayerChangedWorld: FROM group (${groupFrom.name}) is not defined, and plugin configured to share inventory") 41 | 42 | return 43 | } 44 | 45 | // Check if the groups are actually the same group 46 | if (groupFrom == groupTo) 47 | { 48 | ConsoleLogger.debug("onPlayerChangedWorld: Both groups are the same: '$groupFrom'") 49 | return 50 | } 51 | 52 | // Check of the TO group is configured 53 | if (!groupTo.configured && settings.getProperty(PluginSettings.SHARE_IF_UNCONFIGURED)) 54 | { 55 | ConsoleLogger.debug("onPlayerChangedWorld: FROM group (${groupTo.name}) is not defined, and plugin configured to share inventory") 56 | 57 | return 58 | } 59 | 60 | // Check if the player bypasses the changes 61 | if (!settings.getProperty(PluginSettings.DISABLE_BYPASS) && 62 | player.hasPermission(PlayerPermission.BYPASS_WORLDS.getNode())) 63 | { 64 | ConsoleLogger.debug("onPlayerChangedWorld: Player '${player.name}' has bypass worlds permission") 65 | return 66 | } 67 | 68 | // Check if we manage GameModes. If we do, we can skip loading the data 69 | // for a mode they're only going to be in for half a second. 70 | if (settings.getProperty(PluginSettings.MANAGE_GAMEMODES) && 71 | !player.hasPermission(PlayerPermission.BYPASS_ENFORCE_GAMEMODE.getNode())) 72 | { 73 | if (player.gameMode != groupTo.defaultGameMode) 74 | { 75 | ConsoleLogger.debug("onPlayerChangedWorld: We manage GameModes and the GameMode for this group is different from ${player.name}'s current GameMode") 76 | player.gameMode = groupTo.defaultGameMode 77 | 78 | // If GameMode inventories are separated, then the other listener will 79 | // handle it, so we do nothing more here. Else, we do still have to load 80 | // a new inventory, which shouldn't be a problem because if GameModes 81 | // aren't separated, the other listener will return early. Thus it should 82 | // be perfectly safe to load the data from here. 83 | 84 | if (settings.getProperty(PluginSettings.SEPARATE_GM_INVENTORIES)) 85 | { 86 | ConsoleLogger.debug("onPlayerChangedWorld: GameMode inventories are separated, so returning from here") 87 | return 88 | } 89 | } 90 | } 91 | 92 | // All other checks are done, time to get the data 93 | ConsoleLogger.fine("onPlayerChangedWorld: Loading data for player '${player.name}' for group: $groupTo") 94 | 95 | // Add player to the timeouts to prevent item dupe 96 | if (plugin.updateTimeoutsTaskId != -1) 97 | { 98 | plugin.timeouts[player.uniqueId] = plugin.SLOT_TIMEOUT 99 | } 100 | 101 | val loadEvent = InventoryLoadEvent(player, Cause.WORLD_CHANGE, 102 | startingGameMode, player.gameMode, groupTo) 103 | Bukkit.getPluginManager().callEvent(loadEvent) 104 | if (!loadEvent.isCancelled) 105 | { 106 | profileManager.getPlayerData(player, groupTo, player.gameMode) 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/main/kotlin/me/ebonjaeger/perworldinventory/data/migration/MigrationTask.kt: -------------------------------------------------------------------------------- 1 | package me.ebonjaeger.perworldinventory.data.migration 2 | 3 | import com.dumptruckman.bukkit.configuration.util.SerializationHelper 4 | import me.ebonjaeger.perworldinventory.ConsoleLogger 5 | import me.ebonjaeger.perworldinventory.Group 6 | import me.ebonjaeger.perworldinventory.data.ProfileKey 7 | import me.ebonjaeger.perworldinventory.serialization.PlayerSerializer 8 | import net.minidev.json.JSONObject 9 | import net.minidev.json.JSONStyle 10 | import net.minidev.json.parser.JSONParser 11 | import org.bukkit.GameMode 12 | import org.bukkit.OfflinePlayer 13 | import org.bukkit.scheduler.BukkitRunnable 14 | import java.io.File 15 | import java.io.FileReader 16 | import java.io.FileWriter 17 | import java.io.IOException 18 | import java.util.* 19 | 20 | const val ENDER_CHEST_SLOTS = 27 21 | const val INVENTORY_SLOTS = 41 22 | const val MAX_MIGRATIONS_PER_TICK = 10 23 | 24 | class MigrationTask (private val migrationService: MigrationService, 25 | private val offlinePlayers: Array, 26 | private val dataDirectory: File, 27 | private val groups: Collection) : BukkitRunnable() { 28 | 29 | private val migrateQueue: Queue = LinkedList() 30 | 31 | private var index = 0 32 | private var migrated = 0 33 | 34 | override fun run() { 35 | // Calculate our stopping index for this run 36 | val stopIndex = if (index + MAX_MIGRATIONS_PER_TICK < offlinePlayers.size) { // Use index + constant if the result isn't more than the total number of players 37 | index + MAX_MIGRATIONS_PER_TICK 38 | } else { // Index would be greater than number of players, so just use the size of the array 39 | offlinePlayers.size 40 | } 41 | 42 | if (index >= offlinePlayers.size) { // No more players to migrate 43 | migrationService.finishMigration(migrated) 44 | cancel() 45 | } 46 | 47 | // Add players to a queue to be migrated 48 | while (index < stopIndex) { 49 | migrateQueue.offer(offlinePlayers[index]) 50 | index++ 51 | } 52 | 53 | while (migrateQueue.isNotEmpty()) { // Iterate over the queue 54 | val player = migrateQueue.poll() 55 | migrate(player) 56 | } 57 | 58 | if (index % 100 == 0) { // Print migration status every 100 players (about every 5 seconds) 59 | ConsoleLogger.info("Migration progress: $index/${offlinePlayers.size}") 60 | } 61 | } 62 | 63 | @Suppress("UNCHECKED_CAST") // Safe to assume our own Map types 64 | private fun migrate(player: OfflinePlayer) { 65 | val name = player.name 66 | 67 | if (!player.hasPlayedBefore() || name == null) { // It is likely that this player has never actually joined the server 68 | return 69 | } 70 | 71 | for (group in groups) { // Loop through all groups 72 | for (gameMode in GameMode.values()) { // Loop through all GameMode's 73 | if (gameMode == GameMode.SPECTATOR) { // Spectator mode doesn't have an inventory 74 | continue 75 | } 76 | 77 | val key = ProfileKey(player.uniqueId, group, gameMode) 78 | val file = getFile(key) 79 | 80 | if (!file.exists()) { // Player hasn't been in this group or GameMode before 81 | continue 82 | } 83 | 84 | FileReader(file).use { reader -> // Read the old data from the file 85 | val parser = JSONParser(JSONParser.USE_INTEGER_STORAGE) 86 | val data = parser.parse(reader) as JSONObject 87 | 88 | if (data.containsKey("==")) { // This profile has already been migrated 89 | return@use 90 | } else if (!data.containsKey("data-format") || (data["data-format"] as Int) < 2) { // Profile is way too old to migrate 91 | return@use 92 | } 93 | 94 | val profile = PlayerSerializer.deserialize(data, name, INVENTORY_SLOTS, ENDER_CHEST_SLOTS) 95 | val map = SerializationHelper.serialize(profile) 96 | val json = JSONObject(map as Map) 97 | 98 | try { // Write the newly-serialized data back to the file 99 | FileWriter(file).use { writer -> writer.write(json.toJSONString(JSONStyle.LT_COMPRESS)) } 100 | migrated++ 101 | } catch (ex: IOException) { 102 | ConsoleLogger.severe("Could not write data to file '$file' during migration:", ex) 103 | } 104 | } 105 | } 106 | } 107 | } 108 | 109 | /** 110 | * Get the data file for a player. 111 | * 112 | * @param key The [ProfileKey] to get the right file 113 | * @return The data file to read from or write to 114 | */ 115 | private fun getFile(key: ProfileKey): File { 116 | val dir = File(dataDirectory, key.uuid.toString()) 117 | return when(key.gameMode) { 118 | GameMode.ADVENTURE -> File(dir, key.group.name + "_adventure.json") 119 | GameMode.CREATIVE -> File(dir, key.group.name + "_creative.json") 120 | GameMode.SPECTATOR -> File(dir, key.group.name + "_spectator.json") 121 | GameMode.SURVIVAL -> File(dir, key.group.name + ".json") 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/test/kotlin/me/ebonjaeger/perworldinventory/service/EconomyServiceTest.kt: -------------------------------------------------------------------------------- 1 | package me.ebonjaeger.perworldinventory.service 2 | 3 | import com.natpryce.hamkrest.assertion.assertThat 4 | import com.natpryce.hamkrest.present 5 | import io.mockk.MockKAnnotations 6 | import io.mockk.called 7 | import io.mockk.every 8 | import io.mockk.impl.annotations.InjectMockKs 9 | import io.mockk.impl.annotations.MockK 10 | import io.mockk.junit5.MockKExtension 11 | import io.mockk.mockkClass 12 | import io.mockk.verify 13 | import me.ebonjaeger.perworldinventory.configuration.PlayerSettings 14 | import me.ebonjaeger.perworldinventory.configuration.Settings 15 | import me.ebonjaeger.perworldinventory.data.PlayerProfile 16 | import net.milkbowl.vault.economy.Economy 17 | import net.milkbowl.vault.economy.EconomyResponse 18 | import org.bukkit.Server 19 | import org.bukkit.entity.Player 20 | import org.bukkit.plugin.Plugin 21 | import org.bukkit.plugin.PluginManager 22 | import org.bukkit.plugin.RegisteredServiceProvider 23 | import org.bukkit.plugin.ServicesManager 24 | import org.junit.jupiter.api.BeforeEach 25 | import org.junit.jupiter.api.Test 26 | import org.junit.jupiter.api.extension.ExtendWith 27 | 28 | /** 29 | * Test for [EconomyService]. 30 | */ 31 | @ExtendWith(MockKExtension::class) 32 | class EconomyServiceTest { 33 | 34 | @InjectMockKs 35 | private lateinit var economyService: EconomyService 36 | 37 | @MockK 38 | private lateinit var server: Server 39 | @MockK 40 | private lateinit var settings: Settings 41 | @MockK 42 | private lateinit var pluginManager: PluginManager 43 | @MockK 44 | private lateinit var servicesManager: ServicesManager 45 | @MockK 46 | private lateinit var registeredServiceProvider: RegisteredServiceProvider 47 | @MockK 48 | private lateinit var economy: Economy 49 | @MockK 50 | private lateinit var playerProfile: PlayerProfile 51 | 52 | @BeforeEach 53 | fun wireUpMocks() { 54 | MockKAnnotations.init(this) 55 | 56 | every { server.servicesManager } returns servicesManager 57 | every { server.pluginManager } returns pluginManager 58 | every { pluginManager.getPlugin("Vault") } returns mockkClass(Plugin::class) 59 | every { servicesManager.getRegistration(Economy::class.java) } returns registeredServiceProvider 60 | every { registeredServiceProvider.provider } returns economy 61 | } 62 | 63 | @Test 64 | fun shouldInitializeEconomy() { 65 | // given 66 | every { settings.getProperty(PlayerSettings.USE_ECONOMY) } returns true 67 | 68 | // when 69 | economyService.tryLoadEconomy() 70 | 71 | // then 72 | assertThat(getEconomyField(), present()) 73 | } 74 | 75 | @Test 76 | fun shouldNotInitializeEconomyIfSoConfigured() { 77 | // given 78 | every { settings.getProperty(PlayerSettings.USE_ECONOMY) } returns false 79 | 80 | // when 81 | economyService.tryLoadEconomy() 82 | 83 | // then 84 | //assertThat(getEconomyField(), absent()) 85 | verify { 86 | servicesManager wasNot called 87 | registeredServiceProvider wasNot called 88 | } 89 | } 90 | 91 | @Test 92 | fun shouldWithdrawFunds() { 93 | // given 94 | every { settings.getProperty(PlayerSettings.USE_ECONOMY) } returns true 95 | economyService.tryLoadEconomy() 96 | 97 | val player = mockkClass(Player::class) 98 | every { player.name } returns "Bob" 99 | every { economy.getBalance(player) } returns 320.0 100 | val er = EconomyResponse(320.0, 0.0, EconomyResponse.ResponseType.SUCCESS, "Woo?") 101 | every { economy.withdrawPlayer(player, 320.0) } returns er 102 | 103 | // when 104 | economyService.withdrawFromPlayer(player) 105 | 106 | // then 107 | verify { economy.withdrawPlayer(player, 320.0) } 108 | } 109 | 110 | @Test 111 | fun newBalanceGreaterThanOldBalance() { 112 | // given 113 | every { settings.getProperty(PlayerSettings.USE_ECONOMY) } returns true 114 | economyService.tryLoadEconomy() 115 | 116 | val player = mockkClass(Player::class) 117 | val oldBalance = 2.81 118 | val newBalance = 1332.49 119 | every { player.name } returns "Bob" 120 | every { economy.getBalance(player) } returns oldBalance 121 | every { playerProfile.balance } returns newBalance 122 | val er = EconomyResponse((newBalance - oldBalance), newBalance, EconomyResponse.ResponseType.SUCCESS, "Woo?") 123 | every { economy.depositPlayer(player, (newBalance - oldBalance)) } returns er 124 | 125 | // when 126 | economyService.setNewBalance(player, playerProfile.balance) 127 | 128 | // then 129 | verify { economy.depositPlayer(player, (newBalance - oldBalance)) } 130 | } 131 | 132 | @Test 133 | fun newBalanceLessThanOldBalance() { 134 | // given 135 | every { settings.getProperty(PlayerSettings.USE_ECONOMY) } returns true 136 | economyService.tryLoadEconomy() 137 | 138 | val player = mockkClass(Player::class) 139 | val oldBalance = 1332.49 140 | val newBalance = 2.81 141 | val er = EconomyResponse((oldBalance - newBalance), newBalance, EconomyResponse.ResponseType.SUCCESS, "Woo?") 142 | every { economy.getBalance(player) } returns oldBalance 143 | every { playerProfile.balance } returns newBalance 144 | every { player.name } returns "Bob" 145 | every { economy.withdrawPlayer(player, (oldBalance - newBalance)) } returns er 146 | 147 | // when 148 | economyService.setNewBalance(player, playerProfile.balance) 149 | 150 | // then 151 | verify { economy.withdrawPlayer(player, (oldBalance - newBalance)) } 152 | } 153 | 154 | private fun getEconomyField(): Economy? { 155 | return economyService::class.java.getDeclaredField("economy").let { 156 | it.isAccessible = true 157 | return@let it.get(economyService) as Economy? 158 | } 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /src/main/kotlin/me/ebonjaeger/perworldinventory/GroupManager.kt: -------------------------------------------------------------------------------- 1 | package me.ebonjaeger.perworldinventory 2 | 3 | import me.ebonjaeger.perworldinventory.configuration.PluginSettings 4 | import me.ebonjaeger.perworldinventory.configuration.Settings 5 | import me.ebonjaeger.perworldinventory.initialization.PluginFolder 6 | import me.ebonjaeger.perworldinventory.service.BukkitService 7 | import org.bukkit.GameMode 8 | import org.bukkit.configuration.file.YamlConfiguration 9 | import java.io.File 10 | import java.io.IOException 11 | import javax.inject.Inject 12 | 13 | class GroupManager @Inject constructor(@PluginFolder pluginFolder: File, 14 | private val bukkitService: BukkitService, 15 | private val settings: Settings) 16 | { 17 | 18 | private val WORLDS_CONFIG_FILE = File(pluginFolder, "worlds.yml") 19 | 20 | val groups = mutableMapOf() 21 | 22 | /** 23 | * Add a Group. 24 | * 25 | * @param name The name of the group 26 | * @param worlds A Set of world names 27 | * @param gameMode The default GameMode for this group 28 | * @param configured If the group was configured (true) or created on the fly (false) 29 | */ 30 | fun addGroup(name: String, worlds: MutableSet, gameMode: GameMode, configured: Boolean) 31 | { 32 | addGroup(name, worlds, gameMode, null, configured) 33 | } 34 | 35 | /** 36 | * Add a Group. 37 | * 38 | * @param name The name of the group 39 | * @param worlds A Set of world names 40 | * @param gameMode The default GameMode for this group 41 | * @param respawnWorld The world players will spawn in on death 42 | * @param configured If the group was configured (true) or created on the fly (false) 43 | */ 44 | fun addGroup(name: String, worlds: MutableSet, gameMode: GameMode, respawnWorld: String?, configured: Boolean) 45 | { 46 | val group = Group(name, worlds, gameMode, respawnWorld) 47 | group.configured = configured 48 | ConsoleLogger.fine("Adding group to memory: ${group.name}") 49 | ConsoleLogger.debug("Group properties: $group") 50 | groups[name.toLowerCase()] = group 51 | } 52 | 53 | /** 54 | * Get a group by name. This will return null if no group with the given name 55 | * exists. 56 | * 57 | * @param name The name of the Group 58 | * @return The Group 59 | */ 60 | fun getGroup(name: String): Group? 61 | = groups[name.toLowerCase()] 62 | 63 | /** 64 | * Get the group that contains a specific world. This method iterates 65 | * through the groups and checks if each one contains the name of the 66 | * given world. If no groups contain the world, a new group will be 67 | * created and returned. 68 | * 69 | * @param world The name of the world in the group 70 | * @return The group that contains the given world 71 | */ 72 | fun getGroupFromWorld(world: String): Group 73 | { 74 | var group = groups.values.firstOrNull { it.containsWorld(world) } 75 | if (group != null) return group 76 | 77 | // If we reach this point, the group doesn't yet exist. 78 | val worlds = mutableSetOf(world, "${world}_nether", "${world}_the_end") 79 | group = Group(world, worlds, GameMode.SURVIVAL, null) 80 | 81 | addGroup(world, worlds, GameMode.SURVIVAL, false) 82 | if (!settings.getProperty(PluginSettings.DISABLE_NAG)) 83 | { 84 | ConsoleLogger.warning("Creating a new group on the fly for '$world'." + 85 | " Please double check your `worlds.yml` file configuration!") 86 | } 87 | 88 | return group 89 | } 90 | 91 | /** 92 | * Remove a world group. 93 | * 94 | * @param group The name of the group to remove 95 | */ 96 | fun removeGroup(group: String) 97 | { 98 | groups.remove(group.toLowerCase()) 99 | ConsoleLogger.fine("Removed group '$group'") 100 | } 101 | 102 | /** 103 | * Load the groups configured in the file `worlds.yml` into memory. 104 | */ 105 | fun loadGroups() 106 | { 107 | groups.clear() 108 | 109 | bukkitService.runTaskAsynchronously { 110 | val yaml = YamlConfiguration.loadConfiguration(WORLDS_CONFIG_FILE) 111 | bukkitService.runTask { 112 | var section = yaml.getConfigurationSection("groups.") 113 | 114 | if (section == null) { // Ensure the file contains the groups section 115 | section = yaml.createSection("groups") 116 | } 117 | 118 | section.getKeys(false).forEach { key -> 119 | val worlds = yaml.getStringList("groups.$key.worlds").toMutableSet() 120 | val gameMode = GameMode.valueOf( (yaml.getString("groups.$key.default-gamemode") ?: "SURVIVAL").toUpperCase() ) 121 | val respawnWorld = if (yaml.contains("groups.$key.respawnWorld")) 122 | { 123 | yaml.getString("groups.$key.respawnWorld") 124 | } else 125 | { 126 | null 127 | } 128 | 129 | addGroup(key, worlds, gameMode, respawnWorld, true) 130 | } 131 | } 132 | } 133 | } 134 | 135 | /** 136 | * Save all of the groups currently in memory to the disk. 137 | */ 138 | fun saveGroups() 139 | { 140 | val config = YamlConfiguration.loadConfiguration(WORLDS_CONFIG_FILE) 141 | config.options().header(Utils.getWorldsConfigHeader()) 142 | config.set("groups", null) 143 | 144 | groups.values.forEach { toYaml(it, config) } 145 | 146 | try 147 | { 148 | config.save(WORLDS_CONFIG_FILE) 149 | } catch (ex: IOException) 150 | { 151 | ConsoleLogger.warning("Could not save the groups config to disk:", ex) 152 | } 153 | } 154 | 155 | private fun toYaml(group: Group, config: YamlConfiguration) 156 | { 157 | val key = "groups.${group.name}" 158 | config.set(key, null) 159 | config.set("$key.worlds", group.worlds.toList()) 160 | config.set("$key.default-gamemode", group.defaultGameMode.toString()) 161 | config.set("$key.respawnWorld", group.respawnWorld) 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /src/test/kotlin/me/ebonjaeger/perworldinventory/serialization/ItemMetaImpl.kt: -------------------------------------------------------------------------------- 1 | package me.ebonjaeger.perworldinventory.serialization 2 | 3 | import com.google.common.collect.Multimap 4 | import org.bukkit.attribute.Attribute 5 | import org.bukkit.attribute.AttributeModifier 6 | import org.bukkit.enchantments.Enchantment 7 | import org.bukkit.inventory.EquipmentSlot 8 | import org.bukkit.inventory.ItemFlag 9 | import org.bukkit.inventory.meta.Damageable 10 | import org.bukkit.inventory.meta.ItemMeta 11 | import org.bukkit.inventory.meta.tags.CustomItemTagContainer 12 | import org.bukkit.persistence.PersistentDataContainer 13 | 14 | /** 15 | * Implementation of [ItemMeta] for usage in tests. 16 | * 17 | * As a stand-in for state, a [map][providedMap] is maintained, which should be preserved during 18 | * serialization and deserialization. 19 | */ 20 | class ItemMetaTestImpl : ItemMeta, Damageable { 21 | 22 | val providedMap: MutableMap 23 | 24 | private var version = 0 25 | 26 | /** 27 | * Default constructor. 28 | */ 29 | constructor() { 30 | this.providedMap = mutableMapOf() 31 | } 32 | 33 | /** 34 | * Deserialization constructor, as used in 35 | * [org.bukkit.configuration.serialization.ConfigurationSerialization.getConstructor]. 36 | */ 37 | constructor(map: MutableMap) { 38 | this.providedMap = map 39 | } 40 | 41 | override fun serialize(): MutableMap = 42 | HashMap(this.providedMap) 43 | 44 | override fun clone(): ItemMetaTestImpl { 45 | val mapClone = HashMap(providedMap) 46 | return ItemMetaTestImpl(mapClone) 47 | } 48 | 49 | override fun setDisplayName(name: String?) { 50 | throw NotImplementedError("not implemented") 51 | } 52 | 53 | override fun getLore(): MutableList { 54 | throw NotImplementedError("not implemented") 55 | } 56 | 57 | override fun setLore(lore: MutableList?) { 58 | throw NotImplementedError("not implemented") 59 | } 60 | 61 | override fun hasEnchants(): Boolean { 62 | throw NotImplementedError("not implemented") 63 | } 64 | 65 | override fun setLocalizedName(name: String?) { 66 | throw NotImplementedError("not implemented") 67 | } 68 | 69 | override fun hasLore(): Boolean { 70 | throw NotImplementedError("not implemented") 71 | } 72 | 73 | override fun addItemFlags(vararg itemFlags: ItemFlag?) { 74 | throw NotImplementedError("not implemented") 75 | } 76 | 77 | override fun hasDisplayName(): Boolean { 78 | throw NotImplementedError("not implemented") 79 | } 80 | 81 | override fun getItemFlags(): MutableSet { 82 | throw NotImplementedError("not implemented") 83 | } 84 | 85 | override fun setUnbreakable(unbreakable: Boolean) { 86 | throw NotImplementedError("not implemented") 87 | } 88 | 89 | override fun getDisplayName(): String { 90 | throw NotImplementedError("not implemented") 91 | } 92 | 93 | override fun getEnchants(): MutableMap { 94 | throw NotImplementedError("not implemented") 95 | } 96 | 97 | override fun getLocalizedName(): String { 98 | throw NotImplementedError("not implemented") 99 | } 100 | 101 | override fun isUnbreakable(): Boolean { 102 | throw NotImplementedError("not implemented") 103 | } 104 | 105 | override fun removeItemFlags(vararg itemFlags: ItemFlag?) { 106 | throw NotImplementedError("not implemented") 107 | } 108 | 109 | override fun hasLocalizedName(): Boolean { 110 | throw NotImplementedError("not implemented") 111 | } 112 | 113 | override fun getDamage(): Int 114 | { 115 | return 0 116 | } 117 | 118 | override fun hasDamage(): Boolean 119 | { 120 | throw NotImplementedError("not implemented") 121 | } 122 | 123 | override fun setDamage(damage: Int) 124 | { 125 | throw NotImplementedError("not implemented") 126 | } 127 | 128 | override fun addEnchant(ench: Enchantment, level: Int, ignoreLevelRestriction: Boolean): Boolean { 129 | throw NotImplementedError("not implemented") 130 | } 131 | 132 | override fun hasConflictingEnchant(ench: Enchantment): Boolean { 133 | throw NotImplementedError("not implemented") 134 | } 135 | 136 | override fun getCustomModelData(): Int { 137 | throw NotImplementedError("not implemented") 138 | } 139 | 140 | override fun setAttributeModifiers(attributeModifiers: Multimap?) { 141 | throw NotImplementedError("not implemented") 142 | } 143 | 144 | override fun removeAttributeModifier(attribute: Attribute): Boolean { 145 | throw NotImplementedError("not implemented") 146 | } 147 | 148 | override fun removeAttributeModifier(slot: EquipmentSlot): Boolean { 149 | throw NotImplementedError("not implemented") 150 | } 151 | 152 | override fun removeAttributeModifier(attribute: Attribute, modifier: AttributeModifier): Boolean { 153 | throw NotImplementedError("not implemented") 154 | } 155 | 156 | override fun hasCustomModelData(): Boolean { 157 | throw NotImplementedError("not implemented") 158 | } 159 | 160 | override fun addAttributeModifier(attribute: Attribute, modifier: AttributeModifier): Boolean { 161 | throw NotImplementedError("not implemented") 162 | } 163 | 164 | override fun getEnchantLevel(ench: Enchantment): Int { 165 | throw NotImplementedError("not implemented") 166 | } 167 | 168 | override fun setVersion(version: Int) { 169 | this.version = version 170 | } 171 | 172 | override fun hasEnchant(ench: Enchantment): Boolean { 173 | throw NotImplementedError("not implemented") 174 | } 175 | 176 | override fun setCustomModelData(data: Int?) { 177 | throw NotImplementedError("not implemented") 178 | } 179 | 180 | override fun hasAttributeModifiers(): Boolean { 181 | throw NotImplementedError("not implemented") 182 | } 183 | 184 | override fun getCustomTagContainer(): CustomItemTagContainer { 185 | throw NotImplementedError("not implemented") 186 | } 187 | 188 | override fun hasItemFlag(flag: ItemFlag): Boolean { 189 | throw NotImplementedError("not implemented") 190 | } 191 | 192 | override fun removeEnchant(ench: Enchantment): Boolean { 193 | throw NotImplementedError("not implemented") 194 | } 195 | 196 | override fun getAttributeModifiers(): Multimap? { 197 | throw NotImplementedError("not implemented") 198 | } 199 | 200 | override fun getAttributeModifiers(slot: EquipmentSlot): Multimap { 201 | throw NotImplementedError("not implemented") 202 | } 203 | 204 | override fun getAttributeModifiers(attribute: Attribute): MutableCollection { 205 | throw NotImplementedError("not implemented") 206 | } 207 | 208 | override fun getPersistentDataContainer(): PersistentDataContainer { 209 | throw NotImplementedError("not implemented") 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /src/test/kotlin/me/ebonjaeger/perworldinventory/serialization/PlayerSerializerTest.kt: -------------------------------------------------------------------------------- 1 | package me.ebonjaeger.perworldinventory.serialization 2 | 3 | import com.dumptruckman.bukkit.configuration.util.SerializationHelper 4 | import com.natpryce.hamkrest.assertion.assertThat 5 | import com.natpryce.hamkrest.equalTo 6 | import io.mockk.every 7 | import io.mockk.mockkClass 8 | import io.mockk.mockkStatic 9 | import me.ebonjaeger.perworldinventory.TestHelper 10 | import me.ebonjaeger.perworldinventory.data.PlayerProfile 11 | import net.minidev.json.JSONObject 12 | import net.minidev.json.JSONStyle 13 | import net.minidev.json.parser.JSONParser 14 | import org.bukkit.Bukkit 15 | import org.bukkit.GameMode 16 | import org.bukkit.Material 17 | import org.bukkit.UnsafeValues 18 | import org.bukkit.configuration.serialization.ConfigurationSerialization 19 | import org.bukkit.inventory.ItemStack 20 | import org.junit.jupiter.api.BeforeAll 21 | import org.junit.jupiter.api.Test 22 | import org.junit.jupiter.api.TestInstance 23 | 24 | /** 25 | * Tests for [PlayerSerializer]. 26 | */ 27 | @TestInstance(TestInstance.Lifecycle.PER_CLASS) 28 | class PlayerSerializerTest 29 | { 30 | 31 | private lateinit var unsafe: UnsafeValues 32 | 33 | @BeforeAll 34 | fun prepareTestingStuff() { 35 | mockkStatic(Bukkit::class) 36 | val itemFactory = TestHelper.mockItemFactory() 37 | every { Bukkit.getItemFactory() } returns itemFactory 38 | 39 | // Bukkit's serializer needs to know about our test implementation of ItemMeta and PlayerProfile or it will fail 40 | ConfigurationSerialization.registerClass(ItemMetaTestImpl::class.java) 41 | ConfigurationSerialization.registerClass(PlayerProfile::class.java) 42 | 43 | // As of 1.13, Bukkit has a compatibility layer, and serializing an item 44 | // now checks the data version to see what Material name to use. 45 | unsafe = mockkClass(UnsafeValues::class, relaxed = true) 46 | every { Bukkit.getUnsafe() } returns unsafe 47 | every { unsafe.dataVersion } returns 1513 48 | every { unsafe.getMaterial("AIR", 1513) } returns Material.AIR 49 | every { unsafe.getMaterial("DIAMOND", 1513) } returns Material.DIAMOND 50 | every { unsafe.getMaterial("DIAMOND_CHESTPLATE", 1513) } returns Material.DIAMOND_CHESTPLATE 51 | every { unsafe.getMaterial("IRON_LEGGINGS", 1513) } returns Material.IRON_LEGGINGS 52 | every { unsafe.getMaterial("GOLDEN_APPLE", 1513) } returns Material.GOLDEN_APPLE 53 | } 54 | 55 | @Test 56 | @Suppress("UNCHECKED_CAST") 57 | fun verifyCorrectSerialization() { 58 | // given 59 | val armor = arrayOf(ItemStack(Material.AIR), ItemStack(Material.DIAMOND_CHESTPLATE), 60 | ItemStack(Material.IRON_LEGGINGS), ItemStack(Material.AIR)) 61 | val enderChest = arrayOf(ItemStack(Material.DIAMOND), ItemStack(Material.AIR), 62 | ItemStack(Material.DIAMOND), ItemStack(Material.AIR), 63 | ItemStack(Material.DIAMOND), ItemStack(Material.AIR), 64 | ItemStack(Material.DIAMOND), ItemStack(Material.AIR), 65 | ItemStack(Material.DIAMOND), ItemStack(Material.AIR), 66 | ItemStack(Material.DIAMOND), ItemStack(Material.AIR), 67 | ItemStack(Material.DIAMOND), ItemStack(Material.AIR), 68 | ItemStack(Material.DIAMOND), ItemStack(Material.AIR), 69 | ItemStack(Material.DIAMOND), ItemStack(Material.AIR), 70 | ItemStack(Material.DIAMOND), ItemStack(Material.AIR), 71 | ItemStack(Material.DIAMOND), ItemStack(Material.AIR), 72 | ItemStack(Material.DIAMOND), ItemStack(Material.AIR), 73 | ItemStack(Material.DIAMOND), ItemStack(Material.AIR), 74 | ItemStack(Material.DIAMOND)) 75 | val inventory = arrayOf(ItemStack(Material.DIAMOND), ItemStack(Material.AIR), 76 | ItemStack(Material.DIAMOND), ItemStack(Material.AIR), 77 | ItemStack(Material.DIAMOND), ItemStack(Material.AIR), 78 | ItemStack(Material.DIAMOND), ItemStack(Material.AIR), 79 | ItemStack(Material.DIAMOND), ItemStack(Material.AIR), 80 | ItemStack(Material.DIAMOND), ItemStack(Material.AIR), 81 | ItemStack(Material.DIAMOND), ItemStack(Material.AIR), 82 | ItemStack(Material.DIAMOND), ItemStack(Material.AIR), 83 | ItemStack(Material.DIAMOND), ItemStack(Material.AIR), 84 | ItemStack(Material.DIAMOND), ItemStack(Material.AIR), 85 | ItemStack(Material.DIAMOND), ItemStack(Material.AIR), 86 | ItemStack(Material.DIAMOND), ItemStack(Material.AIR), 87 | ItemStack(Material.DIAMOND), ItemStack(Material.AIR), 88 | ItemStack(Material.DIAMOND), ItemStack(Material.AIR), 89 | ItemStack(Material.DIAMOND), ItemStack(Material.AIR), 90 | ItemStack(Material.DIAMOND), ItemStack(Material.AIR), 91 | ItemStack(Material.DIAMOND), ItemStack(Material.AIR), 92 | ItemStack(Material.DIAMOND), ItemStack(Material.AIR), 93 | ItemStack(Material.DIAMOND)) 94 | val profile = PlayerProfile(armor, enderChest, inventory, false, "Bob", 95 | 5.0F, 50.5F, false, 20, 20.0, 14.0, GameMode.SURVIVAL, 5, 4.86F, 96 | mutableListOf(), 0.0F, 0, 500, 500, 0.0) 97 | 98 | // when 99 | val map = SerializationHelper.serialize(profile) as Map 100 | val json = JSONObject(map).toJSONString(JSONStyle.LT_COMPRESS) 101 | 102 | // then 103 | val parsed = JSONParser(JSONParser.USE_INTEGER_STORAGE).parse(json) as JSONObject 104 | val result = SerializationHelper.deserialize(parsed) as PlayerProfile 105 | assertProfilesAreEqual(profile, result) 106 | } 107 | 108 | private fun assertProfilesAreEqual(expected: PlayerProfile, actual: PlayerProfile) 109 | { 110 | for (i in expected.armor.indices) { 111 | assertThat(expected.armor[i].type, equalTo(actual.armor[i].type)) 112 | } 113 | 114 | for (i in expected.inventory.indices) { 115 | assertThat(expected.inventory[i].type, equalTo(actual.inventory[i].type)) 116 | } 117 | 118 | for (i in expected.enderChest.indices) { 119 | assertThat(expected.enderChest[i].type, equalTo(actual.enderChest[i].type)) 120 | } 121 | 122 | assertThat(expected.allowFlight, equalTo(actual.allowFlight)) 123 | assertThat(expected.displayName, equalTo(actual.displayName)) 124 | assertThat(expected.exhaustion, equalTo(actual.exhaustion)) 125 | assertThat(expected.experience, equalTo(actual.experience)) 126 | assertThat(expected.isFlying, equalTo(actual.isFlying)) 127 | assertThat(expected.allowFlight, equalTo(actual.allowFlight)) 128 | assertThat(expected.foodLevel, equalTo(actual.foodLevel)) 129 | assertThat(expected.maxHealth, equalTo(actual.maxHealth)) 130 | assertThat(expected.health, equalTo(actual.health)) 131 | assertThat(expected.gameMode, equalTo(actual.gameMode)) 132 | assertThat(expected.level, equalTo(actual.level)) 133 | assertThat(expected.saturation, equalTo(actual.saturation)) 134 | assertThat(expected.potionEffects, equalTo(actual.potionEffects)) 135 | assertThat(expected.fallDistance, equalTo(actual.fallDistance)) 136 | assertThat(expected.fireTicks, equalTo(actual.fireTicks)) 137 | assertThat(expected.maximumAir, equalTo(actual.maximumAir)) 138 | assertThat(expected.remainingAir, equalTo(actual.remainingAir)) 139 | assertThat(expected.balance, equalTo(actual.balance)) 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/main/kotlin/me/ebonjaeger/perworldinventory/data/FlatFile.kt: -------------------------------------------------------------------------------- 1 | package me.ebonjaeger.perworldinventory.data 2 | 3 | import com.dumptruckman.bukkit.configuration.util.SerializationHelper 4 | import me.ebonjaeger.perworldinventory.ConsoleLogger 5 | import me.ebonjaeger.perworldinventory.initialization.DataDirectory 6 | import me.ebonjaeger.perworldinventory.serialization.LocationSerializer 7 | import me.ebonjaeger.perworldinventory.serialization.PlayerSerializer 8 | import net.minidev.json.JSONObject 9 | import net.minidev.json.JSONStyle 10 | import net.minidev.json.parser.JSONParser 11 | import org.bukkit.GameMode 12 | import org.bukkit.Location 13 | import org.bukkit.entity.Player 14 | import java.io.File 15 | import java.io.FileReader 16 | import java.io.FileWriter 17 | import java.io.IOException 18 | import java.nio.file.FileAlreadyExistsException 19 | import java.nio.file.Files 20 | import javax.inject.Inject 21 | 22 | class FlatFile @Inject constructor(@DataDirectory private val dataDirectory: File) : DataSource 23 | { 24 | 25 | @Suppress("UNCHECKED_CAST") // We know we serialize to a Map since we made it 26 | override fun savePlayer(key: ProfileKey, player: PlayerProfile) 27 | { 28 | val file = getFile(key) 29 | ConsoleLogger.fine("Saving data for player '${player.displayName}'") 30 | ConsoleLogger.debug("Data being saved in file '${file.path}'") 31 | 32 | try 33 | { 34 | createFileIfNotExists(file) 35 | } catch (ex: IOException) 36 | { 37 | if (ex !is FileAlreadyExistsException) 38 | { 39 | ConsoleLogger.severe("Error creating file '${file.path}':", ex) 40 | return 41 | } 42 | } 43 | 44 | ConsoleLogger.fine("Writing player data for player '${player.displayName}' to file") 45 | val map = SerializationHelper.serialize(player) 46 | val json = JSONObject(map as Map) 47 | try 48 | { 49 | FileWriter(file).use { it.write(json.toJSONString(JSONStyle.LT_COMPRESS)) } 50 | } catch (ex: IOException) 51 | { 52 | ConsoleLogger.severe("Could not write data to file '$file':", ex) 53 | } 54 | } 55 | 56 | override fun saveLogout(player: Player) 57 | { 58 | val dir = File(dataDirectory, player.uniqueId.toString()) 59 | val file = File(dir, "last-logout.json") 60 | 61 | try 62 | { 63 | createFileIfNotExists(file) 64 | val data = LocationSerializer.serialize(player.location) 65 | FileWriter(file).use { it.write(data.toJSONString(JSONStyle.LT_COMPRESS)) } 66 | } catch (ex: IOException) 67 | { 68 | if (ex !is FileAlreadyExistsException) 69 | { 70 | ConsoleLogger.severe("Error writing logout location for '${player.name}':", ex) 71 | } 72 | } 73 | } 74 | 75 | override fun saveLocation(player: Player, location: Location) 76 | { 77 | val dir = File(dataDirectory, player.uniqueId.toString()) 78 | val file = File(dir, "last-locations.json") 79 | 80 | try 81 | { 82 | createFileIfNotExists(file) 83 | val data = LocationSerializer.serialize(location) 84 | val key = location.world!!.name // The server will never provide a null world in a Location 85 | 86 | // Get any existing data 87 | val parser = JSONParser(JSONParser.USE_INTEGER_STORAGE) 88 | FileReader(file).use { reader -> 89 | val root = parser.parse(reader) as JSONObject 90 | val locations = if (root.containsKey("locations")) 91 | { 92 | root["locations"] as JSONObject 93 | } else 94 | { 95 | JSONObject() 96 | } 97 | 98 | // If a location for this world already exists, remove it. 99 | if (locations.containsKey(key)) 100 | { 101 | locations.remove(key) 102 | } 103 | 104 | // Write the latest data to disk 105 | locations[key] = data 106 | root["locations"] = locations 107 | FileWriter(file).use { writer -> writer.write(root.toJSONString(JSONStyle.LT_COMPRESS)) } 108 | } 109 | } catch (ex: IOException) 110 | { 111 | if (ex !is FileAlreadyExistsException) 112 | { 113 | ConsoleLogger.severe("Error writing last location for '${player.name}':", ex) 114 | } 115 | } 116 | } 117 | 118 | override fun getPlayer(key: ProfileKey, player: Player): PlayerProfile? 119 | { 120 | val file = getFile(key) 121 | 122 | // If the file does not exist, the player hasn't been to this group before 123 | if (!file.exists()) 124 | { 125 | return null 126 | } 127 | 128 | FileReader(file).use { reader -> 129 | val parser = JSONParser(JSONParser.USE_INTEGER_STORAGE) 130 | val data = parser.parse(reader) as JSONObject 131 | 132 | return if (data.containsKey("==")) { // Data is from ConfigurationSerialization 133 | SerializationHelper.deserialize(data) as PlayerProfile 134 | } else { // Old data format and methods 135 | PlayerSerializer.deserialize(data, player.name, player.inventory.size, player.enderChest.size) 136 | } 137 | } 138 | } 139 | 140 | override fun getLogout(player: Player): Location? 141 | { 142 | val dir = File(dataDirectory, player.uniqueId.toString()) 143 | val file = File(dir, "last-logout.json") 144 | 145 | // This player is likely logging in for the first time 146 | if (!file.exists()) 147 | { 148 | return null 149 | } 150 | 151 | FileReader(file).use { 152 | val parser = JSONParser(JSONParser.USE_INTEGER_STORAGE) 153 | val data = parser.parse(it) as JSONObject 154 | 155 | return LocationSerializer.deserialize(data) 156 | } 157 | } 158 | 159 | override fun getLocation(player: Player, world: String): Location? 160 | { 161 | val dir = File(dataDirectory, player.uniqueId.toString()) 162 | val file = File(dir, "last-locations.json") 163 | 164 | // Clearly they haven't visited any other worlds yet 165 | if (!file.exists()) 166 | { 167 | return null 168 | } 169 | 170 | FileReader(file).use { 171 | val parser = JSONParser(JSONParser.USE_INTEGER_STORAGE) 172 | val root = parser.parse(it) as JSONObject 173 | if (!root.containsKey("locations")) 174 | { 175 | // Somehow the file exists, but still no locations 176 | return null 177 | } 178 | 179 | val locations = root["locations"] as JSONObject 180 | return if (locations.containsKey(world)) 181 | { 182 | LocationSerializer.deserialize(locations["world"] as JSONObject) 183 | } else 184 | { 185 | // They haven't been to this world before, so no data 186 | null 187 | } 188 | } 189 | } 190 | 191 | /** 192 | * Creates the given file if it doesn't exist. 193 | * 194 | * @param file The file to create if necessary 195 | * @return The given file (allows inline use) 196 | * @throws IOException If file could not be created 197 | */ 198 | @Throws(IOException::class) 199 | private fun createFileIfNotExists(file: File): File 200 | { 201 | if (!file.exists()) 202 | { 203 | if (!file.parentFile.exists()) 204 | { 205 | Files.createDirectories(file.parentFile.toPath()) 206 | } 207 | 208 | Files.createFile(file.toPath()) 209 | } 210 | 211 | return file 212 | } 213 | 214 | /** 215 | * Get the data file for a player. 216 | * 217 | * @param key The [ProfileKey] to get the right file 218 | * @return The data file to read from or write to 219 | */ 220 | private fun getFile(key: ProfileKey): File 221 | { 222 | val dir = File(dataDirectory, key.uuid.toString()) 223 | return when(key.gameMode) 224 | { 225 | GameMode.ADVENTURE -> File(dir, key.group.name + "_adventure.json") 226 | GameMode.CREATIVE -> File(dir, key.group.name + "_creative.json") 227 | GameMode.SPECTATOR -> File(dir, key.group.name + "_spectator.json") 228 | GameMode.SURVIVAL -> File(dir, key.group.name + ".json") 229 | } 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /src/test/kotlin/me/ebonjaeger/perworldinventory/command/GroupCommandsTest.kt: -------------------------------------------------------------------------------- 1 | package me.ebonjaeger.perworldinventory.command 2 | 3 | import io.mockk.every 4 | import io.mockk.just 5 | import io.mockk.mockk 6 | import io.mockk.mockkClass 7 | import io.mockk.runs 8 | import io.mockk.verify 9 | import me.ebonjaeger.perworldinventory.Group 10 | import me.ebonjaeger.perworldinventory.GroupManager 11 | import me.ebonjaeger.perworldinventory.TestHelper 12 | import me.ebonjaeger.perworldinventory.TestHelper.mockGroup 13 | import org.bukkit.Bukkit 14 | import org.bukkit.GameMode 15 | import org.bukkit.World 16 | import org.bukkit.command.CommandSender 17 | import org.junit.jupiter.api.BeforeEach 18 | import org.junit.jupiter.api.Test 19 | import kotlin.test.assertEquals 20 | 21 | /** 22 | * Tests for [GroupCommands]. 23 | */ 24 | class GroupCommandsTest 25 | { 26 | 27 | private var groupManager = mockkClass(GroupManager::class) 28 | private val commands = GroupCommands(groupManager) 29 | 30 | @BeforeEach 31 | fun setupMocks() 32 | { 33 | TestHelper.mockBukkit() 34 | 35 | every { groupManager.addGroup(any(), any(), any(), any()) } just runs 36 | every { groupManager.removeGroup(any()) } just runs 37 | every { groupManager.saveGroups() } just runs 38 | } 39 | 40 | @Test 41 | fun addGroupButAlreadyExists() 42 | { 43 | // given 44 | val sender = mockk(relaxed = true) 45 | val group = Group("test", mutableSetOf(), GameMode.SURVIVAL, null) 46 | every { groupManager.getGroup("test") } returns group 47 | 48 | // when 49 | commands.onAddGroup(sender, "test", "survival", "test", "test_nether") 50 | 51 | // then 52 | verify(exactly = 0) { groupManager.addGroup(any(), any(), any(), any()) } 53 | } 54 | 55 | @Test 56 | fun addGroupButInvalidGameMode() 57 | { 58 | // given 59 | val sender = mockk(relaxed = true) 60 | every { groupManager.getGroup("test") } returns null 61 | 62 | // when 63 | commands.onAddGroup(sender, "test", "invalid", "creative") 64 | 65 | // then 66 | verify(exactly = 0) { groupManager.addGroup(any(), any(), any(), any()) } 67 | } 68 | 69 | @Test 70 | fun addGroupSuccessfully() { 71 | // given 72 | val sender = mockk(relaxed = true) 73 | every { groupManager.getGroup("test") } returns null 74 | every { groupManager.addGroup(any(), any(), any(), any()) } just runs 75 | every { groupManager.saveGroups() } just runs 76 | 77 | // when 78 | commands.onAddGroup(sender, "test", "creative", "creative") 79 | 80 | // then 81 | verify { groupManager.addGroup("test", mutableSetOf("creative"), GameMode.CREATIVE, true) } 82 | } 83 | 84 | @Test 85 | fun addWorldInvalidGroup() 86 | { 87 | // given 88 | val sender = mockk(relaxed = true) 89 | every { groupManager.getGroup("bob") } returns null 90 | 91 | // when 92 | commands.onAddWorld(sender, "bob", "bobs_world") 93 | 94 | // then 95 | verify(exactly = 0) { groupManager.saveGroups() } 96 | } 97 | 98 | @Test 99 | fun addWorldInvalidWorld() { 100 | // given 101 | val sender = mockk(relaxed = true) 102 | every { groupManager.getGroup("test") } returns mockGroup("test") 103 | every { Bukkit.getWorld("invalid") } returns null 104 | 105 | // when 106 | commands.onAddWorld(sender, "test", "invalid") 107 | 108 | // then 109 | verify(exactly = 0) { 110 | groupManager.saveGroups() 111 | } 112 | } 113 | 114 | @Test 115 | fun addWorldSuccessfully() 116 | { 117 | // given 118 | val sender = mockk(relaxed = true) 119 | val world = mockk(relaxed = true) 120 | val group = mockGroup("test", mutableSetOf("test")) 121 | 122 | every { Bukkit.getWorld("bob") } returns world 123 | every { world.name } returns "bob" 124 | every { groupManager.getGroup("test") } returns group 125 | 126 | // when 127 | commands.onAddWorld(sender, "test", "bob") 128 | 129 | // then 130 | val expected = Group("test", mutableSetOf("test", "bob"), GameMode.SURVIVAL, null) 131 | assertEquals(expected, group) 132 | verify { groupManager.saveGroups() } 133 | } 134 | 135 | @Test 136 | fun removeGroupInvalidName() { 137 | // given 138 | val sender = mockk(relaxed = true) 139 | every { groupManager.getGroup("invalid") } returns null 140 | 141 | // when 142 | commands.onRemoveGroup(sender, "invalid") 143 | 144 | // then 145 | verify(exactly = 0) { 146 | groupManager.removeGroup(any()) 147 | groupManager.saveGroups() 148 | } 149 | } 150 | 151 | @Test 152 | fun removeGroupSuccessfully() { 153 | // given 154 | val sender = mockk(relaxed = true) 155 | every { groupManager.getGroup("test") } returns mockGroup("test") 156 | every { groupManager.removeGroup(any()) } just runs 157 | every { groupManager.saveGroups() } just runs 158 | 159 | // when 160 | commands.onRemoveGroup(sender, "test") 161 | 162 | // then 163 | verify { groupManager.removeGroup("test") } 164 | verify { groupManager.saveGroups() } 165 | } 166 | 167 | @Test 168 | fun removeWorldInvalidGroup() 169 | { 170 | // given 171 | val sender = mockk(relaxed = true) 172 | every { groupManager.getGroup("bob") } returns null 173 | 174 | // when 175 | commands.onRemoveWorld(sender, "bob", "bobs_world") 176 | 177 | // then 178 | verify(exactly = 0) { groupManager.saveGroups() } 179 | } 180 | 181 | @Test 182 | fun removeWorldInvalidWorld() 183 | { 184 | // given 185 | val sender = mockk(relaxed = true) 186 | every { groupManager.getGroup("test") } returns mockGroup("test") 187 | every { Bukkit.getWorld("invalid") } returns null 188 | 189 | // when 190 | commands.onRemoveWorld(sender, "test", "invalid") 191 | 192 | // then 193 | verify(exactly = 0) { groupManager.saveGroups() } 194 | } 195 | 196 | @Test 197 | fun removeWorldSuccessfully() 198 | { 199 | // given 200 | val sender = mockk(relaxed = true) 201 | val world = mockk(relaxed = true) 202 | val group = mockGroup("test", mutableSetOf("test", "bob")) 203 | 204 | every { Bukkit.getWorld("bob") } returns world 205 | every { world.name } returns "bob" 206 | every { groupManager.getGroup("test") } returns group 207 | 208 | // when 209 | commands.onRemoveWorld(sender, "test", "bob") 210 | 211 | // then 212 | val expected = Group("test", mutableSetOf("test"), GameMode.SURVIVAL, null) 213 | assertEquals(expected, group) 214 | verify { groupManager.saveGroups() } 215 | } 216 | 217 | @Test 218 | fun setRespawnInvalidGroup() 219 | { 220 | // given 221 | val sender = mockk(relaxed = true) 222 | every { groupManager.getGroup("bob") } returns null 223 | 224 | // when 225 | commands.onSetRespawnWorld(sender, "bob", "bobs_world") 226 | 227 | // then 228 | verify(exactly = 0) { groupManager.saveGroups() } 229 | } 230 | 231 | @Test 232 | fun setRespawnInvalidWorld() 233 | { 234 | // given 235 | val sender = mockk(relaxed = true) 236 | every { groupManager.getGroup("test") } returns mockGroup("test") 237 | every { Bukkit.getWorld("invalid") } returns null 238 | 239 | // when 240 | commands.onSetRespawnWorld(sender, "test", "invalid") 241 | 242 | // then 243 | verify(exactly = 0) { groupManager.saveGroups() } 244 | } 245 | 246 | @Test 247 | fun setRespawnSuccessfully() 248 | { 249 | // given 250 | val sender = mockk(relaxed = true) 251 | val world = mockk(relaxed = true) 252 | val group = mockGroup("test", mutableSetOf("test", "bob")) 253 | 254 | every { Bukkit.getWorld("bob") } returns world 255 | every { world.name } returns "bob" 256 | every { groupManager.getGroup("test") } returns group 257 | 258 | // when 259 | commands.onSetRespawnWorld(sender, "test", "bob") 260 | 261 | // then 262 | val expected = Group("test", mutableSetOf("test", "bob"), GameMode.SURVIVAL, "bob") 263 | assertEquals(expected, group) 264 | verify { groupManager.saveGroups() } 265 | } 266 | } 267 | -------------------------------------------------------------------------------- /src/main/kotlin/me/ebonjaeger/perworldinventory/PerWorldInventory.kt: -------------------------------------------------------------------------------- 1 | package me.ebonjaeger.perworldinventory 2 | 3 | import co.aikar.commands.PaperCommandManager 4 | import me.ebonjaeger.perworldinventory.api.PerWorldInventoryAPI 5 | import me.ebonjaeger.perworldinventory.command.* 6 | import me.ebonjaeger.perworldinventory.configuration.MetricsSettings 7 | import me.ebonjaeger.perworldinventory.configuration.PluginSettings 8 | import me.ebonjaeger.perworldinventory.configuration.Settings 9 | import me.ebonjaeger.perworldinventory.data.DataSource 10 | import me.ebonjaeger.perworldinventory.data.DataSourceProvider 11 | import me.ebonjaeger.perworldinventory.data.PlayerProfile 12 | import me.ebonjaeger.perworldinventory.initialization.DataDirectory 13 | import me.ebonjaeger.perworldinventory.initialization.Injector 14 | import me.ebonjaeger.perworldinventory.initialization.InjectorBuilder 15 | import me.ebonjaeger.perworldinventory.initialization.PluginFolder 16 | import me.ebonjaeger.perworldinventory.listener.entity.EntityPortalEventListener 17 | import me.ebonjaeger.perworldinventory.listener.player.* 18 | import org.bstats.bukkit.Metrics 19 | import org.bukkit.Bukkit 20 | import org.bukkit.Server 21 | import org.bukkit.configuration.serialization.ConfigurationSerialization 22 | import org.bukkit.plugin.PluginDescriptionFile 23 | import org.bukkit.plugin.PluginManager 24 | import org.bukkit.plugin.java.JavaPlugin 25 | import org.bukkit.plugin.java.JavaPluginLoader 26 | import java.io.File 27 | import java.nio.file.Files 28 | import java.util.* 29 | 30 | class PerWorldInventory : JavaPlugin 31 | { 32 | 33 | /** 34 | * Get whether or not the server is currently shutting down. 35 | */ 36 | var isShuttingDown = false 37 | private set 38 | 39 | /** 40 | * Get an API class for other plugins to more easily 41 | * integrate with PerWorldInventory. 42 | */ 43 | var api: PerWorldInventoryAPI? = null 44 | private set 45 | 46 | val timeouts = hashMapOf() 47 | var updateTimeoutsTaskId = -1 48 | 49 | private val DATA_DIRECTORY = File(dataFolder, "data") 50 | val SLOT_TIMEOUT = 5 51 | val WORLDS_CONFIG_FILE = File(dataFolder, "worlds.yml") 52 | 53 | constructor(): super() 54 | 55 | /* Constructor used for tests. */ 56 | internal constructor(loader: JavaPluginLoader, description: PluginDescriptionFile, dataFolder: File, file: File) 57 | : super(loader, description, dataFolder, file) 58 | 59 | override fun onEnable() 60 | { 61 | ConsoleLogger.setLogger(logger) 62 | 63 | // Make data folder 64 | if (!Files.exists(DATA_DIRECTORY.toPath())) 65 | { 66 | Files.createDirectories(DATA_DIRECTORY.toPath()) 67 | } 68 | 69 | // Create config file if it does not exist 70 | if (!Files.exists(File(dataFolder, "config.yml").toPath())) 71 | { 72 | saveResource("config.yml", false) 73 | } 74 | 75 | /* Injector initialization */ 76 | val injector = InjectorBuilder().addDefaultHandlers("me.ebonjaeger.perworldinventory").create() 77 | injector.register(PerWorldInventory::class, this) 78 | injector.register(Server::class, server) 79 | injector.register(PluginManager::class, server.pluginManager) 80 | injector.provide(PluginFolder::class, dataFolder) 81 | injector.provide(DataDirectory::class, DATA_DIRECTORY) 82 | injector.registerProvider(DataSource::class, DataSourceProvider::class) 83 | val settings = Settings.create(File(dataFolder, "config.yml")) 84 | injector.register(Settings::class, settings) 85 | 86 | ConsoleLogger.setLogLevel(settings.getProperty(PluginSettings.LOGGING_LEVEL)) 87 | 88 | // Inject and register all the things 89 | setupGroupManager(injector) 90 | injectServices(injector) 91 | registerCommands(injector, injector.getSingleton(GroupManager::class)) 92 | 93 | // Start bStats metrics 94 | if (settings.getProperty(MetricsSettings.ENABLE_METRICS)) 95 | { 96 | startMetrics(settings, injector.getSingleton(GroupManager::class)) 97 | } 98 | 99 | // Start task to prevent item duping across worlds 100 | updateTimeoutsTaskId = server.scheduler.scheduleSyncRepeatingTask( 101 | this, UpdateTimeoutsTask(this), 1L, 1L 102 | ) 103 | 104 | // ConfigurationSerializable classes must be registered as such 105 | ConfigurationSerialization.registerClass(PlayerProfile::class.java) 106 | 107 | ConsoleLogger.fine("PerWorldInventory is enabled with logger level '${settings.getProperty(PluginSettings.LOGGING_LEVEL).name}'") 108 | } 109 | 110 | override fun onDisable() 111 | { 112 | isShuttingDown = true 113 | updateTimeoutsTaskId = -1 114 | timeouts.clear() 115 | server.scheduler.cancelTasks(this) 116 | } 117 | 118 | private fun setupGroupManager(injector: Injector) 119 | { 120 | val groupManager = injector.getSingleton(GroupManager::class) 121 | 122 | if (!Files.exists(WORLDS_CONFIG_FILE.toPath())) 123 | { 124 | saveResource("worlds.yml", false) 125 | } 126 | 127 | groupManager.loadGroups() 128 | } 129 | 130 | internal fun injectServices(injector: Injector) 131 | { 132 | server.pluginManager.registerEvents(injector.getSingleton(InventoryCreativeListener::class), this) 133 | server.pluginManager.registerEvents(injector.getSingleton(PlayerChangedWorldListener::class), this) 134 | server.pluginManager.registerEvents(injector.getSingleton(PlayerGameModeChangeListener::class), this) 135 | server.pluginManager.registerEvents(injector.getSingleton(PlayerQuitListener::class), this) 136 | server.pluginManager.registerEvents(injector.getSingleton(PlayerTeleportListener::class), this) 137 | server.pluginManager.registerEvents(injector.getSingleton(EntityPortalEventListener::class), this) 138 | server.pluginManager.registerEvents(injector.getSingleton(PlayerDeathListener::class), this) 139 | server.pluginManager.registerEvents(injector.getSingleton(PlayerRespawnListener::class), this) 140 | 141 | // The PlayerSpawnLocationEvent is only fired in Spigot 142 | // As of version 1.9.2 143 | if (Bukkit.getVersion().contains("Spigot") && Utils.checkServerVersion(Bukkit.getVersion(), 1, 9, 2)) 144 | { 145 | server.pluginManager.registerEvents(injector.getSingleton(PlayerSpawnLocationListener::class), this) 146 | } 147 | 148 | api = injector.getSingleton(PerWorldInventoryAPI::class) 149 | } 150 | 151 | private fun registerCommands(injector: Injector, groupManager: GroupManager) 152 | { 153 | val commandManager = PaperCommandManager(this) 154 | 155 | commandManager.commandCompletions.registerAsyncCompletion( 156 | "@groups") { groupManager.groups.keys } 157 | 158 | // CommandHelp#showHelp() uses an unstable method internally 159 | commandManager.enableUnstableAPI("help") 160 | 161 | commandManager.registerCommand(HelpCommand(this)) 162 | commandManager.registerCommand(injector.getSingleton(ReloadCommand::class)) 163 | commandManager.registerCommand(injector.getSingleton(ConvertCommand::class)) 164 | commandManager.registerCommand(injector.getSingleton(GroupCommands::class)) 165 | commandManager.registerCommand(injector.getSingleton(MigrateCommand::class)) 166 | } 167 | 168 | /** 169 | * Start sending metrics information to bStats. If so configured, this 170 | * will send the number of configured groups and the number of worlds on 171 | * the server. 172 | * 173 | * @param settings The settings for sending group and world information 174 | */ 175 | private fun startMetrics(settings: Settings, groupManager: GroupManager) 176 | { 177 | val bStats = Metrics(this) 178 | 179 | if (settings.getProperty(MetricsSettings.SEND_NUM_GROUPS)) 180 | { 181 | // Get the total number of configured Groups 182 | bStats.addCustomChart(Metrics.SimplePie("num_groups") { 183 | val numGroups = groupManager.groups.size 184 | 185 | return@SimplePie numGroups.toString() 186 | }) 187 | } 188 | 189 | if (settings.getProperty(MetricsSettings.SEND_NUM_WORLDS)) 190 | { 191 | // Get the total number of worlds (configured or not) 192 | bStats.addCustomChart(Metrics.SimplePie("num_worlds") { 193 | val numWorlds = Bukkit.getWorlds().size 194 | 195 | when 196 | { 197 | numWorlds <= 5 -> return@SimplePie "1-5" 198 | numWorlds <= 10 -> return@SimplePie "6-10" 199 | numWorlds <= 15 -> return@SimplePie "11-15" 200 | numWorlds <= 20 -> return@SimplePie "16-20" 201 | numWorlds <= 25 -> return@SimplePie "21-25" 202 | numWorlds <= 30 -> return@SimplePie "26-30" 203 | else -> return@SimplePie numWorlds.toString() 204 | } 205 | }) 206 | } 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /src/main/kotlin/me/ebonjaeger/perworldinventory/data/PlayerProfile.kt: -------------------------------------------------------------------------------- 1 | package me.ebonjaeger.perworldinventory.data 2 | 3 | import me.ebonjaeger.perworldinventory.serialization.InventoryHelper 4 | import me.ebonjaeger.perworldinventory.serialization.PotionSerializer 5 | import org.bukkit.GameMode 6 | import org.bukkit.attribute.Attribute 7 | import org.bukkit.configuration.serialization.ConfigurationSerializable 8 | import org.bukkit.entity.Player 9 | import org.bukkit.inventory.ItemStack 10 | import org.bukkit.potion.PotionEffect 11 | import org.bukkit.util.NumberConversions 12 | import java.util.* 13 | 14 | /** 15 | * This class is used to represent a Player. 16 | * It contains all of the variables that can be saved, as well as a few things 17 | * for internal use. 18 | */ 19 | data class PlayerProfile(val armor: Array, 20 | val enderChest: Array, 21 | val inventory: Array, 22 | val allowFlight: Boolean, 23 | val displayName: String, 24 | val exhaustion: Float, 25 | val experience: Float, 26 | val isFlying: Boolean, 27 | val foodLevel: Int, 28 | val maxHealth: Double, 29 | val health: Double, 30 | val gameMode: GameMode, 31 | val level: Int, 32 | val saturation: Float, 33 | val potionEffects: MutableCollection, 34 | val fallDistance: Float, 35 | val fireTicks: Int, 36 | val maximumAir: Int, 37 | val remainingAir: Int, 38 | val balance: Double) : ConfigurationSerializable { 39 | 40 | /** 41 | * Simple constructor to use when you have a [Player] object to work with. 42 | * 43 | * @param player The player to build this profile from 44 | * @param balance The amount of currency the player has 45 | */ 46 | constructor(player: Player, 47 | balance: Double) : this( 48 | player.inventory.armorContents, 49 | player.enderChest.contents, 50 | player.inventory.contents, 51 | player.allowFlight, 52 | player.displayName, 53 | player.exhaustion, 54 | player.exp, 55 | player.isFlying, 56 | player.foodLevel, 57 | player.getAttribute(Attribute.GENERIC_MAX_HEALTH)!!.baseValue, // If this is ever null, I will be very surprised 58 | player.health, 59 | player.gameMode, 60 | player.level, 61 | player.saturation, 62 | player.activePotionEffects, 63 | player.fallDistance, 64 | player.fireTicks, 65 | player.maximumAir, 66 | player.remainingAir, 67 | balance) 68 | 69 | companion object { 70 | 71 | /** 72 | * Deserialize a [Map] into a [PlayerProfile]. 73 | * 74 | * @param map The map to deserialize 75 | * @return The profile from the given data 76 | * @see ConfigurationSerializable 77 | */ 78 | @JvmStatic 79 | fun deserialize(map: Map): PlayerProfile { 80 | val inventory = map["inventory"] as Map<*, *> 81 | 82 | /* Inventory contents */ 83 | val contentsList = inventory["contents"] as List<*> 84 | val contents = InventoryHelper.listToInventory(contentsList) 85 | 86 | /* Armor contents */ 87 | val armorList = inventory["armor"] as List<*> 88 | val armor = InventoryHelper.listToInventory(armorList) 89 | 90 | /* Ender Chest contents */ 91 | val enderChestList = map["ender-chest"] as List<*> 92 | val enderChest = InventoryHelper.listToInventory(enderChestList) 93 | 94 | /* Player stats */ 95 | val stats = map["stats"] as MutableMap<*, *> 96 | 97 | /* Potion effects */ 98 | val potionsList = map["potion-effects"] as List<*> 99 | val potions = mutableListOf() 100 | potionsList.forEach { pot -> potions.add(pot as PotionEffect) } 101 | 102 | /* Put it all together */ 103 | return PlayerProfile( 104 | armor, 105 | enderChest, 106 | contents, 107 | stats["can-fly"] as Boolean, 108 | stats["display-name"] as String, 109 | stats["exhaustion"] as Float, 110 | stats["exp"] as Float, 111 | stats["flying"] as Boolean, 112 | stats["food"] as Int, 113 | NumberConversions.toDouble(stats["max-health"]), 114 | NumberConversions.toDouble(stats["health"]), 115 | GameMode.valueOf(stats["gamemode"] as String), 116 | stats["level"] as Int, 117 | stats["saturation"] as Float, 118 | potions, 119 | stats["fallDistance"] as Float, 120 | stats["fireTicks"] as Int, 121 | stats["maxAir"] as Int, 122 | stats["remainingAir"] as Int, 123 | NumberConversions.toDouble(map["balance"]) 124 | ) 125 | } 126 | } 127 | 128 | /** 129 | * Serialize a [PlayerProfile] to it's [Map] representation. 130 | * 131 | * @return A Map representing the profile's state 132 | * @see ConfigurationSerializable.serialize 133 | */ 134 | override fun serialize(): MutableMap { 135 | val map = mutableMapOf() 136 | 137 | /* Player inventories */ 138 | val contents = InventoryHelper.serializeInventory(this.inventory) 139 | val armor = InventoryHelper.serializeInventory(this.armor) 140 | val inventory = linkedMapOf(Pair("contents", contents), Pair("armor", armor)) 141 | val enderChest = InventoryHelper.serializeInventory(this.enderChest) 142 | 143 | /* Player stats */ 144 | val stats = linkedMapOf() 145 | stats["can-fly"] = this.allowFlight 146 | stats["display-name"] = this.displayName 147 | stats["exhaustion"] = this.exhaustion 148 | stats["exp"] = this.experience 149 | stats["flying"] = this.isFlying 150 | stats["food"] = this.foodLevel 151 | stats["gamemode"] = this.gameMode.toString() 152 | stats["max-health"] = this.maxHealth 153 | stats["health"] = this.health 154 | stats["level"] = this.level 155 | stats["saturation"] = this.saturation 156 | stats["fallDistance"] = this.fallDistance 157 | stats["fireTicks"] = this.fireTicks 158 | stats["maxAir"] = this.maximumAir 159 | stats["remainingAir"] = this.remainingAir 160 | 161 | val potionEffects = PotionSerializer.serialize(this.potionEffects) 162 | 163 | map["data-format"] = 4 164 | map["inventory"] = inventory 165 | map["ender-chest"] = enderChest 166 | map["stats"] = stats 167 | map["potion-effects"] = potionEffects 168 | map["balance"] = this.balance 169 | 170 | return map 171 | } 172 | 173 | override fun equals(other: Any?): Boolean { 174 | if (this === other) return true 175 | if (other !is PlayerProfile) return false 176 | 177 | if (!Arrays.equals(armor, other.armor)) return false 178 | if (!Arrays.equals(enderChest, other.enderChest)) return false 179 | if (!Arrays.equals(inventory, other.inventory)) return false 180 | if (allowFlight != other.allowFlight) return false 181 | if (displayName != other.displayName) return false 182 | if (exhaustion != other.exhaustion) return false 183 | if (experience != other.experience) return false 184 | if (isFlying != other.isFlying) return false 185 | if (foodLevel != other.foodLevel) return false 186 | if (maxHealth != other.maxHealth) return false 187 | if (health != other.health) return false 188 | if (gameMode != other.gameMode) return false 189 | if (level != other.level) return false 190 | if (saturation != other.saturation) return false 191 | if (potionEffects != other.potionEffects) return false 192 | if (fallDistance != other.fallDistance) return false 193 | if (fireTicks != other.fireTicks) return false 194 | if (maximumAir != other.maximumAir) return false 195 | if (remainingAir != other.remainingAir) return false 196 | if (balance != other.balance) return false 197 | 198 | return true 199 | } 200 | 201 | override fun hashCode(): Int { 202 | var result = Arrays.hashCode(armor) 203 | result = 31 * result + Arrays.hashCode(enderChest) 204 | result = 31 * result + Arrays.hashCode(inventory) 205 | result = 31 * result + allowFlight.hashCode() 206 | result = 31 * result + displayName.hashCode() 207 | result = 31 * result + exhaustion.hashCode() 208 | result = 31 * result + experience.hashCode() 209 | result = 31 * result + isFlying.hashCode() 210 | result = 31 * result + foodLevel 211 | result = 31 * result + maxHealth.hashCode() 212 | result = 31 * result + health.hashCode() 213 | result = 31 * result + gameMode.hashCode() 214 | result = 31 * result + level 215 | result = 31 * result + saturation.hashCode() 216 | result = 31 * result + potionEffects.hashCode() 217 | result = 31 * result + fallDistance.hashCode() 218 | result = 31 * result + fireTicks 219 | result = 31 * result + maximumAir 220 | result = 31 * result + remainingAir 221 | result = 31 * result + balance.hashCode() 222 | return result 223 | } 224 | } 225 | --------------------------------------------------------------------------------