diff --git a/addons/Kingdoms-Addon-EngineHub-1.0.0.2.jar b/addons/Kingdoms-Addon-EngineHub-1.0.0.3.jar similarity index 86% rename from addons/Kingdoms-Addon-EngineHub-1.0.0.2.jar rename to addons/Kingdoms-Addon-EngineHub-1.0.0.3.jar index 05415522d..d021b7c06 100644 Binary files a/addons/Kingdoms-Addon-EngineHub-1.0.0.2.jar and b/addons/Kingdoms-Addon-EngineHub-1.0.0.3.jar differ diff --git a/addons/Kingdoms-Addon-Map-Viewers-2.1.0.4.jar b/addons/Kingdoms-Addon-Map-Viewers-2.1.0.5.jar similarity index 76% rename from addons/Kingdoms-Addon-Map-Viewers-2.1.0.4.jar rename to addons/Kingdoms-Addon-Map-Viewers-2.1.0.5.jar index 6ce044825..ad674f1e4 100644 Binary files a/addons/Kingdoms-Addon-Map-Viewers-2.1.0.4.jar and b/addons/Kingdoms-Addon-Map-Viewers-2.1.0.5.jar differ diff --git a/addons/Kingdoms-Addon-Outposts-3.0.1.6.5.jar b/addons/Kingdoms-Addon-Outposts-3.0.1.6.6.jar similarity index 72% rename from addons/Kingdoms-Addon-Outposts-3.0.1.6.5.jar rename to addons/Kingdoms-Addon-Outposts-3.0.1.6.6.jar index 70416a394..eacfaa5be 100644 Binary files a/addons/Kingdoms-Addon-Outposts-3.0.1.6.5.jar and b/addons/Kingdoms-Addon-Outposts-3.0.1.6.6.jar differ diff --git a/addons/Kingdoms-Addon-Peace-Treaties-1.2.6.0.5.jar b/addons/Kingdoms-Addon-Peace-Treaties-1.2.6.0.6.jar similarity index 73% rename from addons/Kingdoms-Addon-Peace-Treaties-1.2.6.0.5.jar rename to addons/Kingdoms-Addon-Peace-Treaties-1.2.6.0.6.jar index acda845f2..180be133b 100644 Binary files a/addons/Kingdoms-Addon-Peace-Treaties-1.2.6.0.5.jar and b/addons/Kingdoms-Addon-Peace-Treaties-1.2.6.0.6.jar differ diff --git a/addons/addon-meta.yml b/addons/addon-meta.yml index 99b9d6cf6..24125b8bd 100644 --- a/addons/addon-meta.yml +++ b/addons/addon-meta.yml @@ -1,16 +1,19 @@ outposts: - version: '3.0.1.6.5' - url: 'https://github.com/CryptoMorin/KingdomsX/blob/master/addons/Kingdoms-Addon-Outposts-3.0.1.6.5.jar?raw=true' + version: '3.0.1.6.6' + url: 'https://github.com/CryptoMorin/KingdomsX/blob/master/addons/Kingdoms-Addon-Outposts-3.0.1.6.6.jar?raw=true' supported-core-version: 1.17.2-ALHPA + map-viewers: - version: '2.1.0.4' - url: 'https://github.com/CryptoMorin/KingdomsX/blob/master/addons/Kingdoms-Addon-Map-Viewers-2.1.0.4.jar?raw=true' + version: '2.1.0.5' + url: 'https://github.com/CryptoMorin/KingdomsX/blob/master/addons/Kingdoms-Addon-Map-Viewers-2.1.0.5.jar?raw=true' supported-core-version: 1.17.2-ALHPA + peace-treaties: - version: '1.2.6.0.5' - url: 'https://github.com/CryptoMorin/KingdomsX/blob/master/addons/Kingdoms-Addon-Peace-Treaties-1.2.6.0.5.jar?raw=true' + version: '1.2.6.0.6' + url: 'https://github.com/CryptoMorin/KingdomsX/blob/master/addons/Kingdoms-Addon-Peace-Treaties-1.2.6.0.6.jar?raw=true' supported-core-version: 1.17.2-ALHPA + enginehub: - version: '1.0.0.2' - url: 'https://github.com/CryptoMorin/KingdomsX/blob/master/addons/Kingdoms-Addon-EngineHub-1.0.0.2.jar?raw=true' + version: '1.0.0.3' + url: 'https://github.com/CryptoMorin/KingdomsX/blob/master/addons/Kingdoms-Addon-EngineHub-1.0.0.3.jar?raw=true' supported-core-version: 1.17.2-ALHPA \ No newline at end of file diff --git a/core/service/mythicmobs/v5/src/main/java/org/kingdoms/services/mythicmobs/conditions/KingdomsMythicMobConditionListener.java b/core/service/mythicmobs/v5/src/main/java/org/kingdoms/services/mythicmobs/conditions/KingdomsMythicMobConditionListener.java new file mode 100644 index 000000000..a458dd403 --- /dev/null +++ b/core/service/mythicmobs/v5/src/main/java/org/kingdoms/services/mythicmobs/conditions/KingdomsMythicMobConditionListener.java @@ -0,0 +1,21 @@ +package org.kingdoms.services.mythicmobs.conditions; + +import io.lumine.mythic.api.skills.conditions.ISkillCondition; +import io.lumine.mythic.bukkit.events.MythicConditionLoadEvent; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; + +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; + +public final class KingdomsMythicMobConditionListener implements Listener { + protected static final Map CONDITIONS = new HashMap<>(); + + @EventHandler + public void onConditionLoad(MythicConditionLoadEvent event) { + String conditionName = event.getConditionName().toLowerCase(Locale.ENGLISH); + ISkillCondition condition = CONDITIONS.get(conditionName); + if (condition != null) event.register(condition); + } +} diff --git a/core/service/mythicmobs/v5/src/main/java/org/kingdoms/services/mythicmobs/conditions/MythicMobConditionRegistry.java b/core/service/mythicmobs/v5/src/main/java/org/kingdoms/services/mythicmobs/conditions/MythicMobConditionRegistry.java new file mode 100644 index 000000000..43f457fa5 --- /dev/null +++ b/core/service/mythicmobs/v5/src/main/java/org/kingdoms/services/mythicmobs/conditions/MythicMobConditionRegistry.java @@ -0,0 +1,8 @@ +package org.kingdoms.services.mythicmobs.conditions; + +public final class MythicMobConditionRegistry { + public static void register(String name, SimpleRelationalChecker checker) { + String id = "kingdoms_" + name; + KingdomsMythicMobConditionListener.CONDITIONS.put(id, new RelationalMythicMobSkillCondition(id, checker)); + } +} diff --git a/core/service/mythicmobs/v5/src/main/java/org/kingdoms/services/mythicmobs/conditions/RelationalMythicMobSkillCondition.java b/core/service/mythicmobs/v5/src/main/java/org/kingdoms/services/mythicmobs/conditions/RelationalMythicMobSkillCondition.java new file mode 100644 index 000000000..e9557c4b1 --- /dev/null +++ b/core/service/mythicmobs/v5/src/main/java/org/kingdoms/services/mythicmobs/conditions/RelationalMythicMobSkillCondition.java @@ -0,0 +1,23 @@ +package org.kingdoms.services.mythicmobs.conditions; + +import io.lumine.mythic.api.adapters.AbstractEntity; +import io.lumine.mythic.api.skills.conditions.IEntityComparisonCondition; +import io.lumine.mythic.core.skills.SkillCondition; + +/** + * SkillCondition is an abstract class that handles many ISkillConditions that the class implements. + */ +public class RelationalMythicMobSkillCondition extends SkillCondition implements IEntityComparisonCondition { + private final SimpleRelationalChecker checker; + + public RelationalMythicMobSkillCondition(String line, SimpleRelationalChecker checker) { + super(line); + this.checker = checker; + } + + @Override + public boolean check(AbstractEntity caster, AbstractEntity target) { + // Caster must be a player and be in a kingdom. + return checker.check(caster.getBukkitEntity(), target.getBukkitEntity()); + } +} diff --git a/core/service/mythicmobs/v5/src/main/java/org/kingdoms/services/mythicmobs/conditions/SimpleRelationalChecker.java b/core/service/mythicmobs/v5/src/main/java/org/kingdoms/services/mythicmobs/conditions/SimpleRelationalChecker.java new file mode 100644 index 000000000..1e55f1de3 --- /dev/null +++ b/core/service/mythicmobs/v5/src/main/java/org/kingdoms/services/mythicmobs/conditions/SimpleRelationalChecker.java @@ -0,0 +1,7 @@ +package org.kingdoms.services.mythicmobs.conditions; + +import org.bukkit.entity.Entity; + +public interface SimpleRelationalChecker { + boolean check(Entity caster, Entity target); +} diff --git a/core/service/vanish/src/main/java/org/kingdoms/services/vanish/ServiceCMI.java b/core/service/vanish/src/main/java/org/kingdoms/services/vanish/ServiceCMI.java index ebae2d48d..9b7066376 100644 --- a/core/service/vanish/src/main/java/org/kingdoms/services/vanish/ServiceCMI.java +++ b/core/service/vanish/src/main/java/org/kingdoms/services/vanish/ServiceCMI.java @@ -4,6 +4,8 @@ import com.Zrips.CMI.Containers.CMIUser; import org.bukkit.entity.Player; +import java.util.UUID; + public final class ServiceCMI implements ServiceCommons { @Override public boolean isVanished(Player player) { @@ -33,8 +35,8 @@ public boolean isInGodMode(Player player) { } @Override - public boolean isIgnoring(Player ignorant, Player ignoring) { + public boolean isIgnoring(Player ignorant, UUID ignoring) { CMIUser user = getUser(ignorant); - return user != null && user.isIgnoring(ignoring.getUniqueId()); + return user != null && user.isIgnoring(ignoring); } } diff --git a/core/service/vanish/src/main/java/org/kingdoms/services/vanish/ServiceCommons.java b/core/service/vanish/src/main/java/org/kingdoms/services/vanish/ServiceCommons.java index 6f5ccb13f..f67c23122 100644 --- a/core/service/vanish/src/main/java/org/kingdoms/services/vanish/ServiceCommons.java +++ b/core/service/vanish/src/main/java/org/kingdoms/services/vanish/ServiceCommons.java @@ -3,10 +3,12 @@ import org.bukkit.entity.Player; import org.kingdoms.services.Service; +import java.util.UUID; + public interface ServiceCommons extends Service { boolean isVanished(Player player); boolean isInGodMode(Player player); - boolean isIgnoring(Player ignorant, Player ignoring); + boolean isIgnoring(Player ignorant, UUID ignoring); } diff --git a/core/service/vanish/src/main/java/org/kingdoms/services/vanish/ServiceEssentialsX.java b/core/service/vanish/src/main/java/org/kingdoms/services/vanish/ServiceEssentialsX.java index 9c1bebf23..55efb7166 100644 --- a/core/service/vanish/src/main/java/org/kingdoms/services/vanish/ServiceEssentialsX.java +++ b/core/service/vanish/src/main/java/org/kingdoms/services/vanish/ServiceEssentialsX.java @@ -4,6 +4,8 @@ import org.bukkit.Bukkit; import org.bukkit.entity.Player; +import java.util.UUID; + // https://github.com/EssentialsX/Essentials/blob/2.x/Essentials/src/main/java/com/earth2me/essentials/User.java public final class ServiceEssentialsX implements ServiceCommons { private static final Essentials ESS = (Essentials) Bukkit.getPluginManager().getPlugin("Essentials"); @@ -19,7 +21,7 @@ public boolean isInGodMode(Player player) { } @Override - public boolean isIgnoring(Player ignorant, Player ignoring) { + public boolean isIgnoring(Player ignorant, UUID ignoring) { return ESS.getUser(ignorant).isIgnoredPlayer(ESS.getUser(ignoring)); } } diff --git a/core/src/main/resources/Structures/outpost.yml b/core/src/main/resources/Structures/outpost.yml index 54f0d64d6..134f3f4b2 100644 --- a/core/src/main/resources/Structures/outpost.yml +++ b/core/src/main/resources/Structures/outpost.yml @@ -8,6 +8,18 @@ : HAY_BLOCK : HAY_BLOCK +'[fn-base]': &fn-base + args: [ "", "" ] + return: + buy: 'percentOf(10, * (initial / stock)) + ( * (initial / stock))' + sell: ' * (initial / stock)' + stock: + initial: 10000 + max: 100000 + min: 500 + item: + material: + name: "&eOutpost" default-name: 'Outpost' type: outpost @@ -16,14 +28,49 @@ limits: per-land: 1 total: 10 -# A kingdom-wide limit that prevents players from -# purchasing certain items after a certain amount is purchased. -purchase-limits: - diamonds: # This name doesn't matter - # Here you can use ItemMatcher properties - # https://github.com/CryptoMorin/KingdomsX/wiki/Config#item-matchers - material: DIAMOND - purchase-limit: # This special section needs to be present +# The items that can be sold in the outpost structure. +stocks: + # The name of each entry is basically the ID of the item, + # this ID is used for saving server-wide stock data and being + # used in the GUI with the "stock-" prefix. + # For example, this particular item will be referenced as "stock-diamonds" in the GUI. + diamonds: # <--- Note this name is important + # The amount of resource points used for buying this item from the structure. + # Here you can use the special placeholder "stock" which is the amount of this + # item that is held by the server-wide stock and can be used to adjust prices. + # Note: Other options below use the *fn-base function which basically repeats this + # equation for easier configuration. + buy: 'percentOf(10, 60 * (initial / stock)) + (60 * (initial / stock))' + + # The amount of resource points used for selling this item to the structure. + # Supports "stock" placeholder just like the buy option. + sell: '60 * (initial / stock)' + + # Settings for the "stock" variable used for buy/sell options + # This is a server-wide variable shared between all outpost structures. + # When players buy this item from an outpost, it'll decrease the amount + # of stock depending on how much they bought, when they sell the same item, + # the market stock will increase. + stock: &stock + # The initial value assigned. This is permanent and the server + # will save this data, so changing this option will not work + # unless you reset your data. + initial: 10000 + + # No matter how many items the players buy/sell the stock value + # will not go out of this range. + max: 100000 + min: 500 + + # The item given to the player after they purchase it. + # (The amount is adjusted automatically) + # The items sold will have to match this exact same description. + item: + material: DIAMOND + + # A kingdom-wide limit that prevents players from + # purchasing certain items after a certain amount is purchased. + purchase-limit: # The cooldown doesn't persist between restarts. # We're using math here to make it easier to understand. @@ -35,11 +82,58 @@ purchase-limits: # Players will not be able to purchase items if the total cooldown # is going to be more than this limit. max-cooldown: 6hrs # %outpost_max_cooldown% - xp: - material: EXPERIENCE_BOTTLE + + emerald: *fn-base [ 70, EMERALD ] + gold-ingot: *fn-base [ 50, GOLD_INGOT ] + iron-ingot: *fn-base [ 40, IRON_INGOT ] + nether-brick: *fn-base [ 30, NETHER_BRICK ] + brick: *fn-base [ 20, BRICK ] + lapis-lazuli: *fn-base [ 15, LAPIS_LAZULI ] + coal: *fn-base [ 3, COAL ] + charcoal: *fn-base [ 2, CHARCOAL ] + + ender-eye: *fn-base [ 1000, ENDER_EYE ] + ender-pearl: *fn-base [ 100, ENDER_PEARL ] + dragon-breath: *fn-base [ 3000, DRAGON_BREATH ] + blaze-rod: *fn-base [ 20, BLAZE_ROD ] + blaze-powder: *fn-base [ 30, BLAZE_POWDER ] + prismarine-crystals: *fn-base [ 20, PRISMARINE_CRYSTALS ] + + golden-carrot: *fn-base [ 50, GOLDEN_CARROT ] + magma-cream: *fn-base [ 50, MAGMA_CREAM ] + slime-ball: *fn-base [ 30, SLIME_BALL ] + flint: *fn-base [ 1, FLINT ] + book: *fn-base [ 10, BOOK ] + + xp-bottle: + <<: *fn-base [ 30, EXPERIENCE_BOTTLE ] purchase-limit: cooldown-per-item: '[1hr] / 64' - max-cooldown: 1hr + max-cooldown: 1hrs + + cooked-rabbit: *fn-base [ 3, COOKED_RABBIT ] + cooked-beef: *fn-base [ 2, COOKED_BEEF ] + cooked-porkchop: *fn-base [ 2, COOKED_PORKCHOP ] + cooked-chicken: *fn-base [ 1, COOKED_CHICKEN ] + cooked-fish: *fn-base [ 1, COOKED_COD ] + cooked-salmon: *fn-base [ 1, COOKED_SALMON ] + cooked-mutton: *fn-base [ 1, COOKED_MUTTON ] + + wool: *fn-base [ 15, WHITE_WOOL ] + string: *fn-base [ 1, STRING ] + bucket: *fn-base [ 80, BUCKET ] + bone-meal: *fn-base [ 3, BONE_MEAL ] + rail: *fn-base [ 3, RAIL ] + glass: *fn-base [ 25, GLASS ] + ghast-tear: *fn-base [ 100, GHAST_TEAR ] + + redstone-torch: *fn-base [ 5, REDSTONE_TORCH ] + repeater: *fn-base [ 10, REPEATER ] + comparator: *fn-base [ 15, COMPARATOR ] + dispenser: *fn-base [ 30, DISPENSER ] + dropper: *fn-base [ 20, DROPPER ] + hopper: *fn-base [ 200, HOPPER ] + piston: *fn-base [ 80, PISTON ] particles: 1: @@ -63,4 +157,4 @@ item: - "&7It enables you to access turrets, upgrades," - "and unlockable structures." - "&7Outposts can produce exp bottles from" - - "resource points aswell." \ No newline at end of file + - "resource points aswell." diff --git a/core/src/main/resources/Turrets/healing.yml b/core/src/main/resources/Turrets/healing.yml index 21f98f6ac..36dca6447 100644 --- a/core/src/main/resources/Turrets/healing.yml +++ b/core/src/main/resources/Turrets/healing.yml @@ -24,6 +24,12 @@ entities: whitelist: true list: [ PLAYER ] +# Repairs a random player armor if they're wearing one. +# This only works if the player needs healing so it won't work +# when on full health. It works exactly like mending except that +# if all armors are fully repaired, it doesn't give you exp. +repair-armor: lvl * 3 + fire: 100 particle: 1: diff --git a/core/src/main/resources/chat.yml b/core/src/main/resources/chat.yml index 8571bc6fe..bc4d1ea33 100644 --- a/core/src/main/resources/chat.yml +++ b/core/src/main/resources/chat.yml @@ -99,6 +99,16 @@ channels: # Set to ~ (without quotes) to disable. direct-prefix: '@' +# A way for players to show the item they're holding to other +# players in the chat with a simple hover message. +show-item: + # Currently only the main hand is supported. + main-hand: + replace: + '[i]': &show-item '{$sep}[%item_displayname%{? item_amount == 1 ? "" : " &9x%item_amount%"}{$sep}]' + '[item]': *show-item + '[show]': *show-item + # The chat priority that kingdom handles. Note that changing this will not disable the chat in any way. # This is used when you're using another plugin that manages chat spams and other restrictions. You might want to increase the priorty in these cases. # This option requires a restart to work. diff --git a/core/src/main/resources/config.yml b/core/src/main/resources/config.yml index 9495a7278..588f7a385 100644 --- a/core/src/main/resources/config.yml +++ b/core/src/main/resources/config.yml @@ -306,7 +306,8 @@ database: ssl: # Enable SSL connections. It's not recommended to disable it. - enabled: true + # Most people don't have a SSL connection setup, so this is disabled by default. + enabled: false # Whether invalid host names should be allowed. # Take care before setting this to false, as it makes the application susceptible to man-in-the-middle attacks. @@ -418,6 +419,16 @@ commands: # cooldown: 0 # disabled-worlds: [] + # The default permission to assign to this command. Some commands have default permission that + # allows everyone to uses them, and removing the permission can be difficult at times using a + # permission plugin, so this option handles that. + # - Values: + # * OP: Only opped players (/op) can use this command. + # * NOT_OP: Only players that are not opped can use this command. + # * EVERYONE: Everyone can use this command. + # * NO_ONE: No one can use this command. + # permission-default: OP | NOT_OP | EVERYONE | NO_ONE + # To edit the command names and aliases refer to your language file. # You cannot move the position of command arguments or change their group/parent @@ -1538,7 +1549,16 @@ home: # If safe home is enabled, it'll set the home in the # center of the block without changing the rotation. # This option basically forces /k sethome centerAxis - safe: false + center: false + + # If enabled, Checks if the kingdom home is safe before teleporting. + # If not it'll try to find a safe location to teleport to within + # 5 block radius. + # If there are no safe locations, it'll warn the player if they still + # want to teleport anyway. + check-safe: + self: true + others: true # Used to prevent players from escaping PvP in seconds. # Teleportation will be cancelled if the player gets damaged. diff --git a/core/src/main/resources/guis/structures/nexus/logs.yml b/core/src/main/resources/guis/structures/nexus/logs.yml index c0f3eb7d0..fcf8dca8c 100644 --- a/core/src/main/resources/guis/structures/nexus/logs.yml +++ b/core/src/main/resources/guis/structures/nexus/logs.yml @@ -210,6 +210,14 @@ rows: 6 {$p}Shield Duration{$sep}: {$s}%old-shield-time% {$sep}➜ {$s}%new-shield-time% {$p}Resource Points Cost{$sep}: {$s}%fancy@resource_points% {$p}At{$sep}: {$s}%time% + shield-deactivation: + material: SHIELD + name: '&5Shield Deactivation' + lore: | + {$s}%player% {$p}has deactivated the + kingdom's shield. + {$p}Shield Duration{$sep}: {$s}%old-shield-time% + {$p}At{$sep}: {$s}%time% kingdom-relation-change: material: NETHER_STAR name: '&9Relationship Change' @@ -383,6 +391,14 @@ rows: 6 lore: | {$p}Your kingdom has left {$s}%kingdoms_nation_name% {$p}nation. + {$p}At{$sep}: {$s}%time% + kingdom-vault-item-move: + material: PAPER + name: "&cKingdom Vault Item Move" + lore: | + {$p}Changes{$sep}: + %changes% + {$p}At{$sep}: {$s}%time% 'PeaceTreaties:received': material: BOOK diff --git a/core/src/main/resources/guis/structures/nexus/nexus.yml b/core/src/main/resources/guis/structures/nexus/nexus.yml index 9707108b8..eedc1334f 100644 --- a/core/src/main/resources/guis/structures/nexus/nexus.yml +++ b/core/src/main/resources/guis/structures/nexus/nexus.yml @@ -2,12 +2,13 @@ title: "&2&l%kingdoms_kingdom_name%'s &6&lNexus" rows: 6 sound: BLOCK_BEACON_ACTIVATE, 0.3 -icon: "https://upload.wikimedia.org/wikipedia/commons/a/a0/Nexus-Logo.png" -form-type: CUSTOM -form-options: - settings: - component-type: LABEL - text: "&5Settings" +forms: + icon: "https://upload.wikimedia.org/wikipedia/commons/a/a0/Nexus-Logo.png" + type: CUSTOM + options: + settings: + component-type: LABEL + text: "&5Settings" options: settings: diff --git a/core/src/main/resources/guis/structures/nexus/ranks/permissions/categories.yml b/core/src/main/resources/guis/structures/nexus/ranks/permissions/categories.yml new file mode 100644 index 000000000..f36f84f01 --- /dev/null +++ b/core/src/main/resources/guis/structures/nexus/ranks/permissions/categories.yml @@ -0,0 +1,104 @@ +title: '{$sep}-=( {$p}Permissions {$sep})=-' +rows: 3 + +options: + decoration: + slots: [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 17, 18, 19, 20, 21, 23, 24, 25, 26 ] + material: BLACK_STAINED_GLASS_PANE + name: '' + + category-building: + name: "{$p}Building" + material: DIAMOND_PICKAXE + posx: 2 + posy: 2 + lore: | + {$sep}Permissions related to building. + + {$dot} {$s}Building in kingdom lands in general. + {$dot} {$s}Building in the nexus land only. + {$dot} {$s}Building in own claimed lands only. + + category-members: + name: "{$p}Members" + material: PLAYER_HEAD + posx: 3 + posy: 2 + lore: | + {$sep}Permissions related to managing members. + + {$dot} {$s}/k invsee + {$dot} {$s}/k invite + {$dot} {$s}/k kick + {$dot} {$s}Exclude Taxes + + category-economy: + name: "{$p}Economy" + material: CHEST + posx: 4 + posy: 2 + lore: | + {$sep}Permissions related to managing + resource points & money. + + {$dot} {$s}/k bank {$s}withdraw + {$dot} {$s}Upgrading structures, turrets, etc + {$dot} {$s}Using outpost structures + + + category-diplomacy: + name: "{$p}Diplomacy" + material: BOOK + posx: 6 + posy: 2 + lore: | + {$sep}Permissions related to managing + kingdom settings and relationships + with other kingdoms. + + {$dot} {$s}/k ally {$sep}| {$s}enemy {$sep}| {$s}truce {$sep}| {$s}revoke + {$dot} {$s}/k broadcast + {$dot} {$s}Modify relationship attributes + {$dot} {$s}Modify kingdom settings + {$dot} {$s}View audit logs + {$dot} {$s}Change member ranks & modify ranks + {$dot} {$s}Read mails + {$dot} {$s}Rename kingdom name, tags and change lore + + category-land: + name: "{$p}Land" + material: GRASS_BLOCK + posx: 7 + posy: 2 + lore: | + {$sep}Permissions related to managing + your kingdom lands. + + {$dot} {$s}/k claim + {$dot} {$s}/k unclaim + {$dot} {$s}/k invade + {$dot} {$s}Placing/breaking turrets + {$dot} {$s}Placing/breaking structures + {$dot} {$s}Interacting/using blocks + + category-others: + name: "{$p}Others" + material: PAPER + posx: 8 + posy: 2 + lore: | + {$sep}Other permissions that don't have + a specific category. + + {$dot} {$s}/k fly + {$dot} {$s}/k home + {$dot} {$s}/k sethome + {$dot} {$s}Accessing nexus vault + {$dot} {$s}Bypassing protection signs + {$dot} {$s}Instantly tp with /k tpa + + back: + posx: 5 + posy: 3 + material: BARRIER + name: '{$p}Back' diff --git a/core/src/main/resources/guis/structures/nexus/ranks/permissions.yml b/core/src/main/resources/guis/structures/nexus/ranks/permissions/permissions.yml similarity index 98% rename from core/src/main/resources/guis/structures/nexus/ranks/permissions.yml rename to core/src/main/resources/guis/structures/nexus/ranks/permissions/permissions.yml index a837b5b5e..546d9c83f 100644 --- a/core/src/main/resources/guis/structures/nexus/ranks/permissions.yml +++ b/core/src/main/resources/guis/structures/nexus/ranks/permissions/permissions.yml @@ -1,4 +1,6 @@ -title: '{$sep}-=( {$p}Permissions {$sep})=-' +# %permission_category_name% is the item name used +# for categories in categories.yml +title: '{$sep}-=( {$p}%permission_category_name% {$sep})=-' rows: 6 (import): pagination: { } @@ -277,7 +279,7 @@ rows: 6 lore: | &7Send new mails and reply. skull: "eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvOTVmYmJjNjI1ZmE0ZWI2NDk2YmU4ZGJiZjBhYTJiMjhmMTAyOTdjZmZiY2Y1ZTBhYWY2Y2IxMWU4ZjI2MTZlZCJ9fX0=" - 'Outposts:join': + 'outposts:join': <<: *fn-std-perm [ "Join Outpost Events" ] lore: | &7Permission to use diff --git a/core/src/main/resources/guis/structures/outpost/1.yml b/core/src/main/resources/guis/structures/outpost/1.yml index 23eb32ea9..4960a7c73 100644 --- a/core/src/main/resources/guis/structures/outpost/1.yml +++ b/core/src/main/resources/guis/structures/outpost/1.yml @@ -1,207 +1,110 @@ -title: "&eOutpost" -rows: 6 -sound: BLOCK_ANVIL_USE +(import): + outpost-page: + anchors: [ &base base ] options: - sell-coal: + stock-coal: posx: 2 posy: 2 material: COAL - name: '{$p}Coal {$s}%cost%' - cost: 3 - item: - material: COAL - sell-charcoal: + <<: *base + stock-charcoal: posx: 3 posy: 2 material: CHARCOAL - name: '{$p}Charcoal {$s}%cost%' - cost: 2 - item: - material: CHARCOAL - sell-flint: + <<: *base + stock-flint: posx: 4 posy: 2 material: FLINT - name: '{$p}Flint {$s}%cost%' - cost: 1 - item: - material: FLINT - sell-ender-eye: + <<: *base + stock-ender-eye: posx: 5 posy: 2 material: ENDER_EYE - name: '{$p}Ender Eye {$s}%cost%' - cost: 1000 - item: - material: ENDER_EYE - sell-ender-pearl: + <<: *base + stock-ender-pearl: posx: 6 posy: 2 material: ENDER_PEARL - name: '{$p}Ender Pearl {$s}%cost%' - cost: 100 - item: - material: ENDER_PEARL - sell-slime-ball: + <<: *base + stock-slime-ball: posx: 7 posy: 2 material: SLIME_BALL - name: '{$p}Slime Ball {$s}%cost%' - cost: 30 - item: - material: SLIME_BALL - sell-book: + <<: *base + stock-book: posx: 8 posy: 2 material: BOOK - name: '{$p}Book {$s}%cost%' - cost: 10 - item: - material: BOOK - sell-emerald: + <<: *base + stock-emerald: posx: 2 posy: 3 material: EMERALD - name: '{$p}Emerald {$s}%cost%' - cost: 70 - item: - material: EMERALD - sell-diamond: + <<: *base + stock-diamonds: posx: 3 posy: 3 material: DIAMOND - name: '{$p}Diamond {$s}%cost%' - cost: 60 - item: - material: DIAMOND - sell-gold-ingot: + <<: *base + stock-gold-ingot: posx: 4 posy: 3 material: GOLD_INGOT - name: '{$p}Gold Ingot {$s}%cost%' - cost: 50 - item: - material: GOLD_INGOT - sell-iron-ingot: + <<: *base + stock-iron-ingot: posx: 5 posy: 3 material: IRON_INGOT - name: '{$p}Iron Ingot {$s}%cost%' - cost: 40 - item: - material: IRON_INGOT - sell-nether-brick: + <<: *base + stock-nether-brick: posx: 6 posy: 3 material: NETHER_BRICK - name: '{$p}Nether Brick {$s}%cost%' - cost: 30 - item: - material: NETHER_BRICK - sell-brick: + <<: *base + stock-brick: posx: 7 posy: 3 material: BRICK - name: '{$p}Brick {$s}%cost%' - cost: 20 - item: - material: BRICK - sell-lapis-lazuli: + <<: *base + stock-lapis-lazuli: posx: 8 posy: 3 material: LAPIS_LAZULI - name: '{$p}Lapis Lazuli {$s}%cost%' - cost: 15 - item: - material: LAPIS_LAZULI - sell-experience-bottle: + <<: *base + stock-xp-bottle: posx: 2 posy: 4 material: EXPERIENCE_BOTTLE - name: '{$p}Experience Bottle {$s}%cost%' - cost: 20 - item: - material: EXPERIENCE_BOTTLE - sell-dragon-breath: + <<: *base + stock-dragon-breath: posx: 3 posy: 4 material: DRAGON_BREATH - name: '{$p}Dragon Breath {$s}%cost%' - cost: 3000 - item: - material: DRAGON_BREATH - sell-blaze-rod: + <<: *base + stock-blaze-rod: posx: 4 posy: 4 material: BLAZE_ROD - name: '{$p}Blaze Rod {$s}%cost%' - cost: 20 - item: - material: BLAZE_ROD - sell-prismarine-crystals: + <<: *base + stock-prismarine-crystals: posx: 5 posy: 4 material: PRISMARINE_CRYSTALS - name: '{$p}Prismarine Crystals {$s}%cost%' - cost: 20 - item: - material: PRISMARINE_CRYSTALS - sell-blaze-powder: + <<: *base + stock-blaze-powder: posx: 6 posy: 4 material: BLAZE_POWDER - name: '{$p}Blaze Powder {$s}%cost%' - cost: 30 - item: - material: BLAZE_POWDER - sell-golden-carrot: + <<: *base + stock-golden-carrot: posx: 7 posy: 4 material: GOLDEN_CARROT - name: '{$p}Golden Carrot {$s}%cost%' - cost: 50 - item: - material: GOLDEN_CARROT - sell-magma-cream: + <<: *base + stock-magma-cream: posx: 8 posy: 4 material: MAGMA_CREAM - name: '{$p}Magma Cream {$s}%cost%' - cost: 50 - item: - material: MAGMA_CREAM - break: - name: "&4Break" - material: REDSTONE_BLOCK - sound: ENTITY_ITEM_BREAK - lore: - - "&6Breaks the structure." - slot: 53 - nexus: - name: "&2Opens Nexus" - lore: - - "&6Resource Points&8: &2%kingdoms_resource_points%" - material: NETHER_STAR - posx: 5 - posy: 6 - close: - name: "&2Close" - material: BARRIER - lore: - - "&6Close the GUI." - slot: 45 - black-glass: - name: '' - material: BLACK_STAINED_GLASS_PANE - slots: [ 46, 47, 48, 50, 51, 52 ] - brown-stained-glass-pane: - name: '' - material: BROWN_STAINED_GLASS_PANE - slots: [ 0, 1, 2, 3, 4, 5, 6, 7, 9, 17, 18, 26, 27, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44 ] - next-page: - name: '&6Next Page &7- &e%page%&8/&e%pages%' - material: PLAYER_HEAD - skull: "eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvNjgyYWQxYjljYjRkZDIxMjU5YzBkNzVhYTMxNWZmMzg5YzNjZWY3NTJiZTM5NDkzMzgxNjRiYWM4NGE5NmUifX19" - sound: ITEM_BOOK_PAGE_TURN - slot: 8 \ No newline at end of file + <<: *base diff --git a/core/src/main/resources/guis/structures/outpost/2.yml b/core/src/main/resources/guis/structures/outpost/2.yml index fdac54d8c..8b0883ffa 100644 --- a/core/src/main/resources/guis/structures/outpost/2.yml +++ b/core/src/main/resources/guis/structures/outpost/2.yml @@ -1,44 +1,110 @@ -title: "&eOutpost" -rows: 6 -sound: BLOCK_ANVIL_USE +(import): + outpost-page: + anchors: [ &base base ] options: - construction: - material: STONE_PICKAXE - name: "&4Under construction..." + stock-cooked-rabbit: + posx: 2 + posy: 2 + material: COOKED_RABBIT + <<: *base + stock-cooked-beef: + posx: 3 + posy: 2 + material: COOKED_BEEF + <<: *base + stock-cooked-porkchop: + posx: 4 + posy: 2 + material: COOKED_PORKCHOP + <<: *base + stock-cooked-chicken: posx: 5 + posy: 2 + material: COOKED_CHICKEN + <<: *base + stock-cooked-fish: + posx: 6 + posy: 2 + material: COOKED_COD + <<: *base + stock-cooked-salmon: + posx: 7 + posy: 2 + material: COOKED_SALMON + <<: *base + stock-cooked-mutton: + posx: 8 + posy: 2 + material: COOKED_MUTTON + <<: *base + stock-wool: + posx: 2 posy: 3 - break: - name: "&4Break" - material: REDSTONE_BLOCK - sound: ENTITY_ITEM_BREAK - lore: - - "&6Breaks the structure." - slot: 53 - nexus: - name: "&2Opens Nexus" - lore: - - "&6Resource Points&8: &2%kingdoms_resource_points%" - material: NETHER_STAR + material: WHITE_WOOL + <<: *base + stock-string: + posx: 3 + posy: 3 + material: STRING + <<: *base + stock-bucket: + posx: 4 + posy: 3 + material: BUCKET + <<: *base + stock-bone-meal: + posx: 5 + posy: 3 + material: BONE_MEAL + <<: *base + stock-rail: + posx: 6 + posy: 3 + material: RAIL + <<: *base + stock-glass: + posx: 7 + posy: 3 + material: GLASS + <<: *base + stock-ghast-tear: + posx: 8 + posy: 3 + material: GHAST_TEAR + <<: *base + stock-redstone-torch: + posx: 2 + posy: 4 + material: REDSTONE_TORCH + <<: *base + stock-repeater: + posx: 3 + posy: 4 + material: REPEATER + <<: *base + stock-comparator: + posx: 4 + posy: 4 + material: COMPARATOR + <<: *base + stock-dispenser: posx: 5 - posy: 6 - close: - name: "&2Close" - material: BARRIER - lore: - - "&6Close the GUI." - slot: 45 - black-glass: - name: '' - material: BLACK_STAINED_GLASS_PANE - slots: [ 46, 47, 48, 50, 51, 52 ] - brown-stained-glass-pane: - name: '' - material: BROWN_STAINED_GLASS_PANE - slots: [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 17, 18, 26, 27, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44 ] - previous-page: - name: '&6Previous Page &7- &e%page%&8/&e%pages%' - material: PLAYER_HEAD - sound: ITEM_BOOK_PAGE_TURN - skull: "eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvMzdhZWU5YTc1YmYwZGY3ODk3MTgzMDE1Y2NhMGIyYTdkNzU1YzYzMzg4ZmYwMTc1MmQ1ZjQ0MTlmYzY0NSJ9fX0=" - slot: 0 \ No newline at end of file + posy: 4 + material: DISPENSER + <<: *base + stock-dropper: + posx: 6 + posy: 4 + material: DROPPER + <<: *base + stock-hopper: + posx: 7 + posy: 4 + material: HOPPER + <<: *base + stock-piston: + posx: 8 + posy: 4 + material: PISTON + <<: *base diff --git a/core/src/main/resources/guis/structures/outpost/amount-picker.yml b/core/src/main/resources/guis/structures/outpost/amount-picker.yml index b23a8309f..7a8b9de8a 100644 --- a/core/src/main/resources/guis/structures/outpost/amount-picker.yml +++ b/core/src/main/resources/guis/structures/outpost/amount-picker.yml @@ -8,12 +8,12 @@ options: material: BLACK_STAINED_GLASS_PANE name: '' cancel: - name: "{$e}Click to cancel" + name: "{$e}Cancel" posx: 1 posy: 1 material: BARRIER done: - name: "{$p}Click to buy" + name: "{$p}Buy" lore: "{$p}Price{$sep}: {$s}%fancy@cost%" material: GREEN_WOOL skull: @@ -29,8 +29,10 @@ options: {$e}Minimum value reached{$sep}: {$es}%min% else: lore: | - &7Hold shift while clicking to - decrease by {$p}10 + {$dot} {$p}Left-click{$colon} {$s}-1 + {$dot} {$p}Right-click{$colon} {$s}-10 + {$dot} {$p}Shift Right-click{$colon} {$s}-30 + {$dot} {$p}Shift Left-click{$colon} {$s}set to %min% posx: 2 posy: 2 material: PLAYER_HEAD @@ -48,9 +50,10 @@ options: {$e}Maximum value reached{$sep}: {$es}%max% else: lore: | - &7Hold shift while clicking to - increase by {$p}10 - {$e}Maximum amount{$sep}: {$es}%max% + {$dot} {$p}Left-click{$colon} {$s}+1 + {$dot} {$p}Right-click{$colon} {$s}+10 + {$dot} {$p}Shift Right-click{$colon} {$s}+30 + {$dot} {$p}Shift Left-click{$colon} {$s}set to %max% posx: 8 posy: 2 material: PLAYER_HEAD diff --git a/core/src/main/resources/guis/structures/outpost/selling.yml b/core/src/main/resources/guis/structures/outpost/selling.yml new file mode 100644 index 000000000..6b996a253 --- /dev/null +++ b/core/src/main/resources/guis/structures/outpost/selling.yml @@ -0,0 +1,32 @@ +title: "{$p}Selling {$s}%item_name%" +rows: 6 +sound: BLOCK_ANVIL_USE +interactable: empty +disallow-creative: true + +options: + apply: + name: '&aApply' + material: PLAYER_HEAD + skull: 'eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvNDMxMmNhNDYzMmRlZjVmZmFmMmViMGQ5ZDdjYzdiNTVhNTBjNGUzOTIwZDkwMzcyYWFiMTQwNzgxZjVkZmJjNCJ9fX0=' + posx: 9 + posy: 1 + lore: | + &7Drop items of the same type that + you want to sell. Then click this + option when you're done. + + If you want to cancel, just exit + the GUI. + + {$p}Total Amount{$colon} {$s}%fancy@total_amount% + {$p}Total Resource Points{$colon} {$s}%fancy@total_resource_points% + back: + name: "{$e}Cancel" + material: BARRIER + posx: 1 + posy: 1 + brown-stained-glass-pane: + name: '' + material: BROWN_STAINED_GLASS_PANE + slots: [ 1, 2, 3, 4, 5, 6, 7, 9, 17, 18, 26, 27, 35, 36, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53 ] diff --git a/core/src/main/resources/guis/structures/regulator/attributes.yml b/core/src/main/resources/guis/structures/regulator/attributes.yml index b844ff213..3cc3c5aeb 100644 --- a/core/src/main/resources/guis/structures/regulator/attributes.yml +++ b/core/src/main/resources/guis/structures/regulator/attributes.yml @@ -6,13 +6,27 @@ options: members: name: "&8[%kingdoms_rank_color%%kingdoms_rank_symbol%&8] &2%player%" material: PLAYER_HEAD - lore: - - "%online%" - - "&2Joined&8: &6%kingdoms_date_joined%" - - "&2Donations&8: &6%kingdoms_total_donations%" - - "&2Last Donation&8: &6%kingdoms_date_last_donation_time%" - - "&2Rank&8: &6%kingdoms_rank_name%" - slots: [ 10, 11, 12, 13, 14, 15, 16, 19, 20, 21, 22, 23, 24, 25, 28, 29, 30, 31, 32, 33, 34, 37, 38, 39, 40, 41, 42, 43 ] + is_same_kingdom: + condition: kingdoms_relation == 'SELF' + name: "&8[%kingdoms_rank_color%%kingdoms_rank_symbol%&8] &2%player%" + lore: + - "%online%" + - "&2Joined&8: &6%kingdoms_date_joined%" + - "&2Donations&8: &6%kingdoms_total_donations%" + - "&2Last Donation&8: &6%kingdoms_last_donation_time%" + - "&2Rank&8: &6%kingdoms_rank_name%" + is_other_kingdom: + condition: kingdoms_has_kingdom # or kingdoms_relation != 'SELF' + name: "&e%player%" + lore: + - "%online%" + - "&7This player is a member of another kingdom." + else: + name: "&b%player%" + lore: + - "%online%" + - "&7This player has no kingdom." + slots: [ 10, 11, 12, 13, 14, 15, 16, 19, 20, 21, 22, 23, 24, 25, 28, 29, 30, 31, 32, 33, 34, 37, 38, 39, 40, 41, 42, 43 ] red-glass: name: '' material: RED_STAINED_GLASS_PANE diff --git a/core/src/main/resources/guis/structures/regulator/interactions.yml b/core/src/main/resources/guis/structures/regulator/interactions.yml index 822f4ea4f..f48a4ed85 100644 --- a/core/src/main/resources/guis/structures/regulator/interactions.yml +++ b/core/src/main/resources/guis/structures/regulator/interactions.yml @@ -6,13 +6,27 @@ options: members: name: "&8[%kingdoms_rank_color%%kingdoms_rank_symbol%&8] &2%player%" material: PLAYER_HEAD - lore: - - "%online%" - - "&2Joined&8: &6%kingdoms_date_joined%" - - "&2Donations&8: &6%kingdoms_total_donations%" - - "&2Last Donation&8: &6%kingdoms_date_last_donation_time%" - - "&2Rank&8: &6%kingdoms_rank_name%" - slots: [ 10, 11, 12, 13, 14, 15, 16, 19, 20, 21, 22, 23, 24, 25, 28, 29, 30, 31, 32, 33, 34, 37, 38, 39, 40, 41, 42, 43 ] + is_same_kingdom: + condition: kingdoms_relation == 'SELF' + name: "&8[%kingdoms_rank_color%%kingdoms_rank_symbol%&8] &2%player%" + lore: + - "%online%" + - "&2Joined&8: &6%kingdoms_date_joined%" + - "&2Donations&8: &6%kingdoms_total_donations%" + - "&2Last Donation&8: &6%kingdoms_last_donation_time%" + - "&2Rank&8: &6%kingdoms_rank_name%" + is_other_kingdom: + condition: kingdoms_has_kingdom # or kingdoms_relation != 'SELF' + name: "&e%player%" + lore: + - "%online%" + - "&7This player is a member of another kingdom." + else: + name: "&b%player%" + lore: + - "%online%" + - "&7This player has no kingdom." + slots: [ 10, 11, 12, 13, 14, 15, 16, 19, 20, 21, 22, 23, 24, 25, 28, 29, 30, 31, 32, 33, 34, 37, 38, 39, 40, 41, 42, 43 ] red-glass: name: '' material: RED_STAINED_GLASS_PANE diff --git a/core/src/main/resources/guis/surrender.yml b/core/src/main/resources/guis/surrender.yml index 0599cf40e..6d9e934cb 100644 --- a/core/src/main/resources/guis/surrender.yml +++ b/core/src/main/resources/guis/surrender.yml @@ -39,8 +39,8 @@ options: {$p}Passed{$sep}: {$s}%passed% {$p}Total Lands{$sep}: {$s}%total_lands% # These are for plunders only. - # {$p}Attacker Score{$sep}: {$s}%attacker_score%{$sep}/{$s}%defender_death_limit% - # {$p}Defender Score{$sep}: {$s}%defender_score%{$sep}/{$s}%attacker_death_limit% + # {$p}Attacker Score{$sep}: {$s}%invasion_attacker_score%{$sep}/{$s}%invasion_defender_death_limit% + # {$p}Defender Score{$sep}: {$s}%invasion_defender_score%{$sep}/{$s}%invasion_attacker_death_limit% else: material: PURPLE_STAINED_GLASS_PANE attacking: diff --git a/core/src/main/resources/guis/templates/outpost-page.yml b/core/src/main/resources/guis/templates/outpost-page.yml new file mode 100644 index 000000000..191dbd3b2 --- /dev/null +++ b/core/src/main/resources/guis/templates/outpost-page.yml @@ -0,0 +1,67 @@ +### Shared outpost page settings ### +(module): + description: 'Base settings for outpost pages.' + parameters: { } + +(import): + pagination: { } + +title: "&eOutpost" +rows: 6 +sound: BLOCK_ANVIL_USE + +base: &base + name: "&5%item_name%" + lore: | + {$p}{? !buy_allowed ? "&m"}Buy{$colon} {$s}%fancy@buy% {$sep}(&9Left-click{$sep}){? buy_max_items != -1 ? " {$sep}(&9Max{$colon} &5%buy_max_items%{$sep})" } + {$p}Sell{$colon} {$s}%fancy@sell% {$sep}(&9Right-click{$sep}) + {? !buy_allowed ? " + {$e}You cannot buy this item + {$e}until {$es}%time@buy_cooldown%"} + +options: + info: + posx: 5 + posy: 1 + material: PLAYER_HEAD + skull: 'eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvZDAxYWZlOTczYzU0ODJmZGM3MWU2YWExMDY5ODgzM2M3OWM0MzdmMjEzMDhlYTlhMWEwOTU3NDZlYzI3NGEwZiJ9fX0=' + name: '{$e}Info' + lore: | + &eAll item prices are affected server-wide + based on supply-and-demand principles. + The more people buy the same item, + the prices increase and the more + they sell that item, their buy price + decreases. + + {$note} &7You can {$p}Shift left-click + &7after buying the item once, to + keep buying the same amount. + + break: + name: "&4Break" + material: REDSTONE_BLOCK + sound: ENTITY_ITEM_BREAK + lore: + - "&6Breaks the structure." + slot: 53 + nexus: + name: "&2Open Nexus" + lore: + - "&6Resource Points&8: &2%fancy@kingdoms_resource_points%" + material: NETHER_STAR + posx: 5 + posy: 6 + close: + name: "&2Close" + material: BARRIER + slot: 45 + + black-glass: + name: '' + material: BLACK_STAINED_GLASS_PANE + slots: [ 46, 47, 48, 50, 51, 52 ] + brown-stained-glass-pane: + name: '' + material: CYAN_STAINED_GLASS_PANE + slots: [ 1, 2, 3, 5, 6, 7, 9, 17, 18, 26, 27, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44 ] diff --git a/core/src/main/resources/invasions.yml b/core/src/main/resources/invasions.yml index 4dd0f7678..1f89f586a 100644 --- a/core/src/main/resources/invasions.yml +++ b/core/src/main/resources/invasions.yml @@ -215,7 +215,12 @@ bonus: # Options for when a nexus land is invaded. on-nexus-loss: + # Should the options below apply to the land that /k home is set in + # if no nexus is set? + alternate-to-home: true + disband: false + unclaim-all: false drop-nexus-chest-items: false # Bonuses given to the invader @@ -386,22 +391,31 @@ teleportation: # If the player should not take damage when teleporting to home. should-not-be-damaged: true +# Should the bossbars/scoreboards be displayed only for nearby players around the champion/invasion area +# or set to 0 for all the online members of the defender and attacker kingdoms. +visual-range: 50 + +# Scoreboard shown only when you're within the invasion area radius +scoreboard: + enabled: false + update-rate: 5sec + title: "{$sep}-=( &9Invasion {$sep})=-" + lines: + - "{$p}Champion Health{$colon}" + - "{$sep}[{$s}%invasion_champion_health%{$sep}/{$s}%invasion_champion_max_health%{$sep}]" + # The BossBar shown to nearby players for the chmapion. # This bossbar is also used for attackers during plunder invasions. # Multiple champions will show multiple bossbars of course. bossbar: enabled: true - # Should the bossbar be displayed only for nearby players around the champion/invasion area - # or set to 0 for all the online members of the defender and attacker kingdoms. - range: 50 - # For champion invasions if set to true, this will keep increasing the progress bar, and the champion is dead once the bar is full, # otherwise if set to false, the champion is dead once the progress bar is empty. # Same for plunder invasions, but for your opponent's lives. reverse-progress: true - title: "&2Champion Health &8[&6%health%&7/&6%max_health%&8]" + title: "&2Champion Health &8[&6%invasion_champion_health%&7/&6%invasion_champion_max_health%&8]" color: GREEN style: SEGMENTED_20 flags: [ PLAY_BOSS_MUSIC ] @@ -414,6 +428,8 @@ nations: # Force nation members (kingdoms) to use nation shields instead. # If this is enabled and a nation doesn't have a shield, but the kingdom does, # players will be still able to invade the kingdom. + # If this is false, kingdoms will use their own shield if they have one + # otherwise they will use the nations shield. use-shield: true # https://github.com/CryptoMorin/KingdomsX/wiki/Introduction#nation-zones @@ -599,7 +615,7 @@ time-limit: enabled: true # Special placeholders are the same as the previous bossbar. - title: "&5⌛ &8[&d%time%&8] &5⌛" + title: "&5⌛ &8[&d%invasion_time_left%&8] &5⌛" color: PURPLE style: SOLID @@ -701,6 +717,17 @@ plunder: increment: 0.1 # This happens every tick goal: 100 # after this, attackers win + # The defenders need to also participate in the capture progress. + # The attackers capture progress mechanics stay the same, however in this + # plunder, the capture progress has an initial progess (start-goal option) and this + # time, if the defenders outnumber the attackers, it doesn't just simply stop + # the attackers capture progress, but also decrements it backwards (defenders-decrement option). + # If the defenders manage to decrement the capture progress to zero, they win and keep their land. + tug-of-war: + enabled: false + initial-goal: 10 + defenders-decrement: 0.1 + # Should the capture progress ratio consider kingdom guards # and soldiers of the defender? consider-kingdom-mobs: true diff --git a/core/src/main/resources/misc-upgrades.yml b/core/src/main/resources/misc-upgrades.yml index 608799da3..db73d35b2 100644 --- a/core/src/main/resources/misc-upgrades.yml +++ b/core/src/main/resources/misc-upgrades.yml @@ -17,7 +17,7 @@ invasion-teleportation: enabled: true # This option can be used to make upgrade trees. - #condition: + #condition: # Makes "invasions" upgrade to be a requirement to be maxxed to upgrade this upgrade. # The "miscupgrades.required.invasions" refers to a custom language entry inside your language files. (e.g. en.yml) # %kingdoms_kingdom_upgrade% is a functional placeholder: https://github.com/CryptoMorin/KingdomsX/wiki/Placeholders#placeholder-functions diff --git a/core/src/main/resources/ranks.yml b/core/src/main/resources/ranks.yml index 38a0f76c6..87ccea6d8 100644 --- a/core/src/main/resources/ranks.yml +++ b/core/src/main/resources/ranks.yml @@ -74,7 +74,7 @@ new-rank: # The priority is based on the order of this list. # The first element has the highest priority. This should be always the king rank. # And the last element has the lowest priority. This is the default member rank when new players join. -ranks: +kingdom-ranks: king: color: "&c" symbol: "♚" @@ -108,4 +108,65 @@ ranks: symbol: "♟" material: IRON_BLOCK max-claims: 0 - permissions: [ NEXUS, BUILD, HOME, INVADE, INTERACT, USE, READ_MAILS ] \ No newline at end of file + permissions: [ NEXUS, BUILD, HOME, INVADE, INTERACT, USE, READ_MAILS ] + +national-ranks: + emperor: + color: "&c" + symbol: "♚" + name: "Emperor" + material: EMERALD_BLOCK + max-claims: -1 + permissions: [ ] + + duke: + color: "&6" + symbol: "❇" + name: "Duke" + material: DIAMOND_BLOCK + max-claims: -1 + permissions: [ NEXUS, BUILD, NEXUS_BUILD, HOME, INVADE, NEXUS_CHEST, UNCLAIM, CLAIM, TURRETS, INVITE, KICK, LORE, BROADCAST, + STRUCTURES, SET_HOME, ALLIANCE, TRUCE, ENEMY, MANAGE_RANKS, FLY, WITHDRAW, INTERACT, USE, OUTPOST, NATION, + UPGRADE, MANAGE_MAILS, READ_MAILS, INSTANT_TELEPORT, VIEW_LOGS ] + viscount: + name: "Viscount" + color: "&e" + symbol: "♜" + material: GOLD_BLOCK + max-claims: 10 + permissions: [ NEXUS, BUILD, NEXUS_BUILD, HOME, INVADE, NEXUS_CHEST, UNCLAIM_OWNED, CLAIM, TURRETS, INVITE, WITHDRAW, FLY, + INTERACT, USE, INSTANT_TELEPORT, READ_MAILS, INVSEE ] + member: + name: "Member" + color: "&2" + symbol: "♟" + material: IRON_BLOCK + max-claims: 0 + permissions: [ NEXUS, BUILD, HOME, INVADE, INTERACT, USE, READ_MAILS ] + +# How permissions should be categorized in the ranks GUI +# The names and how they're displayed is configured inside the GUI file, however +# the permissions will appear in the order they're specified here. +permission-categories: + building: + # The permissions that will be displayed in that category. + permissions: [ BUILD, NEXUS_BUILD, BUILD_OWNED ] + + # The name of the GUI file to be used inside 'guis/structures/nexus|national-nexus/ranks/permissions/...' + # This is used to make things easier when multiple categories can be handled by the same GUI layout. + gui: 'permissions' + economy: + permissions: [ WITHDRAW, OUTPOST, UPGRADE ] + gui: 'permissions' + diplomacy: + permissions: [ NATION, ALLIANCE, TRUCE, ENEMY, RELATION_ATTRIBUTES, READ_MAILS, BROADCAST, LORE, NEXUS, SETTINGS, VIEW_LOGS, EDIT_RANKS, MANAGE_RANKS ] + gui: 'permissions' + land: + permissions: [ CLAIM, UNCLAIM, UNCLAIM_OWNED, TURRETS, STRUCTURES, INTERACT, USE, INVADE ] + gui: 'permissions' + members: + permissions: [ INVSEE, INVITE, KICK, EXCLUDE_TAX ] + gui: 'permissions' + others: + permissions: [ FLY, HOME, SET_HOME, NEXUS_CHEST, OUTPOST, PROTECTION_SIGNS, INSTANT_TELEPORT ] + gui: 'permissions' diff --git a/core/src/main/resources/relations.yml b/core/src/main/resources/relations.yml index c398f580f..82ba92e2a 100644 --- a/core/src/main/resources/relations.yml +++ b/core/src/main/resources/relations.yml @@ -73,6 +73,8 @@ force-survival-mode: true # show-holograms: Should turret and structure holograms be shown to them? # pvp: Only works if the main PvP mode option is set to "relational", this accepts all the same options # as the main PvP mode. This also requires a restart. +# conditions: A list of conditions that supports placeholders with the primary context as the +# sender kingdom and the secondary context as the receiver kingdom relations: self: color: "&2" diff --git a/core/src/main/resources/schemas/invasions.yml b/core/src/main/resources/schemas/invasions.yml index 93bddc230..e977f776b 100644 --- a/core/src/main/resources/schemas/invasions.yml +++ b/core/src/main/resources/schemas/invasions.yml @@ -263,8 +263,12 @@ plunder: size: int capture-progress: - increment: decimal - goal: int + increment: Math + goal: Math + tug-of-war: + enabled: bool + initial-goal: Math + defenders-decrement: Math consider-kingdom-mobs: bool bossbar: diff --git a/core/src/main/resources/structures.yml b/core/src/main/resources/structures.yml index df5c49859..3033b1f0c 100644 --- a/core/src/main/resources/structures.yml +++ b/core/src/main/resources/structures.yml @@ -17,7 +17,11 @@ remove-unclaimed: false # Should structures go directly to the player's inventory who broke the structure. to-inventory-on-break: false -# The total amount of structures a single land can have. -# This is different than individual structure limits. -# Set to 0 to disable -total-limit-per-land: 10 \ No newline at end of file +# This is different than individual structure limits which can be configured +# in their own config files. +limit: + # Total amount of structures a kingdom can place. + total: 100 + + # Total amount of structures in a single chunk. + per-land: 10 diff --git a/core/src/main/resources/turrets.yml b/core/src/main/resources/turrets.yml index 2be408a03..410ec1cab 100644 --- a/core/src/main/resources/turrets.yml +++ b/core/src/main/resources/turrets.yml @@ -19,7 +19,8 @@ limit: 'misc_upgrades_max_turrets + 5' # Don't open the GUI when shift right-clicking (useful for placing blocks) disable-shift-click: true -# Remove turrets when lands are unclaimed automatically? This will simply break the turret naturally as if it was broken by a player. +# Remove turrets when lands are unclaimed automatically? This will simply break +# the turret naturally as if it was broken by a player. remove-unclaimed: false # Should turrets go directly to the player's inventory who broke the turret. @@ -33,6 +34,11 @@ teleportation-invulnerability: 5 # Requires a restart to change. allow-targetting-npcs: false +# Should turrets be in manual mode by default? +# For more information about automatic and manual modes, check the +# turret GUI description. +manual-by-default: false + # * These options require a restart to change. pacifists: # Should turrets target pacifist players? diff --git a/enginehub/build.gradle.kts b/enginehub/build.gradle.kts index 1c198286b..8784f3565 100644 --- a/enginehub/build.gradle.kts +++ b/enginehub/build.gradle.kts @@ -12,7 +12,7 @@ plugins { } group = "org.kingdoms.enginehub" -version = "1.0.0.2" +version = "1.0.0.3" description = "Adds support for EngineHub plugins (WorldEdit & WorldGuard) selections & schematic buildings." kingdomsAddon { diff --git a/enginehub/src/main/java/org/kingdoms/enginehub/commands/CommandAdminSchematicOthers.kt b/enginehub/src/main/java/org/kingdoms/enginehub/commands/CommandAdminSchematicOthers.kt index 72aa44c02..2440f8892 100644 --- a/enginehub/src/main/java/org/kingdoms/enginehub/commands/CommandAdminSchematicOthers.kt +++ b/enginehub/src/main/java/org/kingdoms/enginehub/commands/CommandAdminSchematicOthers.kt @@ -88,7 +88,7 @@ class CommandAdminSchematicOrigin(parent: KingdomsParentCommand) : KingdomsComma if (context.requireArgs(3)) return CommandResult.FAILED fun getCoord(index: Int): Int? { - return context.getNumber(index, true, false, null)?.toInt() + return context.getNumber(index, true, false, null)?.value?.toInt() } val x = getCoord(0) ?: return CommandResult.FAILED @@ -104,7 +104,7 @@ class CommandAdminSchematicOrigin(parent: KingdomsParentCommand) : KingdomsComma } override fun tabComplete(context: CommandTabContext): MutableList { - val str = when (context.argPosition) { + val str = when (context.parameterPosition) { 1 -> "" 2 -> "" 3 -> "" diff --git a/enginehub/src/main/java/org/kingdoms/enginehub/schematic/SchematicFolderRegistry.kt b/enginehub/src/main/java/org/kingdoms/enginehub/schematic/SchematicFolderRegistry.kt index 68e2c6ef3..e4e288acd 100644 --- a/enginehub/src/main/java/org/kingdoms/enginehub/schematic/SchematicFolderRegistry.kt +++ b/enginehub/src/main/java/org/kingdoms/enginehub/schematic/SchematicFolderRegistry.kt @@ -4,7 +4,7 @@ import org.kingdoms.data.Pair import org.kingdoms.main.KLogger import org.kingdoms.main.Kingdoms import org.kingdoms.utils.fs.FolderRegistry -import org.kingdoms.utils.internal.StackTraces +import org.kingdoms.utils.internal.stacktrace.StackTraces import java.net.URI import java.net.URISyntaxException import java.nio.file.Path diff --git a/java-higher-api/java10/src/main/java/org/kingdoms/utils/internal/jdk/Java10.java b/java-higher-api/java10/src/main/java/org/kingdoms/utils/internal/jdk/Java10.java index 70a3682dd..b065109ad 100644 --- a/java-higher-api/java10/src/main/java/org/kingdoms/utils/internal/jdk/Java10.java +++ b/java-higher-api/java10/src/main/java/org/kingdoms/utils/internal/jdk/Java10.java @@ -3,7 +3,11 @@ import java.lang.reflect.Method; public final class Java10 { - public static Method getMethodOfCaller(int depth) { + /** + * Stack walker was introduced in Java 9, but certain methods that are + * used here were added later in Java 10. + */ + public static Method getCallerMethod(int depth) { StackWalker.StackFrame caller = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE) .walk(x -> x.skip(depth).findFirst().orElseThrow( () -> new IllegalArgumentException("Method caller depth too deep: " + depth))); diff --git a/java-higher-api/java9/src/main/java/org/kingdoms/utils/internal/jdk/Java9.java b/java-higher-api/java9/src/main/java/org/kingdoms/utils/internal/jdk/Java9.java index 4956eff75..c5ff79964 100644 --- a/java-higher-api/java9/src/main/java/org/kingdoms/utils/internal/jdk/Java9.java +++ b/java-higher-api/java9/src/main/java/org/kingdoms/utils/internal/jdk/Java9.java @@ -1,4 +1,9 @@ package org.kingdoms.utils.internal.jdk; public final class Java9 { + public static Class getCallerClass() { + return StackWalker + .getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE) + .getCallerClass(); + } } diff --git a/outposts/build.gradle.kts b/outposts/build.gradle.kts index d40f2717b..86ee0d83a 100644 --- a/outposts/build.gradle.kts +++ b/outposts/build.gradle.kts @@ -8,7 +8,7 @@ plugins { } group = "org.kingdoms" -version = "3.0.1.6.5" +version = "3.0.1.6.6" description = "An event similar to KoTH" kingdomsAddon { diff --git a/outposts/src/main/java/org/kingdoms/outposts/OutpostEvent.java b/outposts/src/main/java/org/kingdoms/outposts/OutpostEvent.java index de00fb13c..951e13e75 100644 --- a/outposts/src/main/java/org/kingdoms/outposts/OutpostEvent.java +++ b/outposts/src/main/java/org/kingdoms/outposts/OutpostEvent.java @@ -5,7 +5,6 @@ import org.bukkit.ChatColor; import org.bukkit.FireworkEffect; import org.bukkit.Location; -import org.bukkit.entity.EntityType; import org.bukkit.entity.Firework; import org.bukkit.entity.Player; import org.bukkit.inventory.ItemStack; @@ -18,16 +17,16 @@ import org.checkerframework.checker.nullness.qual.Nullable; import org.kingdoms.config.KingdomsConfig; import org.kingdoms.constants.group.Kingdom; +import org.kingdoms.constants.namespace.Namespace; import org.kingdoms.constants.player.KingdomPlayer; import org.kingdoms.data.Pair; import org.kingdoms.enginehub.EngineHubAddon; +import org.kingdoms.locale.ContextualMessenger; import org.kingdoms.locale.Language; -import org.kingdoms.locale.SupportedLanguage; import org.kingdoms.locale.messenger.StaticMessenger; import org.kingdoms.locale.placeholders.context.MessagePlaceholderProvider; import org.kingdoms.main.Kingdoms; -import org.kingdoms.services.managers.ServiceHandler; -import org.kingdoms.utils.XScoreboard; +import org.kingdoms.utils.display.scoreboard.XScoreboard; import org.kingdoms.utils.bossbars.BossBarSession; import org.kingdoms.utils.time.TimeFormatter; @@ -35,6 +34,7 @@ import java.util.concurrent.ThreadLocalRandom; public class OutpostEvent { + private static final Namespace SCOREBOARD_ID = new Namespace("Outposts", "EVENT"); protected static final Map EVENTS = new HashMap<>(); protected static final Map KINGDOMS_IN_EVENTS = new HashMap<>(); @@ -43,10 +43,12 @@ public class OutpostEvent { private final long time; private final @NonNull Map participants = new HashMap<>(); private final @Nullable BossBarSession bossBar; - private final @NonNull XScoreboard scoreboard; private long started; private @Nullable BukkitTask task; + private final @NonNull XScoreboard scoreboard; + private final Map participantScores = new HashMap<>(); + private OutpostEvent(Outpost outpost, long time) { this.outpost = Objects.requireNonNull(outpost, "Cannot create outpust event from null outpost"); this.time = time; @@ -56,10 +58,9 @@ private OutpostEvent(Outpost outpost, long time) { bossBar.setVisible(false); } else bossBar = null; - scoreboard = new XScoreboard("main", - new StaticMessenger(KingdomsConfig.OUTPOST_EVENTS_SCOREBOARD_TITLE.getManager().getString()) - .getProvider(Language.getDefault()).getMessage(), - new MessagePlaceholderProvider()); + scoreboard = new XScoreboard(SCOREBOARD_ID, + new StaticMessenger(KingdomsConfig.OUTPOST_EVENTS_SCOREBOARD_TITLE.getManager().getString()), + new MessagePlaceholderProvider()).useLineNumberAsScore(false); } public static Map getKingdomsInEvents() { @@ -119,8 +120,6 @@ public void start(long startTime) { started = System.currentTimeMillis(); task = new BukkitRunnable() { - final Objective objective = scoreboard.getScoreboard().getObjective("main"); - @Override public void run() { long passed = System.currentTimeMillis() - started; @@ -151,12 +150,9 @@ public void run() { if (EngineHubAddon.INSTANCE.getWorldGuard().isLocationInRegion(member.getLocation(), outpost.getRegion())) scored++; } } - kingdoms.getValue().setScore(scored); - if (objective != null) { - Score score = objective.getScore(ChatColor.GREEN + kingdom.getName()); - score.setScore(scored); - } + kingdoms.getValue().setScore(scored); + setScore(kingdom, scored); } } }.runTaskTimerAsynchronously(Kingdoms.get(), 0, 1); @@ -164,6 +160,19 @@ public void run() { }, startTime); } + private void setScore(Kingdom kingdom, int score) { + XScoreboard.Line kingdomScore = participantScores.get(kingdom.getId()); + if (kingdomScore != null) { + kingdomScore.setScore(score); + } else { + XScoreboard.Line line = scoreboard.addLine( + new StaticMessenger("&a%kingdoms_kingdom_name%"), + new MessagePlaceholderProvider().withContext(kingdom) + ); + participantScores.put(kingdom.getId(), line); + } + } + public boolean isFull() { int max = outpost.getMaxParticipants(); return max > 0 && participants.size() >= max; @@ -277,16 +286,13 @@ public OutpostParticipant participate(Player requester, Kingdom kingdom) { OutpostParticipant participant = new OutpostParticipant(requester.getUniqueId()); participants.put(kingdom.getId(), participant); KINGDOMS_IN_EVENTS.put(kingdom.getId(), this); - - Score score = scoreboard.getScoreboard().getObjective("main").getScore(ChatColor.GREEN + kingdom.getName()); - score.setScore(0); - + setScore(kingdom, 0); return participant; } public void display(Player player) { bossBar.addPlayer(player); - scoreboard.setForPlayer(player); + scoreboard.addPlayer(player); } public long getTime() { diff --git a/peace-treaties/build.gradle.kts b/peace-treaties/build.gradle.kts index b737caa5f..6e14b0f77 100644 --- a/peace-treaties/build.gradle.kts +++ b/peace-treaties/build.gradle.kts @@ -9,7 +9,7 @@ plugins { } group = "org.kingdoms.peacetreaties" -version = "1.2.6.0.5" +version = "1.2.6.0.6" description = "A contract management for neutral relationships." kingdomsAddon { diff --git a/peace-treaties/src/main/java/org/kingdoms/peacetreaties/PeaceTreatiesAddon.java b/peace-treaties/src/main/java/org/kingdoms/peacetreaties/PeaceTreatiesAddon.java index 9dff82c86..a33e1e5ae 100644 --- a/peace-treaties/src/main/java/org/kingdoms/peacetreaties/PeaceTreatiesAddon.java +++ b/peace-treaties/src/main/java/org/kingdoms/peacetreaties/PeaceTreatiesAddon.java @@ -71,6 +71,7 @@ public void onLoad() { LimitTurretsTerm.PROVIDER, LimitStructuresTerm.PROVIDER, LimitClaimsTerm.PROVIDER, KingChangeTerm.PROVIDER) .forEach(termRegistry::register); + PeaceTreatiesPlaceholder.init(); LanguageManager.registerMessenger(PeaceTreatyLang.class); CustomConfigValidators.init(); ConfigManager.registerAsMainConfig(PeaceTreatyConfig.PEACE_TREATIES); @@ -91,8 +92,6 @@ public void onEnable() { new CommandPeaceTreaty(); - PeaceTreatiesPlaceholder.init(); - // peace-treaties.yml ConfigWatcher.register(PeaceTreatyConfig.PEACE_TREATIES.getFile().toPath().getParent(), ConfigWatcher::handleNormalConfigs); ConfigManager.registerNormalWatcher("peace-treaties.yml", (event) -> { @@ -101,7 +100,7 @@ public void onEnable() { TermRegistry.loadTermGroupings(); }); - GUIConfig.loadGUIs(this); + GUIConfig.loadInternalGUIs(this); registerAddon(); HealthCheckupHandler.addCheckupHandler(new PeaceTreatyFSCK()); diff --git a/peace-treaties/src/main/java/org/kingdoms/peacetreaties/commands/CommandPeaceTreatyMiscUpgrades.kt b/peace-treaties/src/main/java/org/kingdoms/peacetreaties/commands/CommandPeaceTreatyMiscUpgrades.kt index 84482ac02..59932f87c 100644 --- a/peace-treaties/src/main/java/org/kingdoms/peacetreaties/commands/CommandPeaceTreatyMiscUpgrades.kt +++ b/peace-treaties/src/main/java/org/kingdoms/peacetreaties/commands/CommandPeaceTreatyMiscUpgrades.kt @@ -1,7 +1,7 @@ package org.kingdoms.peacetreaties.commands import org.kingdoms.commands.* -import org.kingdoms.managers.structures.NexusManager +import org.kingdoms.managers.buildings.structures.NexusManager import org.kingdoms.peacetreaties.config.PeaceTreatyLang import org.kingdoms.peacetreaties.data.PeaceTreaties.Companion.getReceivedPeaceTreaties import org.kingdoms.peacetreaties.terms.types.MiscUpgradesTerm diff --git a/peace-treaties/src/main/java/org/kingdoms/peacetreaties/data/PeaceTreaty.java b/peace-treaties/src/main/java/org/kingdoms/peacetreaties/data/PeaceTreaty.java index 80df4d9c6..6f9b39671 100644 --- a/peace-treaties/src/main/java/org/kingdoms/peacetreaties/data/PeaceTreaty.java +++ b/peace-treaties/src/main/java/org/kingdoms/peacetreaties/data/PeaceTreaty.java @@ -20,7 +20,7 @@ import org.kingdoms.utils.MathUtils; import org.kingdoms.utils.conditions.ConditionProcessor; import org.kingdoms.utils.config.NodeInterpreter; -import org.kingdoms.utils.internal.Fn; +import org.kingdoms.utils.internal.functional.Fn; import java.time.Duration; import java.util.*; diff --git a/peace-treaties/src/main/java/org/kingdoms/peacetreaties/terms/types/KeepLandsTerm.java b/peace-treaties/src/main/java/org/kingdoms/peacetreaties/terms/types/KeepLandsTerm.java index 8a69d9d18..d04aa9b52 100644 --- a/peace-treaties/src/main/java/org/kingdoms/peacetreaties/terms/types/KeepLandsTerm.java +++ b/peace-treaties/src/main/java/org/kingdoms/peacetreaties/terms/types/KeepLandsTerm.java @@ -1,6 +1,8 @@ package org.kingdoms.peacetreaties.terms.types; +import org.bukkit.Location; import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; import org.kingdoms.constants.group.Kingdom; import org.kingdoms.constants.group.model.logs.lands.LogKingdomInvader; import org.kingdoms.constants.land.abstraction.data.DeserializationContext; @@ -26,6 +28,7 @@ import org.kingdoms.peacetreaties.terms.Term; import org.kingdoms.peacetreaties.terms.TermGroupingOptions; import org.kingdoms.peacetreaties.terms.TermProvider; +import org.kingdoms.platform.bukkit.adapters.BukkitAdapter; import org.kingdoms.utils.LocationUtils; import java.util.*; @@ -86,6 +89,11 @@ private void prompt(StandardPeaceTreatyEditor editor, Kingdom kingdom = editor.getPeaceTreaty().getProposerKingdom(); + Location coreLocation; + if (kingdom.getNexus() != null) coreLocation = kingdom.getNexus().toBukkitLocation(); + else if (kingdom.getHome() != null) coreLocation = BukkitAdapter.adapt(kingdom.getHome()); + else coreLocation = null; + if (displayModeGrouped) { List invadedLands = getInvadedLandsSimple(kingdom, editor.getPeaceTreaty().getVictimKingdomId()); @@ -100,6 +108,9 @@ private void prompt(StandardPeaceTreatyEditor editor, prompt(editor, completableFuture, added, addedLogs, displayModeGrouped, page); }); log.addEdits(option.getMessageContext()); + option.getMessageContext().raw("distance_from_core", + coreLocation == null ? "???" : log.getStartLocation().distance(coreLocation) + ); option.done(); } } else { @@ -113,6 +124,7 @@ private void prompt(StandardPeaceTreatyEditor editor, (newPage) -> prompt(editor, completableFuture, added, addedLogs, displayModeGrouped, newPage)); ReusableOptionHandler option = pagination.getOption(); + for (Pair pair : pagination.getPaginatedElements()) { SimpleChunkLocation invadedLand = pair.getKey(); option.setEdits("added", added.contains(invadedLand)).onNormalClicks(() -> { @@ -121,6 +133,10 @@ private void prompt(StandardPeaceTreatyEditor editor, }); pair.getValue().addEdits(option.getMessageContext()); LocationUtils.getChunkEdits(option.getMessageContext(), invadedLand, ""); + + option.getMessageContext().raw("distance_from_core", + coreLocation == null ? "???" : pair.getValue().getStartLocation().distance(coreLocation) + ); option.done(); } } diff --git a/peace-treaties/src/main/resources/guis/peace-treaties/keep-lands.yml b/peace-treaties/src/main/resources/guis/peace-treaties/keep-lands.yml index ef8c1e895..516dec906 100644 --- a/peace-treaties/src/main/resources/guis/peace-treaties/keep-lands.yml +++ b/peace-treaties/src/main/resources/guis/peace-treaties/keep-lands.yml @@ -61,6 +61,9 @@ options: {$p}At{$sep}: {$s}%time% {$p}Affected Lands{$sep}: %affected-lands% + + &9This invasion took place {$p}%distance_from_core% &9blocks away + from the kingdom's core (nexus or home) else: added: condition: added @@ -74,4 +77,7 @@ options: {$p}Ransack Mode{$sep}: %ransack-mode% {$p}Result{$sep}: {$s}%result% {$p}At{$sep}: {$s}%time% + + &9This invasion took place {$p}%distance_from_core% &9blocks away + from the kingdom's core (nexus or home) slots: [ 10, 11, 12, 13, 14, 15, 16, 19, 20, 21, 22, 23, 24, 25, 28, 29, 30, 31, 32, 33, 34, 37, 38, 39, 40, 41, 42, 43 ] \ No newline at end of file diff --git a/platform/bukkit/src/main/java/org/kingdoms/platform/bukkit/adapters/BukkitAdapter.kt b/platform/bukkit/src/main/java/org/kingdoms/platform/bukkit/adapters/BukkitAdapter.kt index eb2dbd5b2..8d97e69cc 100644 --- a/platform/bukkit/src/main/java/org/kingdoms/platform/bukkit/adapters/BukkitAdapter.kt +++ b/platform/bukkit/src/main/java/org/kingdoms/platform/bukkit/adapters/BukkitAdapter.kt @@ -13,8 +13,7 @@ import org.kingdoms.server.location.BlockLocation3 import org.kingdoms.server.location.BlockVector3 import org.kingdoms.server.location.Direction import org.kingdoms.server.location.Vector3 -import org.kingdoms.utils.internal.Fn -import kotlin.contracts.contract +import org.kingdoms.utils.internal.functional.Fn object BukkitAdapter { @JvmStatic fun adapt(direction: Direction): BlockFace = BlockFace.valueOf(direction.name) diff --git a/platform/bukkit/src/main/java/org/kingdoms/platform/bukkit/adapters/BukkitNBTAdapter.java b/platform/bukkit/src/main/java/org/kingdoms/platform/bukkit/adapters/BukkitNBTAdapter.java index c8a49254c..7d4a02161 100644 --- a/platform/bukkit/src/main/java/org/kingdoms/platform/bukkit/adapters/BukkitNBTAdapter.java +++ b/platform/bukkit/src/main/java/org/kingdoms/platform/bukkit/adapters/BukkitNBTAdapter.java @@ -12,7 +12,7 @@ import org.kingdoms.nbt.tag.NBTTag; import org.kingdoms.nbt.tag.NBTTagCompound; import org.kingdoms.nbt.tag.NBTTagType; -import org.kingdoms.utils.internal.Fn; +import org.kingdoms.utils.internal.functional.Fn; import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; diff --git a/platform/bukkit/src/main/java/org/kingdoms/platform/bukkit/core/BukkitServer.java b/platform/bukkit/src/main/java/org/kingdoms/platform/bukkit/core/BukkitServer.java index bfccf28d2..696446473 100644 --- a/platform/bukkit/src/main/java/org/kingdoms/platform/bukkit/core/BukkitServer.java +++ b/platform/bukkit/src/main/java/org/kingdoms/platform/bukkit/core/BukkitServer.java @@ -1,5 +1,6 @@ package org.kingdoms.platform.bukkit.core; +import org.bukkit.Bukkit; import org.bukkit.plugin.java.JavaPlugin; import org.kingdoms.platform.bukkit.adapters.BukkitNBTAdapter; import org.kingdoms.platform.bukkit.events.BukkitEventHandler; @@ -46,6 +47,11 @@ public int getTicks() { return tickTracker.getTicks(); } + @Override + public boolean isMainThread() { + return Bukkit.isPrimaryThread(); + } + @Override public WorldRegistry getWorldRegistry() { return worldRegistry; diff --git a/platform/paper/src/main/java/org/kingdoms/utils/paper/PaperLib.java b/platform/paper/src/main/java/org/kingdoms/utils/paper/PaperLib.java index fb4d8074a..a4f1483f6 100644 --- a/platform/paper/src/main/java/org/kingdoms/utils/paper/PaperLib.java +++ b/platform/paper/src/main/java/org/kingdoms/utils/paper/PaperLib.java @@ -1,8 +1,11 @@ package org.kingdoms.utils.paper; +import org.bukkit.inventory.ItemStack; +import org.jetbrains.annotations.TestOnly; import org.kingdoms.utils.paper.asyncchunks.AsyncChunks; import java.util.function.Function; +import java.util.function.UnaryOperator; public final class PaperLib { private static AsyncChunks asyncChunks; @@ -14,4 +17,19 @@ public static void init(Function versionChecker) { public static AsyncChunks getAsyncChunks() { return asyncChunks; } + + @TestOnly + public static String getItemHoverEvent(ItemStack item) { + // showItem -> https://github.com/PaperMC/Paper/blob/e08e6679fcaf5ce8b91db628309ed530e58a4133/patches/server/0010-Adventure.patch#L4832-L4847 + // unwrap (basically a asNMSCopy()) -> https://github.com/PaperMC/Paper/blob/e08e6679fcaf5ce8b91db628309ed530e58a4133/patches/server/0009-MC-Utils.patch#L5707-L5713 + // asAdventure -> https://github.com/PaperMC/Paper/blob/e08e6679fcaf5ce8b91db628309ed530e58a4133/patches/server/1020-Registry-Modification-API.patch#L423-L425 + // Bukkit.getServer().getItemFactory().asHoverEvent(this, op); + // net.kyori.adventure.text.event.HoverEvent.showItem(UnaryOperator.identity().apply( + // net.kyori.adventure.text.event.HoverEvent.ShowItem.showItem( + // item.getType().getKey(), + // item.getAmount(), + // io.papermc.paper.adventure.PaperAdventure.asAdventure(CraftItemStack.unwrap(item).getComponentsPatch())) // unwrap is fine here because the components patch will be safely copied + // )); + return item.asHoverEvent().toString(); + } } diff --git a/resources/languages/hu/guis/surrender.yml b/resources/languages/hu/guis/surrender.yml index b8c804c52..7654cff97 100644 --- a/resources/languages/hu/guis/surrender.yml +++ b/resources/languages/hu/guis/surrender.yml @@ -39,8 +39,8 @@ options: {$p}Átadott{$sep}: {$s}%passed% {$p}Teljes terület{$sep}: {$s}%total_lands% # These are for plunders only. - # {$p}Attacker Score{$sep}: {$s}%attacker_score%{$sep}/{$s}%defender_death_limit% - # {$p}Defender Score{$sep}: {$s}%defender_score%{$sep}/{$s}%attacker_death_limit% + # {$p}Attacker Score{$sep}: {$s}%invasion_attacker_score%{$sep}/{$s}%invasion_defender_death_limit% + # {$p}Defender Score{$sep}: {$s}%invasion_defender_score%{$sep}/{$s}%invasion_attacker_death_limit% else: material: PURPLE_STAINED_GLASS_PANE attacking: diff --git a/resources/languages/pt/guis/surrender.yml b/resources/languages/pt/guis/surrender.yml index fdfc7f64e..36ab3fd66 100644 --- a/resources/languages/pt/guis/surrender.yml +++ b/resources/languages/pt/guis/surrender.yml @@ -39,8 +39,8 @@ options: {$p}Passado{$sep}: {$s}%passed% {$p}Total de Terras{$sep}: {$s}%total_lands% # These are for plunders only. - # {$p}Attacker Score{$sep}: {$s}%attacker_score%{$sep}/{$s}%defender_death_limit% - # {$p}Defender Score{$sep}: {$s}%defender_score%{$sep}/{$s}%attacker_death_limit% + # {$p}Attacker Score{$sep}: {$s}%invasion_attacker_score%{$sep}/{$s}%invasion_defender_death_limit% + # {$p}Defender Score{$sep}: {$s}%invasion_defender_score%{$sep}/{$s}%invasion_attacker_death_limit% else: material: PURPLE_STAINED_GLASS_PANE attacking: diff --git a/resources/languages/zh/guis/surrender.yml b/resources/languages/zh/guis/surrender.yml index 5fe797de8..b33aa7400 100644 --- a/resources/languages/zh/guis/surrender.yml +++ b/resources/languages/zh/guis/surrender.yml @@ -39,14 +39,14 @@ options: {$p}入侵最大时间{$sep}: {$s}%duration% {$p}入侵已过时间{$sep}: {$s}%passed% {$p}总地块数{$sep}: {$s}%total_lands% - {$p}入侵方分数{$sep}: {$s}%attacker_score%{$sep}/{$s}%defender_death_limit% - {$p}防守方分数{$sep}: {$s}%defender_score%{$sep}/{$s}%attacker_death_limit% + {$p}入侵方分数{$sep}: {$s}%invasion_attacker_score%{$sep}/{$s}%invasion_defender_death_limit% + {$p}防守方分数{$sep}: {$s}%invasion_defender_score%{$sep}/{$s}%invasion_attacker_death_limit% # These are for plunders only. - # {$p}Attacker Score{$sep}: {$s}%attacker_score%{$sep}/{$s}%defender_death_limit% - # {$p}Defender Score{$sep}: {$s}%defender_score%{$sep}/{$s}%attacker_death_limit% + # {$p}Attacker Score{$sep}: {$s}%invasion_attacker_score%{$sep}/{$s}%invasion_defender_death_limit% + # {$p}Defender Score{$sep}: {$s}%invasion_defender_score%{$sep}/{$s}%invasion_attacker_death_limit% # These are for plunders only. - # {$p}Attacker Score{$sep}: {$s}%attacker_score%{$sep}/{$s}%defender_death_limit% - # {$p}Defender Score{$sep}: {$s}%defender_score%{$sep}/{$s}%attacker_death_limit% + # {$p}Attacker Score{$sep}: {$s}%invasion_attacker_score%{$sep}/{$s}%invasion_defender_death_limit% + # {$p}Defender Score{$sep}: {$s}%invasion_defender_score%{$sep}/{$s}%invasion_attacker_death_limit% else: material: PURPLE_STAINED_GLASS_PANE attacking: diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts index 819339526..57a772c9f 100644 --- a/shared/build.gradle.kts +++ b/shared/build.gradle.kts @@ -17,6 +17,7 @@ dependencies { // antlr("org.antlr:antlr4:4.13.1") // compileOnly("it.unimi.dsi:fastutil-core:8.5.13") + compileOnly("com.github.ben-manes.caffeine:caffeine:2.9.2") compileOnly("org.ow2.asm:asm:9.4") { isTransitive = false } compileOnly("org.ow2.asm:asm-commons:9.4") { isTransitive = false } } \ No newline at end of file diff --git a/shared/src/main/java/org/kingdoms/constants/economy/Balance.kt b/shared/src/main/java/org/kingdoms/constants/economy/Balance.kt index d2fed7884..00f3a38f7 100644 --- a/shared/src/main/java/org/kingdoms/constants/economy/Balance.kt +++ b/shared/src/main/java/org/kingdoms/constants/economy/Balance.kt @@ -5,6 +5,7 @@ import org.kingdoms.constants.DataStringRepresentation import org.kingdoms.constants.namespace.Namespace import org.kingdoms.constants.namespace.Namespaced import org.kingdoms.constants.namespace.NamespacedRegistry +import org.kingdoms.utils.internal.numbers.AnyNumber object EconomyRegistry : NamespacedRegistry() diff --git a/shared/src/main/java/org/kingdoms/constants/namespace/NamespacedMetadataContainer.java b/shared/src/main/java/org/kingdoms/constants/namespace/NamespacedMetadataContainer.java index a551d45f3..f53ed5486 100644 --- a/shared/src/main/java/org/kingdoms/constants/namespace/NamespacedMetadataContainer.java +++ b/shared/src/main/java/org/kingdoms/constants/namespace/NamespacedMetadataContainer.java @@ -3,5 +3,5 @@ import java.util.Map; public interface NamespacedMetadataContainer { - Map getMetadata(); + NamespacedMap getMetadata(); } diff --git a/shared/src/main/java/org/kingdoms/server/core/Server.java b/shared/src/main/java/org/kingdoms/server/core/Server.java index c9648a738..50d80bf9e 100644 --- a/shared/src/main/java/org/kingdoms/server/core/Server.java +++ b/shared/src/main/java/org/kingdoms/server/core/Server.java @@ -18,6 +18,8 @@ static Server get() { int getTicks(); + boolean isMainThread(); + WorldRegistry getWorldRegistry(); default void onStartup() {} diff --git a/shared/src/main/java/org/kingdoms/server/location/Location.kt b/shared/src/main/java/org/kingdoms/server/location/Location.kt index 1f5669a1b..096977756 100644 --- a/shared/src/main/java/org/kingdoms/server/location/Location.kt +++ b/shared/src/main/java/org/kingdoms/server/location/Location.kt @@ -30,6 +30,7 @@ class Location( fun subtract(x: Number, y: Number, z: Number) = simpleAdd(-x.toDouble(), -y.toDouble(), -z.toDouble()) fun toBlockVector() = BlockVector3.of(x.toInt(), y.toInt(), z.toInt()) + fun toVectorLocation() = Vector3Location.of(world, x, y, z) fun toVector() = Vector3.of(x, y, z) private fun simpleAdd(x: Number, y: Number, z: Number) = @@ -100,6 +101,7 @@ class Vector3Location( fun subtract(other: BlockPoint3D) = subtract(other.x, other.y, other.z) fun subtract(x: Number, y: Number, z: Number) = simpleAdd(-x.toDouble(), -y.toDouble(), -z.toDouble()) + fun toBlockLocation() = BlockLocation3.of(world, x.toInt(), y.toInt(), z.toInt()) fun toBlockVector() = BlockVector3.of(x.toInt(), y.toInt(), z.toInt()) fun toVector() = Vector3.of(x, y, z) diff --git a/shared/src/main/java/org/kingdoms/server/location/LocationPurifier.kt b/shared/src/main/java/org/kingdoms/server/location/LocationPurifier.kt deleted file mode 100644 index 3a097c043..000000000 --- a/shared/src/main/java/org/kingdoms/server/location/LocationPurifier.kt +++ /dev/null @@ -1,15 +0,0 @@ -package org.kingdoms.server.location - -import org.kingdoms.utils.internal.Purifier - -// Note: Don't do instance check for abstract classes -// Other classes can still inherit from those abstract classes and -// the purifier will think these are pure classes while they're not. - -object PurifierImmutableBlockVector3 : Purifier { - override fun purify(original: BlockVector3): BlockVector3 = BlockVector3.of(original) -} - -object PurifierImmutableBlockVector2 : Purifier { - override fun purify(original: BlockVector2): BlockVector2 = BlockVector2.of(original) -} \ No newline at end of file diff --git a/shared/src/main/java/org/kingdoms/utils/Validate.java b/shared/src/main/java/org/kingdoms/utils/Validate.java new file mode 100644 index 000000000..93c7e7c37 --- /dev/null +++ b/shared/src/main/java/org/kingdoms/utils/Validate.java @@ -0,0 +1,499 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.kingdoms.utils; + +import java.util.Collection; +import java.util.Iterator; +import java.util.Map; + +/** + *

This class assists in validating arguments.

+ * + *

The class is based along the lines of JUnit. If an argument value is + * deemed invalid, an IllegalArgumentException is thrown. For example:

+ * + *
+ * Validate.isTrue( i > 0, "The value must be greater than zero: ", i);
+ * Validate.notNull( surname, "The surname must not be null");
+ * 
+ * + * @author Apache Software Foundation + * @author Ola Berg + * @author Gary Gregory + * @author Norm Deane + * @version $Id: Validate.java 1057051 2011-01-09 23:15:51Z sebb $ + * @since 2.0 + */ +public final class Validate { + private Validate() {} + + // isTrue + //--------------------------------------------------------------------------------- + + /** + *

Validate that the argument condition is true; otherwise + * throwing an exception with the specified message. This method is useful when + * validating according to an arbitrary boolean expression, such as validating an + * object or using your own custom validation expression.

+ * + *
Validate.isTrue( myObject.isOk(), "The object is not OK: ", myObject);
+ * + *

For performance reasons, the object value is passed as a separate parameter and + * appended to the exception message only in the case of an error.

+ * + * @param expression the boolean expression to check + * @param message the exception message if invalid + * @param value the value to append to the message when invalid + * @throws IllegalArgumentException if expression is false + */ + public static void isTrue(boolean expression, String message, Object value) { + if (!expression) { + throw new IllegalArgumentException(message + value); + } + } + + /** + *

Validate that the argument condition is true; otherwise + * throwing an exception with the specified message. This method is useful when + * validating according to an arbitrary boolean expression, such as validating a + * primitive number or using your own custom validation expression.

+ * + *
Validate.isTrue(i > 0.0, "The value must be greater than zero: ", i);
+ * + *

For performance reasons, the long value is passed as a separate parameter and + * appended to the exception message only in the case of an error.

+ * + * @param expression the boolean expression to check + * @param message the exception message if invalid + * @param value the value to append to the message when invalid + * @throws IllegalArgumentException if expression is false + */ + public static void isTrue(boolean expression, String message, long value) { + if (!expression) { + throw new IllegalArgumentException(message + value); + } + } + + /** + *

Validate that the argument condition is true; otherwise + * throwing an exception with the specified message. This method is useful when + * validating according to an arbitrary boolean expression, such as validating a + * primitive number or using your own custom validation expression.

+ * + *
Validate.isTrue(d > 0.0, "The value must be greater than zero: ", d);
+ * + *

For performance reasons, the double value is passed as a separate parameter and + * appended to the exception message only in the case of an error.

+ * + * @param expression the boolean expression to check + * @param message the exception message if invalid + * @param value the value to append to the message when invalid + * @throws IllegalArgumentException if expression is false + */ + public static void isTrue(boolean expression, String message, double value) { + if (!expression) { + throw new IllegalArgumentException(message + value); + } + } + + /** + *

Validate that the argument condition is true; otherwise + * throwing an exception with the specified message. This method is useful when + * validating according to an arbitrary boolean expression, such as validating a + * primitive number or using your own custom validation expression.

+ * + *
+     * Validate.isTrue( (i > 0), "The value must be greater than zero");
+     * Validate.isTrue( myObject.isOk(), "The object is not OK");
+     * 
+ * + * @param expression the boolean expression to check + * @param message the exception message if invalid + * @throws IllegalArgumentException if expression is false + */ + public static void isTrue(boolean expression, String message) { + if (!expression) { + throw new IllegalArgumentException(message); + } + } + + /** + *

Validate that the argument condition is true; otherwise + * throwing an exception. This method is useful when validating according + * to an arbitrary boolean expression, such as validating a + * primitive number or using your own custom validation expression.

+ * + *
+     * Validate.isTrue(i > 0);
+     * Validate.isTrue(myObject.isOk());
+ * + *

The message of the exception is "The validated expression is + * false".

+ * + * @param expression the boolean expression to check + * @throws IllegalArgumentException if expression is false + */ + public static void isTrue(boolean expression) { + if (!expression) { + throw new IllegalArgumentException("The validated expression is false"); + } + } + + // notNull + //--------------------------------------------------------------------------------- + + /** + *

Validate that the specified argument is not null; + * otherwise throwing an exception. + * + *

Validate.notNull(myObject);
+ * + *

The message of the exception is "The validated object is + * null".

+ * + * @param object the object to check + * @throws IllegalArgumentException if the object is null + */ + public static void notNull(Object object) { + notNull(object, "The validated object is null"); + } + + /** + *

Validate that the specified argument is not null; + * otherwise throwing an exception with the specified message. + * + *

Validate.notNull(myObject, "The object must not be null");
+ * + * @param object the object to check + * @param message the exception message if invalid + */ + public static void notNull(Object object, String message) { + if (object == null) { + throw new IllegalArgumentException(message); + } + } + + // notEmpty array + //--------------------------------------------------------------------------------- + + /** + *

Validate that the specified argument array is neither null + * nor a length of zero (no elements); otherwise throwing an exception + * with the specified message. + * + *

Validate.notEmpty(myArray, "The array must not be empty");
+ * + * @param array the array to check + * @param message the exception message if invalid + * @throws IllegalArgumentException if the array is empty + */ + public static void notEmpty(Object[] array, String message) { + if (array == null || array.length == 0) { + throw new IllegalArgumentException(message); + } + } + + /** + *

Validate that the specified argument array is neither null + * nor a length of zero (no elements); otherwise throwing an exception. + * + *

Validate.notEmpty(myArray);
+ * + *

The message in the exception is "The validated array is + * empty". + * + * @param array the array to check + * @throws IllegalArgumentException if the array is empty + */ + public static void notEmpty(Object[] array) { + notEmpty(array, "The validated array is empty"); + } + + // notEmpty collection + //--------------------------------------------------------------------------------- + + /** + *

Validate that the specified argument collection is neither null + * nor a size of zero (no elements); otherwise throwing an exception + * with the specified message. + * + *

Validate.notEmpty(myCollection, "The collection must not be empty");
+ * + * @param collection the collection to check + * @param message the exception message if invalid + * @throws IllegalArgumentException if the collection is empty + */ + public static void notEmpty(Collection collection, String message) { + if (collection == null || collection.isEmpty()) { + throw new IllegalArgumentException(message); + } + } + + /** + *

Validate that the specified argument collection is neither null + * nor a size of zero (no elements); otherwise throwing an exception. + * + *

Validate.notEmpty(myCollection);
+ * + *

The message in the exception is "The validated collection is + * empty".

+ * + * @param collection the collection to check + * @throws IllegalArgumentException if the collection is empty + */ + public static void notEmpty(Collection collection) { + notEmpty(collection, "The validated collection is empty"); + } + + // notEmpty map + //--------------------------------------------------------------------------------- + + /** + *

Validate that the specified argument map is neither null + * nor a size of zero (no elements); otherwise throwing an exception + * with the specified message. + * + *

Validate.notEmpty(myMap, "The map must not be empty");
+ * + * @param map the map to check + * @param message the exception message if invalid + * @throws IllegalArgumentException if the map is empty + */ + public static void notEmpty(Map map, String message) { + if (map == null || map.isEmpty()) { + throw new IllegalArgumentException(message); + } + } + + /** + *

Validate that the specified argument map is neither null + * nor a size of zero (no elements); otherwise throwing an exception. + * + *

Validate.notEmpty(myMap);
+ * + *

The message in the exception is "The validated map is + * empty".

+ * + * @param map the map to check + * @throws IllegalArgumentException if the map is empty + * @see #notEmpty(Map, String) + */ + public static void notEmpty(Map map) { + notEmpty(map, "The validated map is empty"); + } + + // notEmpty string + //--------------------------------------------------------------------------------- + + /** + *

Validate that the specified argument string is + * neither null nor a length of zero (no characters); + * otherwise throwing an exception with the specified message. + * + *

Validate.notEmpty(myString, "The string must not be empty");
+ * + * @param string the string to check + * @param message the exception message if invalid + * @throws IllegalArgumentException if the string is empty + */ + public static String notEmpty(String string, String message) { + if (string == null || string.isEmpty()) { + throw new IllegalArgumentException(message); + } + return string; + } + + /** + *

Validate that the specified argument string is + * neither null nor a length of zero (no characters); + * otherwise throwing an exception with the specified message. + * + *

Validate.notEmpty(myString);
+ * + *

The message in the exception is "The validated + * string is empty".

+ * + * @param string the string to check + * @throws IllegalArgumentException if the string is empty + */ + public static String notEmpty(String string) { + return notEmpty(string, "The validated string is empty"); + } + + // notNullElements array + //--------------------------------------------------------------------------------- + + /** + *

Validate that the specified argument array is neither + * null nor contains any elements that are null; + * otherwise throwing an exception with the specified message. + * + *

Validate.noNullElements(myArray, "The array contain null at position %d");
+ * + *

If the array is null, then the message in the exception + * is "The validated object is null".

+ * + * @param array the array to check + * @param message the exception message if the collection has null elements + * @throws IllegalArgumentException if the array is null or + * an element in the array is null + */ + public static void noNullElements(Object[] array, String message) { + Validate.notNull(array); + for (int i = 0; i < array.length; i++) { + if (array[i] == null) { + throw new IllegalArgumentException(message); + } + } + } + + /** + *

Validate that the specified argument array is neither + * null nor contains any elements that are null; + * otherwise throwing an exception. + * + *

Validate.noNullElements(myArray);
+ * + *

If the array is null, then the message in the exception + * is "The validated object is null".

+ * + *

If the array has a null element, then the message in the + * exception is "The validated array contains null element at index: + * " followed by the index.

+ * + * @param array the array to check + * @throws IllegalArgumentException if the array is null or + * an element in the array is null + */ + public static void noNullElements(Object[] array) { + Validate.notNull(array); + for (int i = 0; i < array.length; i++) { + if (array[i] == null) { + throw new IllegalArgumentException("The validated array contains null element at index: " + i); + } + } + } + + // notNullElements collection + //--------------------------------------------------------------------------------- + + /** + *

Validate that the specified argument collection is neither + * null nor contains any elements that are null; + * otherwise throwing an exception with the specified message. + * + *

Validate.noNullElements(myCollection, "The collection contains null elements");
+ * + *

If the collection is null, then the message in the exception + * is "The validated object is null".

+ * + * @param collection the collection to check + * @param message the exception message if the collection has + * @throws IllegalArgumentException if the collection is null or + * an element in the collection is null + */ + public static void noNullElements(Collection collection, String message) { + Validate.notNull(collection); + for (T t : collection) { + if (t == null) { + throw new IllegalArgumentException(message); + } + } + } + + /** + *

Validate that the specified argument collection is neither + * null nor contains any elements that are null; + * otherwise throwing an exception. + * + *

Validate.noNullElements(myCollection);
+ * + *

If the collection is null, then the message in the exception + * is "The validated object is null".

+ * + *

If the collection has a null element, then the message in the + * exception is "The validated collection contains null element at index: + * " followed by the index.

+ * + * @param collection the collection to check + * @throws IllegalArgumentException if the collection is null or + * an element in the collection is null + */ + public static void noNullElements(Collection collection) { + Validate.notNull(collection); + int i = 0; + for (Iterator it = collection.iterator(); it.hasNext(); i++) { + if (it.next() == null) { + throw new IllegalArgumentException("The validated collection contains null element at index: " + i); + } + } + } + + /** + *

Validate an argument, throwing IllegalArgumentException + * if the argument collection is null or has elements that + * are not of type clazz or a subclass.

+ * + *
+     * Validate.allElementsOfType(collection, String.class, "Collection has invalid elements");
+     * 
+ * + * @param collection the collection to check, not null + * @param clazz the Class which the collection's elements are expected to be, not null + * @param message the exception message if the Collection has elements not of type clazz + * @since 2.1 + */ + public static void allElementsOfType(Collection collection, Class clazz, String message) { + Validate.notNull(collection); + Validate.notNull(clazz); + for (Object o : collection) { + if (!clazz.isInstance(o)) { + throw new IllegalArgumentException(message); + } + } + } + + /** + *

+ * Validate an argument, throwing IllegalArgumentException if the argument collection is + * null or has elements that are not of type clazz or a subclass. + *

+ * + *
+     * Validate.allElementsOfType(collection, String.class);
+     * 
+ * + *

+ * The message in the exception is 'The validated collection contains an element not of type clazz at index: '. + *

+ * + * @param collection the collection to check, not null + * @param clazz the Class which the collection's elements are expected to be, not null + * @since 2.1 + */ + public static void allElementsOfType(Collection collection, Class clazz) { + Validate.notNull(collection); + Validate.notNull(clazz); + int i = 0; + for (Iterator it = collection.iterator(); it.hasNext(); i++) { + if (!clazz.isInstance(it.next())) { + throw new IllegalArgumentException("The validated collection contains an element not of type " + + clazz.getName() + " at index: " + i); + } + } + } +} diff --git a/shared/src/main/java/org/kingdoms/utils/cache/JavaMapWrapper.java b/shared/src/main/java/org/kingdoms/utils/cache/JavaMapWrapper.java new file mode 100644 index 000000000..638062faf --- /dev/null +++ b/shared/src/main/java/org/kingdoms/utils/cache/JavaMapWrapper.java @@ -0,0 +1,106 @@ +package org.kingdoms.utils.cache; + +import com.github.benmanes.caffeine.cache.CacheLoader; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Collection; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +public final class JavaMapWrapper implements PeekableMap { + private final ConcurrentHashMap cache; + private final CacheLoader loader; + + public JavaMapWrapper(ConcurrentHashMap cache, CacheLoader loader) { + this.cache = cache; + this.loader = loader; + } + + @Override + public int size() { + return cache.size(); + } + + @Override + public boolean isEmpty() { + return cache.isEmpty(); + } + + @Override + public boolean containsKey(Object key) { + return cache.containsKey(key); + } + + @Override + public boolean containsValue(Object value) { + throw new UnsupportedOperationException(); + } + + @SuppressWarnings("unchecked") + @Override + public V get(Object key) { + V data = cache.get(key); + if (data != null) return data; + + try { + data = loader.load((K) key); + if (data != null) put((K) key, data); + return data; + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Nullable + @Override + public V put(K key, V value) { + cache.put(key, value); + return null; + } + + @Override + public V remove(Object key) { + cache.remove(key); + return null; + } + + @Override + public void putAll(@NotNull Map m) { + cache.putAll(m); + } + + @Override + public void clear() { + cache.clear(); + } + + @NotNull + @Override + public Set keySet() { + return cache.keySet(); + } + + @NotNull + @Override + public Collection values() { + return cache.values(); + } + + @NotNull + @Override + public Set> entrySet() { + return cache.entrySet(); + } + + @Override + public V peek(K key) { + return cache.get(key); + } + + @Override + public V getIfPresent(K key) { + return cache.get(key); + } +} diff --git a/shared/src/main/java/org/kingdoms/utils/cache/PeekableMap.java b/shared/src/main/java/org/kingdoms/utils/cache/PeekableMap.java new file mode 100644 index 000000000..66a94cb22 --- /dev/null +++ b/shared/src/main/java/org/kingdoms/utils/cache/PeekableMap.java @@ -0,0 +1,9 @@ +package org.kingdoms.utils.cache; + +import java.util.Map; + +public interface PeekableMap extends Map { + V peek(K key); + + V getIfPresent(K key); +} diff --git a/shared/src/main/java/org/kingdoms/utils/cache/RunnableCountDownLatch.kt b/shared/src/main/java/org/kingdoms/utils/cache/RunnableCountDownLatch.kt new file mode 100644 index 000000000..47b1953fd --- /dev/null +++ b/shared/src/main/java/org/kingdoms/utils/cache/RunnableCountDownLatch.kt @@ -0,0 +1,25 @@ +package org.kingdoms.utils.cache + +import java.util.concurrent.atomic.AtomicInteger +import java.util.function.Consumer + +class RunnableCountDownLatch(countdown: Int, private val runnable: Consumer) { + private val countdown: AtomicInteger + val total: AtomicInteger + + init { + require(countdown > 0) { "Countdown number must be greater than zero" } + this.countdown = AtomicInteger(countdown) + this.total = AtomicInteger(countdown) + } + + fun increase() { + countdown.incrementAndGet() + total.incrementAndGet() + } + + fun countDown() { + if (countdown.get() <= 0) throw IllegalStateException("Already down to zero: " + countdown.get()) + if (countdown.decrementAndGet() == 0) runnable.accept(this) + } +} \ No newline at end of file diff --git a/shared/src/main/java/org/kingdoms/utils/cache/caffeine/CacheHandler.java b/shared/src/main/java/org/kingdoms/utils/cache/caffeine/CacheHandler.java new file mode 100644 index 000000000..4de3c777a --- /dev/null +++ b/shared/src/main/java/org/kingdoms/utils/cache/caffeine/CacheHandler.java @@ -0,0 +1,29 @@ +package org.kingdoms.utils.cache.caffeine; + +import com.github.benmanes.caffeine.cache.Caffeine; +import com.github.benmanes.caffeine.cache.Scheduler; + +import java.util.concurrent.Executors; +import java.util.concurrent.ForkJoinPool; +import java.util.concurrent.ScheduledExecutorService; + +public final class CacheHandler { + private static final ForkJoinPool POOL = new ForkJoinPool(); + private static final Scheduler CACHE_SCHEDULER = Scheduler.forScheduledExecutorService(newScheduler()); + + public static Caffeine newBuilder() { + return Caffeine.newBuilder().executor(POOL); + } + + public static ForkJoinPool getPool() { + return POOL; + } + + public static Scheduler getCacheScheduler() { + return CACHE_SCHEDULER; + } + + public static ScheduledExecutorService newScheduler() { + return Executors.newSingleThreadScheduledExecutor(Executors.defaultThreadFactory()); + } +} diff --git a/shared/src/main/java/org/kingdoms/utils/cache/caffeine/CaffeineWrapper.java b/shared/src/main/java/org/kingdoms/utils/cache/caffeine/CaffeineWrapper.java new file mode 100644 index 000000000..040662405 --- /dev/null +++ b/shared/src/main/java/org/kingdoms/utils/cache/caffeine/CaffeineWrapper.java @@ -0,0 +1,98 @@ +package org.kingdoms.utils.cache.caffeine; + +import com.github.benmanes.caffeine.cache.LoadingCache; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.kingdoms.utils.cache.PeekableMap; + +import java.util.Collection; +import java.util.Map; +import java.util.Set; + +public final class CaffeineWrapper implements PeekableMap { + private final LoadingCache cache; + + public CaffeineWrapper(LoadingCache cache) { + this.cache = cache; + } + + @Override + public int size() { + cache.cleanUp(); + return (int) cache.estimatedSize(); + } + + @Override + public boolean isEmpty() { + return size() != 0; + } + + @SuppressWarnings("unchecked") + @Override + public boolean containsKey(Object key) { + return cache.getIfPresent((K) key) != null; + } + + @Override + public boolean containsValue(Object value) { + throw new UnsupportedOperationException(); + } + + @SuppressWarnings("unchecked") + @Override + public V get(Object key) { + return cache.get((K) key); + } + + @Nullable + @Override + public V put(K key, V value) { + cache.put(key, value); + return null; + } + + @SuppressWarnings("unchecked") + @Override + public V remove(Object key) { + cache.invalidate((K) key); + return null; + } + + @Override + public void putAll(@NotNull Map m) { + cache.putAll(m); + } + + @Override + public void clear() { + cache.invalidateAll(); + } + + @NotNull + @Override + public Set keySet() { + return cache.asMap().keySet(); + } + + @NotNull + @Override + public Collection values() { + return cache.asMap().values(); + } + + @NotNull + @Override + public Set> entrySet() { + return cache.asMap().entrySet(); + } + + @Override + public V peek(K key) { + return cache.getIfPresent(key); + } + + @Override + public V getIfPresent(K key) { + return cache.getIfPresent(key); + } +} diff --git a/shared/src/main/java/org/kingdoms/utils/cache/caffeine/ExpirableMap.kt b/shared/src/main/java/org/kingdoms/utils/cache/caffeine/ExpirableMap.kt new file mode 100644 index 000000000..23e22d312 --- /dev/null +++ b/shared/src/main/java/org/kingdoms/utils/cache/caffeine/ExpirableMap.kt @@ -0,0 +1,90 @@ +package org.kingdoms.utils.cache.caffeine + +import com.github.benmanes.caffeine.cache.Cache +import com.github.benmanes.caffeine.cache.Caffeine +import com.github.benmanes.caffeine.cache.Policy +import com.github.benmanes.caffeine.cache.stats.CacheStats +import java.util.concurrent.ConcurrentMap +import java.util.function.Function + +class ExpirableMap : Cache { + val defaultExpirationStrategy: ExpirationStrategy? + private val cache: Cache> + + @JvmOverloads + constructor(defaultExpirationStrategy: ExpirationStrategy? = null) { + this.defaultExpirationStrategy = defaultExpirationStrategy + this.cache = CacheHandler.newBuilder() + .expireAfter(ExpirableObjectExpiry>()) + .build() + } + + @JvmOverloads + constructor( + builder: Caffeine>, + defaultExpirationStrategy: ExpirationStrategy? = null + ) { + this.defaultExpirationStrategy = defaultExpirationStrategy + this.cache = builder.build() + + if (defaultExpirationStrategy != null) { + builder.expireAfter(ExpirableObjectExpiry>()) + } + } + + override fun estimatedSize(): Long = cache.estimatedSize() + + override fun stats(): CacheStats = cache.stats() + + override fun asMap(): ConcurrentMap = throw UnsupportedOperationException() + + override fun cleanUp() { + cache.cleanUp() + } + + override fun policy(): Policy = throw UnsupportedOperationException() + + override fun invalidate(key: K) { + cache.invalidate(key) + } + + override fun invalidateAll(keys: MutableIterable?) { + cache.invalidateAll(keys) + } + + fun put(key: K, value: V, expiry: ExpirationStrategy) { + cache.put(key, ReferencedExpirableObject(value, expiry)) + } + + private val assertDefaultStrategy + get() = defaultExpirationStrategy + ?: throw UnsupportedOperationException("No default expiration strategy was set") + + override fun put(key: K, value: V) { + cache.put(key, ReferencedExpirableObject(value, assertDefaultStrategy)) + } + + override fun getIfPresent(key: K): V? = cache.getIfPresent(key)?.reference + + fun contains(key: K) = cache.getIfPresent(key) != null + + override fun get(key: K, mappingFunction: Function): V { + return cache.get(key) { ReferencedExpirableObject(mappingFunction.apply(key), assertDefaultStrategy) } + .reference + } + + override fun getAllPresent(keys: MutableIterable?): MutableMap { + return cache.getAllPresent(keys)?.asSequence()!!.associateTo(hashMapOf()) { it.key to it.value.reference } + } + + override fun getAll( + keys: MutableIterable?, + mappingFunction: Function, out MutableMap>? + ): MutableMap = throw UnsupportedOperationException() + + override fun putAll(map: MutableMap?) = throw UnsupportedOperationException() + + override fun invalidateAll() { + cache.invalidateAll() + } +} \ No newline at end of file diff --git a/shared/src/main/java/org/kingdoms/utils/cache/caffeine/ExpirableObject.kt b/shared/src/main/java/org/kingdoms/utils/cache/caffeine/ExpirableObject.kt new file mode 100644 index 000000000..364b67ef4 --- /dev/null +++ b/shared/src/main/java/org/kingdoms/utils/cache/caffeine/ExpirableObject.kt @@ -0,0 +1,47 @@ +package org.kingdoms.utils.cache.caffeine + +import com.github.benmanes.caffeine.cache.Expiry +import java.time.Duration + +interface ExpirableObject { + val expirationStrategy: ExpirationStrategy +} + +class ExpirationStrategy( + val expiryAfterCreate: Duration, + val expiryAfterUpdate: Duration? = null, + val expiryAfterRead: Duration? = null +) { + companion object { + @JvmStatic fun all(duration: Duration) = ExpirationStrategy(duration, duration, duration) + @JvmStatic fun expireAfterRead(duration: Duration) = ExpirationStrategy(duration, duration, duration) + @JvmStatic fun expireAfterCreate(duration: Duration) = ExpirationStrategy(duration, null, null) + } +} + +class ReferencedExpirableObject( + val reference: T, + override val expirationStrategy: ExpirationStrategy +) : ExpirableObject + +class ExpirableObjectExpiry : Expiry { + override fun expireAfterCreate( + key: K, + value: V, + currentTime: Long + ): Long = value.expirationStrategy.expiryAfterCreate.toNanos() + + override fun expireAfterUpdate( + key: K, + value: V, + currentTime: Long, + currentDuration: Long, + ): Long = value.expirationStrategy.expiryAfterUpdate?.toNanos() ?: currentDuration + + override fun expireAfterRead( + key: K, + value: V, + currentTime: Long, + currentDuration: Long, + ): Long = value.expirationStrategy.expiryAfterRead?.toNanos() ?: currentDuration +} \ No newline at end of file diff --git a/shared/src/main/java/org/kingdoms/utils/cache/caffeine/ExpirableSet.java b/shared/src/main/java/org/kingdoms/utils/cache/caffeine/ExpirableSet.java new file mode 100644 index 000000000..e037fcabe --- /dev/null +++ b/shared/src/main/java/org/kingdoms/utils/cache/caffeine/ExpirableSet.java @@ -0,0 +1,47 @@ +package org.kingdoms.utils.cache.caffeine; + +import java.time.Duration; +import java.util.Objects; + +public class ExpirableSet { + private final ExpirableMap map; + private final long duration; + + public ExpirableSet(ExpirationStrategy expirationStrategy) { + Objects.requireNonNull(expirationStrategy, "Expiration etrategies cannot be null"); + this.map = new ExpirableMap<>(expirationStrategy); + this.duration = map.getDefaultExpirationStrategy().getExpiryAfterCreate().toMillis(); + } + + public boolean add(K key) { + boolean contained = contains(key); + this.map.put(key, System.currentTimeMillis()); + return !contained; + } + + public Duration getTimeLeft(K key) { + Long added = this.map.getIfPresent(key); + if (added == null) return Duration.ZERO; + + long passed = System.currentTimeMillis() - added; + long left = duration - passed; + + return left <= 0 ? Duration.ZERO : Duration.ofMillis(left); + } + + public void clear() { + map.invalidateAll(); + } + + public boolean contains(K key) { + return this.map.getIfPresent(key) != null; + } + + public void remove(K key) { + this.map.invalidate(key); + } + + public void cleanUp() { + map.cleanUp(); + } +} diff --git a/shared/src/main/java/org/kingdoms/utils/cache/single/CacheableObject.java b/shared/src/main/java/org/kingdoms/utils/cache/single/CacheableObject.java new file mode 100644 index 000000000..f92bfbeb3 --- /dev/null +++ b/shared/src/main/java/org/kingdoms/utils/cache/single/CacheableObject.java @@ -0,0 +1,65 @@ +package org.kingdoms.utils.cache.single; + +import java.util.Objects; +import java.util.function.Supplier; + +/** + * Any operation that can be cached. It may expire multiple times. + */ +public interface CacheableObject extends Supplier { + /** + * Invalidate the cache, but don't re-evaluate. Only re-evaluates if {@link #get()} is called again. + */ + void invalidate(); + + /** + * Whether this operation is cached. + */ + boolean isCached(); + + /** + * If the cached object equals the given object. + * If the object is not cached, it will be. + *

+ * Equivalent to: + *

+     *     Objects.equals(get(), other);
+     * 
+ */ + default boolean contains(T other) { + return Objects.equals(get(), other); + } + + /** + * Equivalent to: + *

+     *     get() == null
+     * 
+ * @see #isPresent() + */ + default boolean isNull() { + return get() == null; + } + + /** + * Equivalent to: + *

+     *     get() != null
+     * 
+ * @see #isNull() + */ + default boolean isPresent() { + return get() != null; + } + + /** + * Gets the cached object or caches it and returns it. + */ + @Override + T get(); + + /** + * Sets the cached value. + */ + void set(T cache); +} diff --git a/shared/src/main/java/org/kingdoms/utils/cache/single/CachedSupplier.java b/shared/src/main/java/org/kingdoms/utils/cache/single/CachedSupplier.java new file mode 100644 index 000000000..22991326c --- /dev/null +++ b/shared/src/main/java/org/kingdoms/utils/cache/single/CachedSupplier.java @@ -0,0 +1,44 @@ +package org.kingdoms.utils.cache.single; + +import org.checkerframework.checker.nullness.qual.Nullable; + +import java.util.Objects; +import java.util.function.Supplier; + +public class CachedSupplier implements CacheableObject { + protected final Supplier supplier; + protected T cached; + protected Boolean present; + + public CachedSupplier(Supplier supplier) { + this.supplier = Objects.requireNonNull(supplier); + } + + public static CachedSupplier of(Supplier supplier) { + if (supplier instanceof CachedSupplier) return (CachedSupplier) supplier; + return new CachedSupplier<>(supplier); + } + + @Override + public void invalidate() { + this.cached = null; + } + + @Override + public boolean isCached() { + return present; + } + + public T get() { + if (cached == null) { + cached = supplier.get(); + present = cached != null; + } + return cached; + } + + @Override + public void set(@Nullable T cache) { + this.cached = cache; + } +} diff --git a/shared/src/main/java/org/kingdoms/utils/cache/single/ExpirableCachedSupplier.kt b/shared/src/main/java/org/kingdoms/utils/cache/single/ExpirableCachedSupplier.kt new file mode 100644 index 000000000..c0faf737d --- /dev/null +++ b/shared/src/main/java/org/kingdoms/utils/cache/single/ExpirableCachedSupplier.kt @@ -0,0 +1,27 @@ +package org.kingdoms.utils.cache.single + +import java.time.Duration +import java.util.function.Supplier + +open class ExpirableCachedSupplier( + getter: Supplier, + private val cacheTime: Duration, +) : CachedSupplier(getter) { + private var lastChecked: Long = System.currentTimeMillis() + + init { + if (cacheTime.seconds <= 5) throw IllegalArgumentException("Any cache time under 5 seconds is not likely to help with performance: ${cacheTime.toMillis()}ms") + } + + override fun get(): T { + val currentTime = System.currentTimeMillis() + val diff = currentTime - lastChecked + + if (cached == null || cacheTime.minusMillis(diff).isNegative) { + cached = supplier.get() + lastChecked = currentTime + } + + return cached!! + } +} \ No newline at end of file diff --git a/shared/src/main/java/org/kingdoms/utils/cache/single/TickedCache.java b/shared/src/main/java/org/kingdoms/utils/cache/single/TickedCache.java new file mode 100644 index 000000000..0fefae4a6 --- /dev/null +++ b/shared/src/main/java/org/kingdoms/utils/cache/single/TickedCache.java @@ -0,0 +1,47 @@ +package org.kingdoms.utils.cache.single; + +import org.kingdoms.server.core.Server; + +/** + * A very simple wrapper class that contains a cached object that is cached based on the + * server ticks. Similar to Caffeine's {@link com.github.benmanes.caffeine.cache.Cache}. + * + * @param the type of the cached object. + */ +public class TickedCache implements CacheableObject { + private T value; + private final int expirationTicks; + private int lastUpdateTicks; + + public TickedCache(int expirationTicks) { + if (expirationTicks <= 0) + throw new IllegalArgumentException("Expiration ticks cannot be less than 1: " + expirationTicks); + this.expirationTicks = expirationTicks; + } + + public boolean hasExpired() { + return this.value == null || ((Server.get().getTicks() - lastUpdateTicks) >= expirationTicks); + } + + @Override + public boolean isCached() { + return !hasExpired(); + } + + @Override + public void invalidate() { + set(null); + } + + @Override + public T get() { + if (hasExpired()) throw new IllegalStateException("Cannot access expired value: " + value); + return value; + } + + @Override + public void set(T value) { + this.value = value; + this.lastUpdateTicks = Server.get().getTicks(); + } +} diff --git a/shared/src/main/java/org/kingdoms/utils/fs/FSUtil.java b/shared/src/main/java/org/kingdoms/utils/fs/FSUtil.java new file mode 100644 index 000000000..cc5558375 --- /dev/null +++ b/shared/src/main/java/org/kingdoms/utils/fs/FSUtil.java @@ -0,0 +1,323 @@ +package org.kingdoms.utils.fs; + +import com.google.common.base.Strings; +import org.kingdoms.utils.internal.functional.Fn; +import org.kingdoms.utils.internal.arrays.ArrayUtils; +import org.kingdoms.utils.internal.runnables.IORunnable; + +import java.io.*; +import java.nio.channels.Channels; +import java.nio.channels.FileChannel; +import java.nio.channels.FileLock; +import java.nio.channels.WritableByteChannel; +import java.nio.charset.StandardCharsets; +import java.nio.file.*; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.*; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.BiConsumer; +import java.util.function.BiPredicate; +import java.util.function.Predicate; +import java.util.stream.StreamSupport; + +public final class FSUtil { + public static final StandardOpenOption[] STD_WRITER = {StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING}; + private static final int DEFAULT_BUFFER_SIZE = 8192; + + /** + * There's no better way in Java. We're bound by this stupid encapsulation of Java. + */ + public static int countEntriesOf(Path folder) { + if (!Files.isDirectory(folder)) + throw new IllegalArgumentException("Path is not a folder: " + folder.toAbsolutePath()); + try (DirectoryStream fs = Files.newDirectoryStream(folder)) { + return ArrayUtils.sizeOfIterator(fs.iterator()); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + /** + * Mainly here because of Kotlin's stupid unnecessary checks and that array copy for varargs. + */ + public static BufferedWriter standardWriter(Path path) throws IOException { + return Files.newBufferedWriter(path, StandardCharsets.UTF_8, STD_WRITER); + } + + public static int countEntriesOf(Path folder, Predicate filter) { + if (!Files.isDirectory(folder)) + throw new IllegalArgumentException("Path is not a folder: " + folder.toAbsolutePath()); + try (DirectoryStream fs = Files.newDirectoryStream(folder)) { + return (int) StreamSupport.stream(fs.spliterator(), false).filter(filter).count(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public static Path findSlotForCounterFile(Path folder, String prefix, String extension) { + int counter = 1; + + Path file; + do { + file = folder.resolve(prefix + '-' + counter++ + '.' + extension); + } while (Files.exists(file)); + + return file; + } + + public static Path findSlotForCounterFolder(Path folder, String prefix) { + int counter = 1; + + Path file; + do { + file = folder.resolve(prefix + '-' + counter++); + } while (Files.exists(file) && Files.isDirectory(file)); + + return file; + } + + /** + * Similr to {@link #transfer(InputStream, OutputStream)} but first tries to lock "from" + * before writing. This is only useful of "beforeWrite" parameter is used. + *

+ * Mainly designed to + */ + public void lockBeforeCopy(Path from, OutputStream to, Runnable beforeWrite) throws IOException { + try (FileChannel fileChannel = FileChannel.open(from, StandardOpenOption.READ)) { + try (FileLock lock = fileChannel.lock(0, Long.MAX_VALUE, true)) { + WritableByteChannel zsChan = Channels.newChannel(to); + // magic number for Windows, (64Mb - 32Kb) + long maxCount = (64 * 1024 * 1024L) - (32 * 1024L); + long size = fileChannel.size(); + long position = 0L; + + beforeWrite.run(); + while (size > 0) { + long count = fileChannel.transferTo(position, Math.min(maxCount, size), zsChan); + position += count; + size -= count; + } + } + } + } + + private static final Set ILLEGAL_CHARACTERS = new HashSet<>(Arrays.asList( + // All + '/', '\\', '\n', '\r', '\t', '\0', '\f', '`', '?', '*', '<', '>', '|', '\"', ':', + + // Unix Specific + '\000', + + // Windows Specific + '"', '*', '<', '>', '?', '|' + )); + + private static boolean isInvalidFileNameChar(char ch) { + return Character.isISOControl(ch) || ILLEGAL_CHARACTERS.contains(ch); + } + + public static boolean isValidPath(String path) { + if (Strings.isNullOrEmpty(path)) return false; + try { + Paths.get(path); + } catch (InvalidPathException ex) { + return false; + } + return true; + } + + public static boolean isValidFileName(String name) { + for (char ch : name.toCharArray()) { + if (isInvalidFileNameChar(ch)) return false; + } + return true; + } + + public static String removeInvalidFileChars(String name, String replaceWith) { + StringBuilder builder = new StringBuilder(); + for (char ch : name.toCharArray()) { + if (isInvalidFileNameChar(ch)) { + builder.append(replaceWith); + } else { + builder.append(ch); + } + } + return builder.toString(); + } + + public static String oneOfValidFileNames(String... names) { + for (String name : names) { + if (isValidFileName(name)) return name; + } + throw new IllegalArgumentException("None of the file names are valid: " + Arrays.toString(names)); + } + + public static boolean isFolderEmpty(Path folder) { + return countEntriesOf(folder) == 0; + } + + public static void deleteFolder(Path folder) { + deleteFolder(folder, Fn.alwaysFalse()); + } + + /** + * Deletes a folder entirely if it exists. + */ + @SuppressWarnings("resource") + public static void deleteFolder(Path folder, Predicate ignore) { + if (!Files.exists(folder)) return; + try { + AtomicBoolean errored = new AtomicBoolean(); + Files.list(folder).forEach(path -> { + try { + if (folder.equals(path)) return; + if (ignore.test(path)) return; + + if (Files.isDirectory(path)) deleteFolder(path); + else Files.delete(path); + } catch (IOException ex) { + errored.set(true); + ex.printStackTrace(); + } + }); + if (!errored.get()) Files.delete(folder); + } catch (IOException ex) { + throw new RuntimeException(ex); + } + } + + @SuppressWarnings("resource") + public static void deleteAllFileTypes(Path folder, String type) { + try { + Files.list(folder).forEach(path -> { + try { + if (folder.equals(path)) return; + if (!path.toString().endsWith(type)) return; // path.endsWith() doesn't work it's fucking useless + if (Files.isDirectory(path)) return; + Files.delete(path); + } catch (IOException ex) { + ex.printStackTrace(); + } + }); + } catch (IOException ex) { + ex.printStackTrace(); + } + } + + /** + * Gets all the files in this folder and it's sub-folders. + * None of the returned paths point to any of the folders, only the files. + */ + public static List getFiles(Path folder) { + List files = new ArrayList<>(); + PathIterator iterator = new PathIterator( + null, + (p, attrs) -> {if (attrs.isRegularFile()) files.add(p);} + ); + try { + Files.walkFileTree(folder, iterator); + return files; + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public static InputStream stringToInputStream(String string) { + return new ByteArrayInputStream(string.getBytes(StandardCharsets.UTF_8)); + } + + public static void transfer(InputStream in, OutputStream out) throws IOException { + Objects.requireNonNull(in, "in"); + Objects.requireNonNull(out, "out"); + + byte[] buffer = new byte[DEFAULT_BUFFER_SIZE]; + int read; + while ((read = in.read(buffer, 0, DEFAULT_BUFFER_SIZE)) >= 0) { + out.write(buffer, 0, read); + } + } + + public static void copyFolder(Path source, Path destination) { + try { + Files.walkFileTree(source, new CopyFileVisitor(source, destination)); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + /** + * Attempts to lock the entire file before transferring it to another stream. + * This prevents doing things after making sure the file can be read at all, + * mostly useful for putting zip enteries. + * + * @param beforeTransfer must be present. This method is useless without it. + */ + public static void lockAndTransfer(Path file, OutputStream transferTo, IORunnable beforeTransfer) throws IOException { + try (FileChannel fileChannel = FileChannel.open(file, StandardOpenOption.READ); + FileLock lock = fileChannel.lock(0, Long.MAX_VALUE, true)) { + WritableByteChannel zsChan = Channels.newChannel(transferTo); + // magic number for Windows, (64Mb - 32Kb) + long maxCount = (64 * 1024 * 1024L) - (32 * 1024L); + long size = fileChannel.size(); + long position = 0L; + + beforeTransfer.run(); + while (size > 0) { + long count = fileChannel.transferTo(position, Math.min(maxCount, size), zsChan); + position += count; + size -= count; + } + } + } + + private static final class PathIterator extends SimpleFileVisitor { + public final BiConsumer visitor; + private final BiPredicate filter; + + private PathIterator(BiPredicate filter, BiConsumer consumer) { + this.visitor = consumer; + this.filter = filter; + } + + private FileVisitResult visit(Path path, BasicFileAttributes attrs) { + if (filter == null || filter.test(path, attrs)) { + visitor.accept(path, attrs); + return FileVisitResult.CONTINUE; + } + return FileVisitResult.SKIP_SUBTREE; + } + + @Override + public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) { + return visit(dir, attrs); + } + + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) { + return visit(file, attrs); + } + } + + private static final class CopyFileVisitor extends SimpleFileVisitor { + private final Path sourcePath, targetPath; + + public CopyFileVisitor(Path sourcePath, Path targetPath) { + this.sourcePath = sourcePath; + this.targetPath = targetPath; + } + + @Override + public FileVisitResult preVisitDirectory(final Path dir, + final BasicFileAttributes attrs) throws IOException { + Files.createDirectories(targetPath.resolve(sourcePath.relativize(dir))); + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult visitFile(final Path file, + final BasicFileAttributes attrs) throws IOException { + Files.copy(file, targetPath.resolve(sourcePath.relativize(file))); + return FileVisitResult.CONTINUE; + } + } +} diff --git a/shared/src/main/java/org/kingdoms/utils/fs/walker/FileTreeIterator.java b/shared/src/main/java/org/kingdoms/utils/fs/walker/FileTreeIterator.java new file mode 100644 index 000000000..98d90ce4c --- /dev/null +++ b/shared/src/main/java/org/kingdoms/utils/fs/walker/FileTreeIterator.java @@ -0,0 +1,110 @@ +package org.kingdoms.utils.fs.walker; + +import org.kingdoms.utils.fs.walker.visitors.PathVisit; + +import java.io.Closeable; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.FileVisitOption; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.Iterator; +import java.util.NoSuchElementException; + +/** + * An {@code Iterator} to iterate over the nodes of a file tree. + * + * {@snippet lang = java: + * try (FileTreeIterator iterator = new FileTreeIterator(start, maxDepth, options)) { + * while (iterator.hasNext()) { + * Event ev = iterator.next(); + * Path path = ev.file(); + * BasicFileAttributes attrs = ev.attributes(); + * } + * } + *} + */ + +class FileTreeIterator implements Iterator, Closeable, FileWalkerController { + private final FileTreeWalker walker; + private PathVisit next; + + /** + * Creates a new iterator to walk the file tree starting at the given file. + * + * @throws IllegalArgumentException + * if {@code maxDepth} is negative + * @throws IOException + * if an I/O errors occurs opening the starting file + * @throws SecurityException + * if the security manager denies access to the starting file + * @throws NullPointerException + * if {@code start} or {@code options} is {@code null} or + * the options array contains a {@code null} element + */ + FileTreeIterator(Path start, int maxDepth, FileVisitOption... options) throws IOException { + this.walker = new FileTreeWalker(Arrays.asList(options), maxDepth); + this.next = walker.walk(start); + assert next.getVisitType() == PathVisit.Type.ENTRY || + next.getVisitType() == PathVisit.Type.START_DIRECTORY; + + IOException ioe = next.getException(); + if (ioe != null) throw ioe; + } + + private void fetchNextIfNeeded() { + if (next == null) { + PathVisit ev = walker.next(); + while (ev != null) { + IOException ioe = ev.getException(); + if (ioe != null) + throw new UncheckedIOException(ioe); + + // END_DIRECTORY events are ignored + if (ev.getVisitType() != PathVisit.Type.END_DIRECTORY) { + next = ev; + return; + } + ev = walker.next(); + } + } + } + + @Override + public boolean hasNext() { + if (!walker.isOpen()) + throw new IllegalStateException(); + + fetchNextIfNeeded(); + return next != null; + } + + @Override + public PathVisit next() { + if (!walker.isOpen()) + throw new IllegalStateException(); + + fetchNextIfNeeded(); + if (next == null) + throw new NoSuchElementException(); + + PathVisit result = next; + next = null; + return result; + } + + @Override + public void skipDirectory() { + walker.skipDirectory(); + } + + @Override + public void skipRemainingSiblings() { + walker.skipRemainingSiblings(); + } + + @Override + public void close() { + walker.close(); + } +} diff --git a/shared/src/main/java/org/kingdoms/utils/fs/walker/FileTreeWalker.java b/shared/src/main/java/org/kingdoms/utils/fs/walker/FileTreeWalker.java new file mode 100644 index 000000000..418baf415 --- /dev/null +++ b/shared/src/main/java/org/kingdoms/utils/fs/walker/FileTreeWalker.java @@ -0,0 +1,352 @@ +package org.kingdoms.utils.fs.walker; + +import org.kingdoms.utils.fs.walker.visitors.PathVisit; +import org.kingdoms.utils.fs.walker.visitors.PathVisit.Type; +import org.kingdoms.utils.fs.walker.visitors.PathVisitor; + +import java.io.Closeable; +import java.io.IOException; +import java.nio.file.*; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.*; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + + +/** + * Walks a file tree, generating a sequence of events corresponding to the files + * in the tree. + */ +public class FileTreeWalker implements Closeable, FileWalkerController { + private final boolean followLinks; + private final LinkOption[] linkOptions; + private final int maxDepth; + private final ArrayDeque stack = new ArrayDeque<>(); + private boolean closed; + + public static Stream walk(Path start, + Set options, + int maxDepth, + AtomicReference controller) throws IOException { + FileTreeIterator iterator = new FileTreeIterator(start, maxDepth, options.toArray(new FileVisitOption[0])); + controller.set(iterator); + try { + Spliterator spliterator = + Spliterators.spliteratorUnknownSize(iterator, Spliterator.DISTINCT); + return StreamSupport.stream(spliterator, false) + .onClose(iterator::close); + } catch (Error | RuntimeException ex) { + iterator.close(); + throw ex; + } + } + + public static void walkFileTree(Path start, + Set options, + int maxDepth, + PathVisitor visitor) { + try (FileTreeWalker walker = new FileTreeWalker(options, maxDepth)) { + PathVisit ev = walker.walk(start); + do { + FileVisitResult result; + switch (ev.getVisitType()) { + case ENTRY: { + result = visitor.onVisit(ev); + break; + } + case START_DIRECTORY: { + // FileVisitResult res = visitor.preVisitDirectory(ev.file, ev.attrs); + FileVisitResult res = visitor.onVisit(ev); + + // if SKIP_SIBLINGS and SKIP_SUBTREE is returned then + // there shouldn't be any more events for the current + // directory. + if (res == FileVisitResult.SKIP_SUBTREE || res == FileVisitResult.SKIP_SIBLINGS) { + walker.skipDirectory(); + } + result = res; + break; + } + case END_DIRECTORY: { + FileVisitResult res = visitor.onVisit(ev); + + // SKIP_SIBLINGS is a no-op for postVisitDirectory + if (res == FileVisitResult.SKIP_SIBLINGS) { + res = FileVisitResult.CONTINUE; + } + result = res; + break; + } + default: + throw new AssertionError("Should not get here: " + ev.getVisitType()); + } + + if (Objects.requireNonNull(result) != FileVisitResult.CONTINUE) { + if (result == FileVisitResult.TERMINATE) { + break; + } else if (result == FileVisitResult.SKIP_SIBLINGS) { + walker.skipRemainingSiblings(); + } + } + ev = walker.next(); + } while (ev != null); + } + } + + /** + * The element on the walking stack corresponding to a directory node. + */ + private static class DirectoryNode { + private final Path dir; + private final Object key; + private final DirectoryStream stream; + private final Iterator iterator; + private boolean skipped; + + DirectoryNode(Path dir, Object key, DirectoryStream stream) { + this.dir = dir; + this.key = key; + this.stream = stream; + this.iterator = stream.iterator(); + } + + void skip() { + skipped = true; + } + } + + /** + * Creates a {@code FileTreeWalker}. + * + * @throws IllegalArgumentException + * if {@code maxDepth} is negative + * @throws ClassCastException + * if {@code options} contains an element that is not a + * {@code FileVisitOption} + * @throws NullPointerException + * if {@code options} is {@code null} or the options + * array contains a {@code null} element + */ + FileTreeWalker(Collection options, int maxDepth) { + boolean fl = false; + for (FileVisitOption option : options) { + // will throw NPE if options contains null + switch (option) { + case FOLLOW_LINKS: + fl = true; + break; + default: + throw new AssertionError("Should not get here"); + } + } + if (maxDepth < 0) + throw new IllegalArgumentException("'maxDepth' is negative"); + + this.followLinks = fl; + this.linkOptions = (fl) ? new LinkOption[0] : new LinkOption[]{LinkOption.NOFOLLOW_LINKS}; + this.maxDepth = maxDepth; + } + + /** + * Returns the attributes of the given file, taking into account whether + * the walk is following sym links is not. The {@code canUseCached} + * argument determines whether this method can use cached attributes. + */ + private BasicFileAttributes getAttributes(Path file) throws IOException { + // attempt to get attributes of file. If fails and we are following + // links then a link target might not exist so get attributes of link + BasicFileAttributes attrs; + try { + attrs = Files.readAttributes(file, BasicFileAttributes.class, linkOptions); + } catch (IOException ioe) { + if (!followLinks) throw ioe; + + // attempt to get attrmptes without following links + attrs = Files.readAttributes(file, + BasicFileAttributes.class, + LinkOption.NOFOLLOW_LINKS + ); + } + return attrs; + } + + /** + * Returns true if walking into the given directory would result in a + * file system loop/cycle. + */ + private boolean wouldLoop(Path dir, Object key) { + // if this directory and ancestor has a file key then we compare + // them; otherwise we use less efficient isSameFile test. + for (DirectoryNode ancestor : stack) { + Object ancestorKey = ancestor.key; + if (key != null && ancestorKey != null) { + if (key.equals(ancestorKey)) { + return true; // Cycle detected + } + } else { + try { + if (Files.isSameFile(dir, ancestor.dir)) { + return true; // Cycle detected + } + } catch (IOException | SecurityException ignored) { + } + } + } + return false; + } + + /** + * Visits the given file, returning the {@code Event} corresponding to that + * visit. + * + * The {@code ignoreSecurityException} parameter determines whether + * any SecurityException should be ignored or not. If a SecurityException + * is thrown, and is ignored, then this method returns {@code null} to + * mean that there is no event corresponding to a visit to the file. + * + * The {@code canUseCached} parameter determines whether cached attributes + * for the file can be used or not. + */ + private PathVisit visit(Path entry, boolean ignoreSecurityException, boolean canUseCached) { + // need the file attributes + BasicFileAttributes attrs; + try { + attrs = getAttributes(entry); + } catch (IOException ioe) { + return new PathVisit(Type.ENTRY, entry, ioe); + } catch (SecurityException se) { + if (ignoreSecurityException) + return null; + throw se; + } + + // at maximum depth or file is not a directory + int depth = stack.size(); + if (depth >= maxDepth || !attrs.isDirectory()) { + return new PathVisit(Type.ENTRY, entry, attrs); + } + + // check for cycles when following links + if (followLinks && wouldLoop(entry, attrs.fileKey())) { + return new PathVisit(Type.ENTRY, entry, + new FileSystemLoopException(entry.toString())); + } + + // file is a directory, attempt to open it + DirectoryStream stream = null; + try { + stream = Files.newDirectoryStream(entry); + } catch (IOException ioe) { + return new PathVisit(Type.ENTRY, entry, ioe); + } catch (SecurityException se) { + if (ignoreSecurityException) + return null; + throw se; + } + + // push a directory node to the stack and return an event + stack.push(new DirectoryNode(entry, attrs.fileKey(), stream)); + return new PathVisit(Type.START_DIRECTORY, entry, attrs); + } + + /** + * Start walking from the given file. + */ + PathVisit walk(Path file) { + if (closed) + throw new IllegalStateException("Closed"); + + PathVisit ev = visit(file, + false, // ignoreSecurityException + false); // canUseCached + assert ev != null; + return ev; + } + + /** + * Returns the next Event or {@code null} if there are no more events or + * the walker is closed. + */ + PathVisit next() { + DirectoryNode top = stack.peek(); + if (top == null) + return null; // stack is empty, we are done + + // continue iteration of the directory at the top of the stack + PathVisit ev; + do { + Path entry = null; + IOException ioe = null; + + // get next entry in the directory + if (!top.skipped) { + Iterator iterator = top.iterator; + try { + if (iterator.hasNext()) { + entry = iterator.next(); + } + } catch (DirectoryIteratorException x) { + ioe = x.getCause(); + } + } + + // no next entry so close and pop directory, + // creating corresponding event + if (entry == null) { + try { + top.stream.close(); + } catch (IOException e) { + if (ioe == null) { + ioe = e; + } else { + ioe.addSuppressed(e); + } + } + stack.pop(); + return new PathVisit(Type.END_DIRECTORY, top.dir, ioe); + } + + // visit the entry + ev = visit(entry, + true, // ignoreSecurityException + true); // canUseCached + + } while (ev == null); + + return ev; + } + + @Override + public void skipDirectory() { + if (!stack.isEmpty()) { + DirectoryNode node = stack.pop(); + try { + node.stream.close(); + } catch (IOException ignore) { + } + } + } + + @Override + public void skipRemainingSiblings() { + if (!stack.isEmpty()) { + stack.peek().skip(); + } + } + + public boolean isOpen() { + return !closed; + } + + /** + * Closes/pops all directories on the stack. + */ + @Override + public void close() { + if (!closed) { + while (!stack.isEmpty()) skipDirectory(); + closed = true; + } + } +} diff --git a/shared/src/main/java/org/kingdoms/utils/fs/walker/FileWalkerController.kt b/shared/src/main/java/org/kingdoms/utils/fs/walker/FileWalkerController.kt new file mode 100644 index 000000000..0e4a0c00f --- /dev/null +++ b/shared/src/main/java/org/kingdoms/utils/fs/walker/FileWalkerController.kt @@ -0,0 +1,28 @@ +package org.kingdoms.utils.fs.walker + +import java.nio.file.FileVisitResult + +interface FileWalkerController { + /** + * Pops the directory node that is the current top of the stack so that + * there are no more events for the directory (including no END_DIRECTORY) + * event. This method is a no-op if the stack is empty or the walker is + * closed. + */ + fun skipDirectory() + + /** + * Skips the remaining entries in the directory at the top of the stack. + * This method is a no-op if the stack is empty or the walker is closed. + */ + fun skipRemainingSiblings() + + fun close() + + fun processResult(result: FileVisitResult) = when (result) { + FileVisitResult.TERMINATE -> close() + FileVisitResult.SKIP_SUBTREE -> skipDirectory() + FileVisitResult.SKIP_SIBLINGS -> skipRemainingSiblings() + FileVisitResult.CONTINUE -> {} + } +} \ No newline at end of file diff --git a/shared/src/main/java/org/kingdoms/utils/fs/walker/visitors/FunctionalPathVisitor.kt b/shared/src/main/java/org/kingdoms/utils/fs/walker/visitors/FunctionalPathVisitor.kt new file mode 100644 index 000000000..a34ce31df --- /dev/null +++ b/shared/src/main/java/org/kingdoms/utils/fs/walker/visitors/FunctionalPathVisitor.kt @@ -0,0 +1,67 @@ +package org.kingdoms.utils.fs.walker.visitors + +import java.nio.file.FileVisitResult +import java.nio.file.Path +import java.util.function.Consumer +import java.util.function.Predicate + +class FunctionalPathVisitor(val root: Path) : PathVisitor { + var visitors: MutableList = arrayListOf() + + private fun getPathPredicate(folder: Boolean, resolvablePath: Path) = + if (folder) StartsWithPathFilter(resolvablePath) + else ExactPathFilter(resolvablePath) + + private fun stringVisitor(folder: Boolean, resolvablePath: String, visit: Boolean): FunctionalPathVisitor { + var exactPath: Path = root + resolvablePath.split('/').forEach { exactPath = root.resolve(it) } + return visitor(folder, getPathPredicate(folder, exactPath), visit) + } + + private fun pathVisitor(folder: Boolean, path: Path, visit: Boolean): FunctionalPathVisitor { + require(path.startsWith(root)) { "Given path '$path' isn't included in the root path '$root'" } + return visitor(folder, getPathPredicate(folder, path), visit) + } + + private fun visitor(folder: Boolean, filter: Predicate, visit: Boolean): FunctionalPathVisitor { + val handle = ConditionalPathVisitor(filter, if (visit) VisitAll else SkipAll) + visitors.add(handle) + return this + } + + fun onlyIf(condition: Boolean, handler: Consumer): FunctionalPathVisitor { + if (condition) handler.accept(this) + return this + } + + fun visitFiles(filter: Predicate): FunctionalPathVisitor = visitor(false, filter, true) + fun skipFiles(filter: Predicate): FunctionalPathVisitor = visitor(false, filter, true) + + fun visitFolders(filter: Predicate): FunctionalPathVisitor = visitor(true, filter, true) + fun skipFolders(filter: Predicate): FunctionalPathVisitor = visitor(true, filter, true) + + + fun visitFile(resolvablePath: String): FunctionalPathVisitor = stringVisitor(false, resolvablePath, true) + fun skipFile(resolvablePath: String): FunctionalPathVisitor = stringVisitor(false, resolvablePath, false) + + fun visitFolder(resolvablePath: String): FunctionalPathVisitor = stringVisitor(true, resolvablePath, true) + fun skipFolder(resolvablePath: String): FunctionalPathVisitor = stringVisitor(true, resolvablePath, false) + + fun visitFile(path: Path): FunctionalPathVisitor = pathVisitor(false, path, true) + fun skipFile(path: Path): FunctionalPathVisitor = pathVisitor(false, path, false) + + fun visitFolder(path: Path): FunctionalPathVisitor = pathVisitor(true, path, true) + fun skipFolder(path: Path): FunctionalPathVisitor = pathVisitor(true, path, false) + + override fun onVisit(visit: PathVisit): FileVisitResult { + if (visit.path == root) return FileVisitResult.CONTINUE + + for (pathVisitHandle in visitors) { + if (pathVisitHandle.predicate.test(visit.path)) { + return pathVisitHandle.visitor.onVisit(visit) + } + } + + return FileVisitResult.SKIP_SUBTREE + } +} \ No newline at end of file diff --git a/shared/src/main/java/org/kingdoms/utils/fs/walker/visitors/PathVisit.kt b/shared/src/main/java/org/kingdoms/utils/fs/walker/visitors/PathVisit.kt new file mode 100644 index 000000000..16d090ef3 --- /dev/null +++ b/shared/src/main/java/org/kingdoms/utils/fs/walker/visitors/PathVisit.kt @@ -0,0 +1,27 @@ +package org.kingdoms.utils.fs.walker.visitors + +import java.io.IOException +import java.nio.file.Path +import java.nio.file.attribute.BasicFileAttributes + +class PathVisit internal constructor( + val visitType: Type, + val path: Path, + val attributes: BasicFileAttributes?, + val exception: IOException? +) { + internal constructor(type: Type, file: Path, attrs: BasicFileAttributes?) : this(type, file, attrs, null) + internal constructor(type: Type, file: Path, ioe: IOException?) : this(type, file, null, ioe) + + fun hasErrors(): Boolean = exception != null + + enum class Type { + START_DIRECTORY, + END_DIRECTORY, + + /** + * An entry (file) in a directory + */ + ENTRY + } +} \ No newline at end of file diff --git a/shared/src/main/java/org/kingdoms/utils/fs/walker/visitors/PathVisitors.kt b/shared/src/main/java/org/kingdoms/utils/fs/walker/visitors/PathVisitors.kt new file mode 100644 index 000000000..68f0ec4bb --- /dev/null +++ b/shared/src/main/java/org/kingdoms/utils/fs/walker/visitors/PathVisitors.kt @@ -0,0 +1,69 @@ +package org.kingdoms.utils.fs.walker.visitors + +import org.kingdoms.utils.fs.walker.FileTreeWalker +import org.kingdoms.utils.fs.walker.FileWalkerController +import java.nio.file.FileVisitResult +import java.nio.file.Path +import java.util.concurrent.atomic.AtomicReference +import java.util.function.Predicate +import java.util.stream.Stream + +class ConditionalPathVisitor(val predicate: Predicate, val visitor: PathVisitor) { + override fun toString(): String = "ConditionalPathVisitor($predicate -> $visitor)" +} + +class ExactPathFilter(val exactPath: Path) : Predicate { + override fun test(path: Path): Boolean = exactPath == path + + override fun toString(): String = "ExactPathFilter($exactPath)" +} + +class StartsWithPathFilter(val startsWith: Path) : Predicate { + override fun test(path: Path): Boolean = path.startsWith(startsWith) + + override fun toString(): String = "StartsWithPathFilter($startsWith)" +} + +interface PathVisitor { + fun onVisit(visit: PathVisit): FileVisitResult + fun collect(root: Path) = CollectorPathVisitor(root, this).getFiles() + fun stream(root: Path): Stream { + val controller = AtomicReference() + return FileTreeWalker.walk(root, hashSetOf(), Integer.MAX_VALUE, controller).filter { + val result = onVisit(it) + controller.get().processResult(result) + when (result) { + FileVisitResult.CONTINUE, FileVisitResult.SKIP_SIBLINGS -> true + else -> false + } + } + } +} + +class CollectorPathVisitor(val root: Path, val visitor: PathVisitor) : PathVisitor { + val list: MutableList = arrayListOf() + + override fun onVisit(visit: PathVisit): FileVisitResult { + val result = visitor.onVisit(visit) + when (result) { + FileVisitResult.CONTINUE, FileVisitResult.SKIP_SIBLINGS -> list.add(visit.path) + else -> {} + } + return result + } + + fun getFiles(): List { + FileTreeWalker.walkFileTree(root, hashSetOf(), Integer.MAX_VALUE, this) + return list + } +} + +object VisitAll : PathVisitor { + override fun onVisit(visit: PathVisit) = FileVisitResult.CONTINUE + override fun toString(): String = "PathVisitor:VisitAll" +} + +object SkipAll : PathVisitor { + override fun onVisit(visit: PathVisit) = FileVisitResult.SKIP_SUBTREE + override fun toString(): String = "PathVisitor:SkipAll" +} diff --git a/shared/src/main/java/org/kingdoms/utils/internal/PurifierMap.kt b/shared/src/main/java/org/kingdoms/utils/internal/KeyTransformerMap.kt similarity index 70% rename from shared/src/main/java/org/kingdoms/utils/internal/PurifierMap.kt rename to shared/src/main/java/org/kingdoms/utils/internal/KeyTransformerMap.kt index 31e92ba28..695848b1d 100644 --- a/shared/src/main/java/org/kingdoms/utils/internal/PurifierMap.kt +++ b/shared/src/main/java/org/kingdoms/utils/internal/KeyTransformerMap.kt @@ -1,16 +1,18 @@ package org.kingdoms.utils.internal -@Suppress("ReplaceGetOrSet") class PurifierMap( +import java.util.function.Function + +@Suppress("ReplaceGetOrSet") class KeyTransformerMap( private val original: MutableMap, - private val purifier: Purifier + private val keyTransformation: Function ) : MutableMap by original { companion object { - @JvmStatic fun MutableMap.purify(purifier: Purifier) = PurifierMap(this, purifier) + @JvmStatic fun MutableMap.purify(purifier: Function) = KeyTransformerMap(this, purifier) } @Suppress("NOTHING_TO_INLINE") - private inline fun purify(k: K) = purifier.purify(k) + private inline fun purify(k: K) = keyTransformation.apply(k) override fun remove(key: K): V? = original.remove(purify(key)) override fun putAll(from: Map) = original.putAll(from.map { purify(it.key) to it.value }) diff --git a/shared/src/main/java/org/kingdoms/utils/internal/KoltinInternalExtensions.kt b/shared/src/main/java/org/kingdoms/utils/internal/KoltinInternalExtensions.kt index 68c368442..6c9b62e83 100644 --- a/shared/src/main/java/org/kingdoms/utils/internal/KoltinInternalExtensions.kt +++ b/shared/src/main/java/org/kingdoms/utils/internal/KoltinInternalExtensions.kt @@ -2,6 +2,8 @@ package org.kingdoms.utils.internal +import org.kingdoms.utils.internal.functional.Fn + inline fun MutableList.replaceLast(crossinline newValue: (T) -> T) { if (this.isEmpty()) throw IllegalStateException("Cannot replace last element in an empty list: $this") this[this.size - 1] = newValue(this[this.size - 1]) diff --git a/shared/src/main/java/org/kingdoms/utils/internal/NestedMap.java b/shared/src/main/java/org/kingdoms/utils/internal/NestedMap.java deleted file mode 100644 index 591a092bf..000000000 --- a/shared/src/main/java/org/kingdoms/utils/internal/NestedMap.java +++ /dev/null @@ -1,61 +0,0 @@ -package org.kingdoms.utils.internal; - -import java.util.Map; -import java.util.Objects; - -class NestedMap { - private Map child; - private V value; - - public NestedMap() { - this.child = null; - this.value = null; - } - - public boolean isTypeKnown() { - return this.child != null || this.value != null; - } - - public boolean hasChildren() { - return this.child != null; - } - - public NestedMap(Map child) { - this.child = Objects.requireNonNull(child); - this.value = null; - } - - public NestedMap(V value) { - this.child = null; - this.value = value; - } - - public NestedMap getChild() { - assertType(); - if (!hasChildren()) throw new IllegalStateException("This map isn't nested"); - return Fn.cast(this.child); - } - - public void setChild() { - setChild(new NestedMap<>()); - } - - public void setChild(NestedMap child) { - this.child = Fn.cast(child); - } - - private void assertType() { - if (!isTypeKnown()) throw new IllegalStateException("This map has no known type to get its values"); - } - - public V getValue() { - assertType(); - if (hasChildren()) throw new IllegalStateException("This map has more nested children"); - return value; - } - - public void setValue(V v) { - if (!isTypeKnown() || hasChildren()) throw new IllegalStateException("This map has more nested children"); - this.value = v; - } -} \ No newline at end of file diff --git a/shared/src/main/java/org/kingdoms/utils/internal/Purifier.kt b/shared/src/main/java/org/kingdoms/utils/internal/Purifier.kt deleted file mode 100644 index 245706ed7..000000000 --- a/shared/src/main/java/org/kingdoms/utils/internal/Purifier.kt +++ /dev/null @@ -1,5 +0,0 @@ -package org.kingdoms.utils.internal - -interface Purifier { - fun purify(original: T): T -} \ No newline at end of file diff --git a/shared/src/main/java/org/kingdoms/utils/internal/functional/Finalizer.java b/shared/src/main/java/org/kingdoms/utils/internal/functional/Finalizer.java new file mode 100644 index 000000000..a44befc38 --- /dev/null +++ b/shared/src/main/java/org/kingdoms/utils/internal/functional/Finalizer.java @@ -0,0 +1,42 @@ +package org.kingdoms.utils.internal.functional; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +/** + * A class that holds multiple {@link Runnable} which are added to this object when a certain condition is met + * but these runnables themselves change the state of other objects and they must only be run if all other + * conditions are met. + *

+ * All runnables are executed in insertion order. + * This finalizer is not re-usable and can be only ran once. + */ +public final class Finalizer implements Runnable { + private final List tasks = new ArrayList<>(5); + private boolean ran = false; + + private void ensureOpen() { + if (ran) throw new IllegalStateException("Finalizer was already executed"); + } + + public void add(Finalizer finalizer) { + ensureOpen(); + this.tasks.addAll(finalizer.tasks); + } + + public void add(Runnable runnable) { + ensureOpen(); + Objects.requireNonNull(runnable, "Cannot add null task"); + tasks.add(runnable); + } + + @Override + public void run() { + ensureOpen(); + ran = true; + for (Runnable task : tasks) { + task.run(); + } + } +} diff --git a/shared/src/main/java/org/kingdoms/utils/internal/Fn.java b/shared/src/main/java/org/kingdoms/utils/internal/functional/Fn.java similarity index 99% rename from shared/src/main/java/org/kingdoms/utils/internal/Fn.java rename to shared/src/main/java/org/kingdoms/utils/internal/functional/Fn.java index 20b7b9aff..e97e4ce0a 100644 --- a/shared/src/main/java/org/kingdoms/utils/internal/Fn.java +++ b/shared/src/main/java/org/kingdoms/utils/internal/functional/Fn.java @@ -1,4 +1,4 @@ -package org.kingdoms.utils.internal; +package org.kingdoms.utils.internal.functional; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; diff --git a/shared/src/main/java/org/kingdoms/utils/internal/functional/SecondarySupplier.java b/shared/src/main/java/org/kingdoms/utils/internal/functional/SecondarySupplier.java new file mode 100644 index 000000000..3721963f4 --- /dev/null +++ b/shared/src/main/java/org/kingdoms/utils/internal/functional/SecondarySupplier.java @@ -0,0 +1,12 @@ +package org.kingdoms.utils.internal.functional; + +import java.util.function.Supplier; + +/** + * This is simply a copy of {@link Supplier} to be used in methods parameters + * in order to bypass Java's type erasure backwards compatibility. + */ +@FunctionalInterface +public interface SecondarySupplier extends Supplier { + T get(); +} diff --git a/shared/src/main/java/org/kingdoms/utils/internal/functional/TriConsumer.java b/shared/src/main/java/org/kingdoms/utils/internal/functional/TriConsumer.java new file mode 100644 index 000000000..485e8574c --- /dev/null +++ b/shared/src/main/java/org/kingdoms/utils/internal/functional/TriConsumer.java @@ -0,0 +1,6 @@ +package org.kingdoms.utils.internal.functional; + +@FunctionalInterface +public interface TriConsumer { + void accept(A a, B b, C c); +} diff --git a/shared/src/main/java/org/kingdoms/utils/internal/iterator/Iterables.java b/shared/src/main/java/org/kingdoms/utils/internal/iterator/Iterables.java index c23dbad61..d9fb36b46 100644 --- a/shared/src/main/java/org/kingdoms/utils/internal/iterator/Iterables.java +++ b/shared/src/main/java/org/kingdoms/utils/internal/iterator/Iterables.java @@ -1,14 +1,16 @@ package org.kingdoms.utils.internal.iterator; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Iterator; -import java.util.List; +import org.kingdoms.utils.internal.arrays.ArrayIterator; + +import java.util.*; +import java.util.function.Function; import java.util.stream.Stream; public final class Iterables { private Iterables() {} + public static Iterator ofIterator(T[] array) {return ArrayIterator.of(array);} + public static java.lang.Iterable of(Iterator iterator) {return () -> iterator;} public static java.lang.Iterable of(Stream stream) {return stream::iterator;} @@ -25,4 +27,27 @@ public static > C collect(Iterator from, C to) { } return to; } + + public static void chooseRandom(Iterator items, Function fromList) { + List shuffled = Iterables.collect(items, new ArrayList<>()); + Collections.shuffle(shuffled); + + for (T item : shuffled) { + if (fromList.apply(item)) return; + } + } + + public static > C removeAndCollect(int elements, Collection removeFrom, C collectTo) { + if (elements < 0) throw new IllegalArgumentException("Cannot collect negative elements: " + elements); + if (elements == 0) return collectTo; + + Iterator iter = removeFrom.iterator(); + + while (elements-- > 0 && iter.hasNext()) { + collectTo.add(iter.next()); + iter.remove(); + } + + return collectTo; + } } diff --git a/shared/src/main/java/org/kingdoms/utils/internal/ListUtils.kt b/shared/src/main/java/org/kingdoms/utils/internal/iterator/ListUtils.kt similarity index 96% rename from shared/src/main/java/org/kingdoms/utils/internal/ListUtils.kt rename to shared/src/main/java/org/kingdoms/utils/internal/iterator/ListUtils.kt index d6a25a231..378437cee 100644 --- a/shared/src/main/java/org/kingdoms/utils/internal/ListUtils.kt +++ b/shared/src/main/java/org/kingdoms/utils/internal/iterator/ListUtils.kt @@ -1,4 +1,4 @@ -package org.kingdoms.utils.internal +package org.kingdoms.utils.internal.iterator object ListUtils { @JvmStatic fun List.plusAt(index: Int, vararg elements: T): List { diff --git a/shared/src/main/java/org/kingdoms/utils/internal/numbers/AnyNumber.kt b/shared/src/main/java/org/kingdoms/utils/internal/numbers/AnyNumber.kt index 9d082d351..04da1e020 100644 --- a/shared/src/main/java/org/kingdoms/utils/internal/numbers/AnyNumber.kt +++ b/shared/src/main/java/org/kingdoms/utils/internal/numbers/AnyNumber.kt @@ -1,24 +1,24 @@ package org.kingdoms.utils.internal.numbers -enum class NumberType( - val jvmClass: Class, - val minValue: Number, val maxValue: Number, - val byteSize: Int -) { - BYTE(Byte::class.java, Byte.MIN_VALUE, Byte.MAX_VALUE, Byte.SIZE_BYTES), - SHORT(Short::class.java, Short.MIN_VALUE, Short.MAX_VALUE, Short.SIZE_BYTES), - INT(Int::class.java, Int.MIN_VALUE, Int.MAX_VALUE, Int.SIZE_BYTES), - LONG(Long::class.java, Long.MIN_VALUE, Long.MAX_VALUE, Long.SIZE_BYTES), - FLOAT(Float::class.java, Float.MIN_VALUE, Float.MAX_VALUE, Float.SIZE_BYTES), - DOUBLE(Double::class.java, Double.MIN_VALUE, Double.MAX_VALUE, Double.SIZE_BYTES) -} +import org.kingdoms.constants.DataStringRepresentation -interface AnyNumber : Comparable { +/** + * An immutable, finite, non-NaN, non-Infinity number. + * If any of the operations causes the number to become NaN or -/+Infinity, + * a [IllegalStateException] is thrown. + */ +interface AnyNumber : Comparable, DataStringRepresentation { val value: Number val type: NumberType fun constructNew(value: Number): AnyNumber + val isNegative: Boolean + val isPositive: Boolean + val isZero: Boolean + val isEven: Boolean get() = rem(TWO) == ZERO + val isOdd: Boolean get() = !isEven + operator fun unaryMinus(): AnyNumber operator fun unaryPlus(): AnyNumber operator fun inc(): AnyNumber @@ -31,7 +31,49 @@ interface AnyNumber : Comparable { operator fun rem(other: AnyNumber): AnyNumber companion object { - @JvmStatic @get:JvmName("abstractNumber") val Float.abstractNumber: AnyNumber get() = _Float(this) - @JvmStatic @get:JvmName("abstractNumber") val Int.abstractNumber: AnyNumber get() = _Int(this) + private val TWO = of(2) + private val ZERO = of(0) + @JvmStatic @get:JvmSynthetic val Number.abstractNumber: AnyNumber get() = of(this) + @JvmStatic @get:JvmSynthetic val Float.abstractNumber: AnyNumber get() = _Float(this) + @JvmStatic @get:JvmSynthetic val Int.abstractNumber: AnyNumber get() = _Int(this) + @JvmStatic @get:JvmSynthetic val Double.abstractNumber: AnyNumber get() = _Double(this) + + @JvmStatic fun of(number: Number): AnyNumber = when (number) { + is Int -> _Int(number) + is Float -> _Float(number) + is Double -> _Double(number) + else -> throw UnsupportedOperationException("Unsupported number format: $number (${number.javaClass})") + } + + @JvmStatic fun of(float: Float): AnyNumber = _Float(float) + @JvmStatic fun of(int: Int): AnyNumber = _Int(int) + @JvmStatic fun of(double: Double): AnyNumber = _Double(double) + @JvmStatic fun of(string: String): AnyNumber? { + for (type in arrayOf(NumberType.INT, NumberType.LONG, NumberType.DOUBLE)) { + type.parseString(string)?.let { return it } + } + return null + } } } + +interface FloatingPointNumber : AnyNumber + +internal abstract class AbstractAnyNumber : AnyNumber { + final override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is AnyNumber) return false + return this.value == other.value + } + + final override fun hashCode(): Int = value.hashCode() + val asString: String get() = value.toString() + final override fun asDataString(): String = asString + final override fun toString(): String = this.type.name + "($value)" +} + +internal abstract class AbstractFloatingPointNumber : AbstractAnyNumber(), FloatingPointNumber { + protected inline fun requireFinite(requirement: Boolean, lazyMessage: () -> String) { + if (!requirement) throw NonFiniteNumberException(lazyMessage()) + } +} \ No newline at end of file diff --git a/shared/src/main/java/org/kingdoms/utils/internal/numbers/NonFiniteNumberException.kt b/shared/src/main/java/org/kingdoms/utils/internal/numbers/NonFiniteNumberException.kt new file mode 100644 index 000000000..20af77be3 --- /dev/null +++ b/shared/src/main/java/org/kingdoms/utils/internal/numbers/NonFiniteNumberException.kt @@ -0,0 +1,3 @@ +package org.kingdoms.utils.internal.numbers + +class NonFiniteNumberException(message: String?) : RuntimeException(message) \ No newline at end of file diff --git a/shared/src/main/java/org/kingdoms/utils/internal/numbers/NumberExtensions.kt b/shared/src/main/java/org/kingdoms/utils/internal/numbers/NumberExtensions.kt index 5c1e5c081..9d8604ddc 100644 --- a/shared/src/main/java/org/kingdoms/utils/internal/numbers/NumberExtensions.kt +++ b/shared/src/main/java/org/kingdoms/utils/internal/numbers/NumberExtensions.kt @@ -5,4 +5,13 @@ object NumberExtensions { inline fun Double.squared() = this * this inline fun Float.squared() = this * this inline fun Int.squared() = this * this + + inline val Number.isEven: Boolean get() = (this.toInt() % 2) == 0 + + @JvmStatic + fun Number.requireNonNegative() { + if (AnyNumber.of(this).isNegative) { + throw IllegalStateException("Required a non-negative number, but got: $this") + } + } } \ No newline at end of file diff --git a/shared/src/main/java/org/kingdoms/utils/internal/numbers/NumberImpl.kt b/shared/src/main/java/org/kingdoms/utils/internal/numbers/NumberImpl.kt index b23a11847..2d892a5c0 100644 --- a/shared/src/main/java/org/kingdoms/utils/internal/numbers/NumberImpl.kt +++ b/shared/src/main/java/org/kingdoms/utils/internal/numbers/NumberImpl.kt @@ -2,44 +2,84 @@ package org.kingdoms.utils.internal.numbers -internal class _Int(private var jvmValue: Int) : AnyNumber { +internal class _Int(override val value: Int) : AbstractAnyNumber() { override val type = NumberType.INT - override val value = this.jvmValue - private inline val AnyNumber.toThis: Int get() = this.value.toInt() + override val isNegative: Boolean get() = value < 0 + override val isPositive: Boolean get() = value > 0 + override val isZero: Boolean get() = value == 0 + + private inline val AnyNumber.convert: Int get() = this.value.toInt() override fun constructNew(value: Number): AnyNumber = _Int(value.toInt()) - override fun unaryMinus() = constructNew(-jvmValue) - override fun unaryPlus() = constructNew(+jvmValue) - override fun inc() = constructNew(jvmValue + 1) - override fun dec() = constructNew(jvmValue - 1) + override fun unaryMinus() = constructNew(-value) + override fun unaryPlus() = constructNew(+value) + override fun inc() = constructNew(value + 1) + override fun dec() = constructNew(value - 1) - override fun plus(other: AnyNumber) = constructNew(jvmValue + other.toThis) - override fun minus(other: AnyNumber) = constructNew(jvmValue - other.toThis) - override fun times(other: AnyNumber) = constructNew(jvmValue * other.toThis) - override fun div(other: AnyNumber) = constructNew(jvmValue / other.toThis) - override fun rem(other: AnyNumber) = constructNew(jvmValue % other.toThis) + override fun plus(other: AnyNumber) = constructNew(value + other.convert) + override fun minus(other: AnyNumber) = constructNew(value - other.convert) + override fun times(other: AnyNumber) = constructNew(value * other.convert) + override fun div(other: AnyNumber) = constructNew(value / other.convert) + override fun rem(other: AnyNumber) = constructNew(value % other.convert) - override fun compareTo(other: AnyNumber) = jvmValue.compareTo(other.toThis) + override fun compareTo(other: AnyNumber) = value.compareTo(other.convert) } -internal class _Float(private var jvmValue: Float) : AnyNumber { +internal class _Float(override val value: Float) : AbstractFloatingPointNumber() { override val type = NumberType.FLOAT - override val value = this.jvmValue - private inline val AnyNumber.toThis: Float get() = this.value.toFloat() + override val isNegative: Boolean get() = value < 0f + override val isPositive: Boolean get() = value > 0f + override val isZero: Boolean get() = value == 0f + + init { + requireFinite(!value.isNaN()) { "Value is not finite, but NaN: $value" } + requireFinite(!value.isInfinite()) { "Value is not finite, but Infinity: $value" } + } + + private inline val AnyNumber.convert: Float get() = this.value.toFloat() override fun constructNew(value: Number): AnyNumber = _Float(value.toFloat()) - override fun unaryMinus() = constructNew(-jvmValue) - override fun unaryPlus() = constructNew(+jvmValue) - override fun inc() = constructNew(jvmValue + 1) - override fun dec() = constructNew(jvmValue - 1) + override fun unaryMinus() = constructNew(-value) + override fun unaryPlus() = constructNew(+value) + override fun inc() = constructNew(value + 1f) + override fun dec() = constructNew(value - 1f) + + override fun plus(other: AnyNumber) = constructNew(value + other.convert) + override fun minus(other: AnyNumber) = constructNew(value - other.convert) + override fun times(other: AnyNumber) = constructNew(value * other.convert) + override fun div(other: AnyNumber) = constructNew(value / other.convert) + override fun rem(other: AnyNumber) = constructNew(value % other.convert) + + override fun compareTo(other: AnyNumber) = value.compareTo(other.convert) +} + +internal class _Double(override val value: Double) : AbstractFloatingPointNumber() { + override val type = NumberType.DOUBLE + + override val isNegative: Boolean get() = value < 0.0 + override val isPositive: Boolean get() = value > 0.0 + override val isZero: Boolean get() = value == 0.0 + + init { + requireFinite(!value.isNaN()) { "Value is not finite, but NaN: $value" } + requireFinite(!value.isInfinite()) { "Value is not finite, but Infinity: $value" } + } + + private inline val AnyNumber.convert: Double get() = this.value.toDouble() + override fun constructNew(value: Number): AnyNumber = _Double(value.toDouble()) + + override fun unaryMinus() = constructNew(-value) + override fun unaryPlus() = constructNew(+value) + override fun inc() = constructNew(value + 1.0) + override fun dec() = constructNew(value - 1.0) - override fun plus(other: AnyNumber) = constructNew(jvmValue + other.toThis) - override fun minus(other: AnyNumber) = constructNew(jvmValue - other.toThis) - override fun times(other: AnyNumber) = constructNew(jvmValue * other.toThis) - override fun div(other: AnyNumber) = constructNew(jvmValue / other.toThis) - override fun rem(other: AnyNumber) = constructNew(jvmValue % other.toThis) + override fun plus(other: AnyNumber) = constructNew(value + other.convert) + override fun minus(other: AnyNumber) = constructNew(value - other.convert) + override fun times(other: AnyNumber) = constructNew(value * other.convert) + override fun div(other: AnyNumber) = constructNew(value / other.convert) + override fun rem(other: AnyNumber) = constructNew(value % other.convert) - override fun compareTo(other: AnyNumber) = jvmValue.compareTo(other.toThis) + override fun compareTo(other: AnyNumber) = value.compareTo(other.convert) } \ No newline at end of file diff --git a/shared/src/main/java/org/kingdoms/utils/internal/numbers/NumberProcessor.kt b/shared/src/main/java/org/kingdoms/utils/internal/numbers/NumberProcessor.kt new file mode 100644 index 000000000..bf3f77c33 --- /dev/null +++ b/shared/src/main/java/org/kingdoms/utils/internal/numbers/NumberProcessor.kt @@ -0,0 +1,54 @@ +package org.kingdoms.utils.internal.numbers + +import org.kingdoms.utils.internal.numbers.AnyNumber.Companion.of + +enum class NumberConstraint { INTEGER_ONLY, POSITIVE, ZERO_OR_POSITIVE } +enum class NumberFailReason { NOT_A_NUMBER, OUT_OF_BOUNDS, INTEGER_ONLY, POSITIVE, ZERO_OR_POSITIVE } + +class NumberProcessor(val string: String, val constraints: Set) { + val failedConstraints: MutableSet = hashSetOf() + private lateinit var _number: AnyNumber + val number: AnyNumber + get() { + require(failedConstraints.isEmpty()) { "Number processor has failed" } + return _number + } + + fun getMostImportantFailure(): NumberFailReason { + require(failedConstraints.isNotEmpty()) { "Number processor did not fail" } + return failedConstraints.minByOrNull { it.ordinal }!! + } + + val isSuccessful: Boolean get() = failedConstraints.isEmpty() + + private fun fail(reason: NumberFailReason) { + failedConstraints.add(reason) + } + + @Suppress("LocalVariableName") + fun process() { + val _number = NumberType.INT.parseString(string) ?: NumberType.DOUBLE.parseString(string) + if (_number == null) { + fail(NumberFailReason.NOT_A_NUMBER) + return + } + this._number = _number + + if (_number.type != NumberType.INT && constraints.contains(NumberConstraint.INTEGER_ONLY)) { + fail(NumberFailReason.INTEGER_ONLY) + } + + if (_number <= of(_number.type.minValue) || _number >= of(_number.type.maxValue)) fail(NumberFailReason.OUT_OF_BOUNDS) + if (constraints.contains(NumberConstraint.POSITIVE) && _number <= of(0)) fail(NumberFailReason.POSITIVE) + if (constraints.contains(NumberConstraint.ZERO_OR_POSITIVE) && _number < of(0)) fail(NumberFailReason.ZERO_OR_POSITIVE) + } + + companion object { + @JvmStatic fun getNumber(string: String, constraints: Collection): NumberProcessor { + return NumberProcessor(string, constraints.toSet()).apply { process() } + } + @JvmStatic fun getNumber(string: String, vararg constraints: NumberConstraint): NumberProcessor { + return NumberProcessor(string, constraints.toSet()).apply { process() } + } + } +} \ No newline at end of file diff --git a/shared/src/main/java/org/kingdoms/utils/internal/numbers/NumberType.kt b/shared/src/main/java/org/kingdoms/utils/internal/numbers/NumberType.kt new file mode 100644 index 000000000..74d5d31bb --- /dev/null +++ b/shared/src/main/java/org/kingdoms/utils/internal/numbers/NumberType.kt @@ -0,0 +1,39 @@ +package org.kingdoms.utils.internal.numbers + +enum class NumberType( + val jvmClass: Class, + val minValue: Number, val maxValue: Number, + val byteSize: Int +) { + BYTE(Byte::class.java, Byte.MIN_VALUE, Byte.MAX_VALUE, Byte.SIZE_BYTES) { + override fun parseStringRaw(string: String): AnyNumber = AnyNumber.of(java.lang.Byte.parseByte(string)) + }, + SHORT(Short::class.java, Short.MIN_VALUE, Short.MAX_VALUE, Short.SIZE_BYTES) { + override fun parseStringRaw(string: String): AnyNumber = AnyNumber.of(java.lang.Short.parseShort(string)) + }, + INT(Int::class.java, Int.MIN_VALUE, Int.MAX_VALUE, Int.SIZE_BYTES) { + override fun parseStringRaw(string: String): AnyNumber = AnyNumber.of(java.lang.Integer.parseInt(string)) + }, + LONG(Long::class.java, Long.MIN_VALUE, Long.MAX_VALUE, Long.SIZE_BYTES) { + override fun parseStringRaw(string: String): AnyNumber = AnyNumber.of(java.lang.Long.parseLong(string)) + }, + FLOAT(Float::class.java, Float.MIN_VALUE, Float.MAX_VALUE, Float.SIZE_BYTES) { + override fun parseStringRaw(string: String): AnyNumber = AnyNumber.of(java.lang.Float.parseFloat(string)) + }, + DOUBLE(Double::class.java, Double.MIN_VALUE, Double.MAX_VALUE, Double.SIZE_BYTES) { + override fun parseStringRaw(string: String): AnyNumber = AnyNumber.of(java.lang.Double.parseDouble(string)) + }; + + // val minValue: AnyNumber = AnyNumber.of(minValue) + // val maxValue: AnyNumber = AnyNumber.of(maxValue) + + protected abstract fun parseStringRaw(string: String): AnyNumber + + fun parseString(string: String): AnyNumber? = try { + parseStringRaw(string) + } catch (ignored: NumberFormatException) { + null + } + + val isFloatingPoint: Boolean get() = this == FLOAT || this == DOUBLE +} \ No newline at end of file diff --git a/shared/src/main/java/org/kingdoms/utils/internal/numbers/Radix.kt b/shared/src/main/java/org/kingdoms/utils/internal/numbers/Radix.kt index ea38afb20..257ccc2f7 100644 --- a/shared/src/main/java/org/kingdoms/utils/internal/numbers/Radix.kt +++ b/shared/src/main/java/org/kingdoms/utils/internal/numbers/Radix.kt @@ -4,6 +4,7 @@ enum class Radix(val radix: Int, val prefix: String) { BINARY(2, "0b"), OCTAL(8, "0o"), DECIMAL(10, ""), HEXADECIMAL(16, "0x"); companion object { + @Suppress("EnumValuesSoftDeprecate") @JvmField val RADIX: Array = values() } } \ No newline at end of file diff --git a/shared/src/main/java/org/kingdoms/utils/internal/reference/Reference.kt b/shared/src/main/java/org/kingdoms/utils/internal/reference/Reference.kt new file mode 100644 index 000000000..ef3fd4b01 --- /dev/null +++ b/shared/src/main/java/org/kingdoms/utils/internal/reference/Reference.kt @@ -0,0 +1,48 @@ +package org.kingdoms.utils.internal.reference + +import org.kingdoms.utils.internal.reference.Reference.Companion.throwEmpty +import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.atomic.AtomicReference + +interface Reference { + /** + * @throws NoSuchElementException if [exists] is false. + */ + fun get(): T + fun orElse(default: T) = if (exists()) get() else default + + fun set(value: T) + fun exists(): Boolean + fun remove() + + companion object { + @Suppress("NOTHING_TO_INLINE") + @JvmStatic + inline fun throwEmpty(): Nothing = throw NoSuchElementException("No value is present") + } +} + +class BoolReference(private var value: AtomicBoolean?) : Reference { + override fun get() = (value ?: throwEmpty()).get() + override fun exists(): Boolean = value != null + override fun remove() { + value = null + } + + override fun set(value: Boolean) { + if (this.value == null) this.value = AtomicBoolean(value) + else this.value!!.set(value) + } +} + +class ObjectReference(private var value: AtomicReference) : Reference { + override fun get() = (value.get() ?: throwEmpty()) + override fun exists(): Boolean = value.get() != null + override fun remove() { + value.set(null) + } + + override fun set(value: O) { + this.value.set(value) + } +} \ No newline at end of file diff --git a/shared/src/main/java/org/kingdoms/utils/internal/reference/ReferenceContainers.kt b/shared/src/main/java/org/kingdoms/utils/internal/reference/ReferenceContainers.kt new file mode 100644 index 000000000..3869e9775 --- /dev/null +++ b/shared/src/main/java/org/kingdoms/utils/internal/reference/ReferenceContainers.kt @@ -0,0 +1,28 @@ +package org.kingdoms.utils.internal.reference + +private class MapEntry( + override val key: K, + override val value: V +) : Map.Entry + +class ReferencedMap>(val map: MutableMap) : Map { + override val entries: Set> + get() = map.entries.filter { it.value.exists() }.mapTo(hashSetOf()) { MapEntry(it.key, it.value.get()) } + override val keys: Set + get() = map.entries.filter { it.value.exists() }.mapTo(hashSetOf()) { it.key } + override val values: Collection + get() = map.entries.filter { it.value.exists() }.mapTo(hashSetOf()) { it.value.get() } + override val size: Int + get() = map.entries.filter { it.value.exists() }.size + + @Suppress("ReplaceSizeZeroCheckWithIsEmpty") + override fun isEmpty(): Boolean = size == 0 + + override fun get(key: K): V? { + val value = (map[key] ?: return null) + return if (value.exists()) value.get() else null + } + + override fun containsValue(value: V): Boolean = values.contains(value) + override fun containsKey(key: K): Boolean = keys.contains(key) +} \ No newline at end of file diff --git a/shared/src/main/java/org/kingdoms/utils/internal/string/ObjectPrettyStringFactory.kt b/shared/src/main/java/org/kingdoms/utils/internal/string/ObjectPrettyStringFactory.kt index fb16605cb..9538a358c 100644 --- a/shared/src/main/java/org/kingdoms/utils/internal/string/ObjectPrettyStringFactory.kt +++ b/shared/src/main/java/org/kingdoms/utils/internal/string/ObjectPrettyStringFactory.kt @@ -1,7 +1,7 @@ package org.kingdoms.utils.internal.string import org.kingdoms.constants.namespace.Namespace -import org.kingdoms.utils.internal.Fn +import org.kingdoms.utils.internal.functional.Fn import java.util.* fun interface PrettyString { diff --git a/shared/src/main/java/org/kingdoms/utils/internal/string/QuantumString.java b/shared/src/main/java/org/kingdoms/utils/internal/string/QuantumString.java new file mode 100644 index 000000000..491bcf507 --- /dev/null +++ b/shared/src/main/java/org/kingdoms/utils/internal/string/QuantumString.java @@ -0,0 +1,111 @@ +package org.kingdoms.utils.internal.string; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.jetbrains.annotations.NotNull; + +import java.util.Locale; +import java.util.Objects; +import java.util.stream.IntStream; + +public class QuantumString implements CharSequence, Cloneable { + private final @Nullable String original; + private final @NonNull String quantumValue; + + public static QuantumString of(String original) { + return new QuantumString(original, true); + } + + /** + * @param original the string to wrap. + * @param quantum if this wrapper should be case-insensitive, otherwise case-sensitive. + */ + public QuantumString(@NonNull String original, boolean quantum) { + Objects.requireNonNull(original, "Quantum original string cannot be null"); + this.original = quantum ? original : null; + + // The local doesn't matter as long as it's consistent. + this.quantumValue = quantum ? original.toLowerCase(Locale.ENGLISH) : original; + } + + public static QuantumString empty() { + return new QuantumString("", false); + } + + public boolean isQuantum() { + return original != null; + } + + @Override + public int hashCode() { + return quantumValue.hashCode(); + } + + @Override + public boolean equals(Object obj) { + return this == obj || + (obj instanceof QuantumString && this.quantumValue.equals(((QuantumString) obj).quantumValue)); + } + + @Override + public String toString() { + return "QuantumString:[quantum= " + isQuantum() + ", original=" + original + ", quantumValue=" + quantumValue + ']'; + } + + @Override + public int length() { + return quantumValue.length(); + } + + @SuppressWarnings("all") + public boolean isEmpty() { + return quantumValue.isEmpty(); + } + + @Override + public char charAt(int index) { + return getQuantum().charAt(index); + } + + @NotNull + @Override + public CharSequence subSequence(int start, int end) { + return getQuantum().subSequence(start, end); + } + + @NotNull + @Override + public IntStream chars() { + return getQuantum().chars(); + } + + @NotNull + @Override + public IntStream codePoints() { + return getQuantum().codePoints(); + } + + @Override + public Object clone() { + try { + return super.clone(); + } catch (CloneNotSupportedException e) { + throw new AssertionError(); + } + } + + @Nullable + public String getOriginal() { + return original; + } + + @NonNull + public String getQuantumValue() { + return quantumValue; + } + + @NonNull + public String getQuantum() { + return isQuantum() ? original : quantumValue; + } +} diff --git a/shared/src/main/java/org/kingdoms/utils/internal/string/StringMatcher.java b/shared/src/main/java/org/kingdoms/utils/internal/string/StringMatcher.java new file mode 100644 index 000000000..ad2ba91df --- /dev/null +++ b/shared/src/main/java/org/kingdoms/utils/internal/string/StringMatcher.java @@ -0,0 +1,109 @@ +package org.kingdoms.utils.internal.string; + +import java.util.Collection; +import java.util.Objects; +import java.util.regex.Pattern; + +/** + * Different ways to filter strings. A matcher doesn't need to necessarily search + * and check the entire string to match, it can only check certain regions as well. + *

+ * The main purpose of this class is to configurable values easier to manage and also + * avoid the performance penalty of RegEx. + */ +public interface StringMatcher { + boolean matches(String string); + + final class Exact implements StringMatcher { + private final String exact; + + public Exact(String exact) {this.exact = exact;} + + @Override + public boolean matches(String string) { + return exact.equals(string); + } + } + + final class Contains implements StringMatcher { + private final String contains; + + public Contains(String contains) {this.contains = contains;} + + @Override + public boolean matches(String string) { + return string.contains(contains); + } + } + + final class EndsWith implements StringMatcher { + private final String endsWith; + + public EndsWith(String endsWith) {this.endsWith = endsWith;} + + @Override + public boolean matches(String string) { + return string.endsWith(endsWith); + } + } + + final class StartsWith implements StringMatcher { + private final String startsWith; + + public StartsWith(String startsWith) {this.startsWith = startsWith;} + + @Override + public boolean matches(String string) { + return string.endsWith(startsWith); + } + } + + final class Regex implements StringMatcher { + private final Pattern pattern; + + public Regex(Pattern pattern) {this.pattern = pattern;} + + @Override + public boolean matches(String string) { + return pattern.matcher(string).matches(); + } + } + + final class Aggregate implements StringMatcher { + private final StringMatcher[] matchers; + + public Aggregate(StringMatcher[] matchers) {this.matchers = matchers;} + + @Override + public boolean matches(String string) { + for (StringMatcher matcher : matchers) { + if (matcher.matches(string)) return true; + } + return false; + } + } + + static StringMatcher group(Collection matchers) { + return new Aggregate(matchers.toArray(new StringMatcher[0])); + } + + static StringMatcher fromString(String text) { + Objects.requireNonNull(text, "Cannot construct checker from null text"); + int handlerIndexEnd = text.indexOf(':'); + if (handlerIndexEnd == -1) return new Exact(text); + + String handlerName = text.substring(0, handlerIndexEnd); + String realText = text.substring(handlerIndexEnd + 1); + + // @formatter:off + switch (handlerName) { + case "CONTAINS": return new Contains (realText); + case "STARTS" : return new StartsWith(realText); + case "ENDS" : return new EndsWith (realText); + case "REGEX" : return new Regex (Pattern.compile(realText)); + } + // @formatter:on + + return new Exact(text); + } +} diff --git a/shared/src/main/java/org/kingdoms/utils/internal/tracker/TrackedCollection.kt b/shared/src/main/java/org/kingdoms/utils/internal/tracker/TrackedCollection.kt new file mode 100644 index 000000000..5c916f384 --- /dev/null +++ b/shared/src/main/java/org/kingdoms/utils/internal/tracker/TrackedCollection.kt @@ -0,0 +1,85 @@ +package org.kingdoms.utils.internal.tracker + +import java.util.function.Function + +class TrackedSet( + val original: MutableSet, + val onAdd: Function, + val onRemove: Function, +) : MutableSet by original { + override fun addAll(elements: Collection): Boolean { + var changed = false + for (element in elements) { + if (original.add(element)) { + onAdd.apply(element) + changed = true + } + } + return changed + } + + override fun clear() { + for (element in original) onRemove.apply(element) + original.clear() + } + + override fun iterator() = TrackedIterator(original.iterator(), onRemove) + + override fun add(element: K): Boolean { + if (original.add(element)) { + onAdd.apply(element) + return true + } + return false + } + + override fun remove(element: K): Boolean { + if (original.remove(element)) { + onRemove.apply(element) + return true + } + return false + } + + override fun removeAll(elements: Collection): Boolean { + var changed = false + for (element in elements) { + if (original.remove(element)) { + onRemove.apply(element) + changed = true + } + } + return changed + } + + override fun retainAll(elements: Collection): Boolean { + var changed = false + for (element in original) { + if (!elements.contains(element)) { + onRemove.apply(element) + changed = true + } + } + return changed + } +} + +class TrackedIterator( + val original: MutableIterator, + val onRemove: Function, +) : + MutableIterator by original { + var next: K? = null + + override fun remove() { + if (next == null) original.remove() + if (!onRemove.apply(next!!)) return + original.remove() + } + + override fun next(): K { + val n = original.next() + this.next = n + return n + } +} \ No newline at end of file diff --git a/shared/src/main/java/org/kingdoms/utils/internal/tracker/TrackedList.kt b/shared/src/main/java/org/kingdoms/utils/internal/tracker/TrackedList.kt new file mode 100644 index 000000000..49849752b --- /dev/null +++ b/shared/src/main/java/org/kingdoms/utils/internal/tracker/TrackedList.kt @@ -0,0 +1,89 @@ +package org.kingdoms.utils.internal.tracker + +import java.util.function.Function + +class TrackedCollection( + val original: MutableCollection, + val onAdd: Function, + val onRemove: Function, +) : MutableCollection by original { + override fun addAll(elements: Collection): Boolean { + var changed = false + for (element in elements) { + if (original.add(element)) { + onAdd.apply(element) + changed = true + } + } + return changed + } + + override fun clear() { + for (element in original) onRemove.apply(element) + original.clear() + } + + override fun iterator() = TrackedCollectionIterator(original.iterator(), onRemove) + + override fun add(element: K): Boolean { + if (original.add(element)) { + onAdd.apply(element) + return true + } + return false + } + + override fun remove(element: K): Boolean { + if (original.remove(element)) { + onRemove.apply(element) + return true + } + return false + } + + override fun removeAll(elements: Collection): Boolean { + var changed = false + for (element in elements) { + if (original.remove(element)) { + onRemove.apply(element) + changed = true + } + } + return changed + } + + override fun retainAll(elements: Collection): Boolean { + var changed = false + for (element in original) { + if (!elements.contains(element)) { + onRemove.apply(element) + changed = true + } + } + return changed + } + + override fun toString(): String { + return "${javaClass.simpleName}($original)" + } +} + +class TrackedCollectionIterator( + val original: MutableIterator, + val onRemove: Function, +) : + MutableIterator by original { + var next: K? = null + + override fun remove() { + if (next == null) original.remove() + if (!onRemove.apply(next!!)) return + original.remove() + } + + override fun next(): K { + val n = original.next() + this.next = n + return n + } +} \ No newline at end of file diff --git a/shared/src/main/java/org/kingdoms/utils/internal/tracker/TrackedMap.kt b/shared/src/main/java/org/kingdoms/utils/internal/tracker/TrackedMap.kt new file mode 100644 index 000000000..98ce9ec58 --- /dev/null +++ b/shared/src/main/java/org/kingdoms/utils/internal/tracker/TrackedMap.kt @@ -0,0 +1,100 @@ +package org.kingdoms.utils.internal.tracker + +import java.util.function.BiConsumer +import java.util.function.Function + +class TrackedMap( + val original: MutableMap, + val onAdd: BiConsumer, + val onRemove: BiConsumer, +) : MutableMap by original { + class BackedMap(private val original: MutableMap, private val adder: Boolean) : + BiConsumer { + override fun accept(t: K, u: V) { + if (adder) original[t] = u + else original.remove(t) + } + } + + companion object { + @JvmStatic + fun backedBy(current: MutableMap, original: MutableMap): MutableMap { + // public static Map ofSelf(Map current, Map original) { + // return new TrackedMap<>(current, new BackedMap<>(original, true), new BackedMap<>(original, false)); + // } + return TrackedMap(current, BackedMap(original, true), BackedMap(original, false)) + } + } + + override val entries: MutableSet> + get() = TrackedSet(original.entries, + onAdd = Function, Boolean> { entry -> + this.onAdd.accept(entry.key, entry.value) + return@Function true + }, + onRemove = Function, Boolean> { (key, value) -> + this.onRemove.accept(key, value) + return@Function true + } + ) + + override val keys: MutableSet + get() = TrackedSet(original.keys, + { _ -> throw UnsupportedOperationException() }, + { _ -> throw UnsupportedOperationException() } + ) + + + override val values: MutableCollection + get() = TrackedCollection(original.values, + onAdd = { _ -> throw UnsupportedOperationException() }, + onRemove = { _ -> throw UnsupportedOperationException() } + ) + + override fun clear() { + original.entries.forEach { (key, value) -> onRemove.accept(key, value) } + original.clear() + } + + override fun remove(key: K): V? { + val removed = original.remove(key) + if (removed != null) onRemove.accept(key, removed) + return removed + } + + override fun putAll(from: Map) { + for (entry in from) { + original[entry.key] = entry.value + onAdd.accept(entry.key, entry.value) + } + } + + override fun put(key: K, value: V): V? { + onAdd.accept(key, value) + return original.put(key, value) + } + + override fun toString(): String { + return "${javaClass.simpleName}($original)" + } +} + +class TrackedMapIterator( + val original: MutableIterator, + val onRemove: Function, +) : + MutableIterator by original { + var next: K? = null + + override fun remove() { + if (next == null) original.remove() + if (!onRemove.apply(next!!)) return + original.remove() + } + + override fun next(): K { + val n = original.next() + this.next = n + return n + } +} \ No newline at end of file diff --git a/shared/src/main/java/org/kingdoms/utils/internal/uuid/FastUUID.java b/shared/src/main/java/org/kingdoms/utils/internal/uuid/FastUUID.java new file mode 100644 index 000000000..420f53151 --- /dev/null +++ b/shared/src/main/java/org/kingdoms/utils/internal/uuid/FastUUID.java @@ -0,0 +1,272 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2018 Jon Chambers + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package org.kingdoms.utils.internal.uuid; + +import java.util.Arrays; +import java.util.Random; +import java.util.UUID; + +/** + * A utility class for quickly and efficiently parsing {@link java.util.UUID} instances from strings and writing UUID + * instances as strings. The methods contained in this class are optimized for speed and to minimize garbage collection + * pressure. In benchmarks, {@link #fromString(CharSequence)} is a little more than 14 times faster than + * {@link UUID#fromString(String)}, and {@link #toString(UUID)} is a little more than six times faster than + * {@link UUID#toString()} when compared to the implementations in Java 8 and older. Under Java 9 and newer, + * {@link #fromString(CharSequence)} is about six times faster than the JDK implementation and {@link #toString(UUID)} + * does not offer any performance enhancements (or regressions!) the {@link UUID#toString()} will be faster. + *

+ * Modified version by Crypto Morin + * + * @author Jon Chambers + * @version 2020.30 + */ +public final class FastUUID { + /** + * OpenJDK 14 and newer use a fancy native approach to converting UUIDs to strings and we're better off using + * that if it's available. + *

+ * Java 11+ use Long.fastUUID which for some reasons is faster. + */ + private static final boolean USE_JDK_UUID_TO_STRING; + private static final int UUID_STRING_LENGTH = 36; + private static final char[] HEX_DIGITS = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'}; + private static final long[] NIBBLES = new long[128]; + public static final UUID ZERO = new UUID(0, 0); + + static { + String java = System.getProperty("java.specification.version"); + int version; + try { + version = Integer.parseInt(java); + } catch (NumberFormatException ex) { + version = 0; + } + USE_JDK_UUID_TO_STRING = version >= 11; + } + + static { + Arrays.fill(NIBBLES, -1); + + NIBBLES['0'] = 0x0; + NIBBLES['1'] = 0x1; + NIBBLES['2'] = 0x2; + NIBBLES['3'] = 0x3; + NIBBLES['4'] = 0x4; + NIBBLES['5'] = 0x5; + NIBBLES['6'] = 0x6; + NIBBLES['7'] = 0x7; + NIBBLES['8'] = 0x8; + NIBBLES['9'] = 0x9; + + NIBBLES['a'] = 0xa; + NIBBLES['b'] = 0xb; + NIBBLES['c'] = 0xc; + NIBBLES['d'] = 0xd; + NIBBLES['e'] = 0xe; + NIBBLES['f'] = 0xf; + + NIBBLES['A'] = 0xa; + NIBBLES['B'] = 0xb; + NIBBLES['C'] = 0xc; + NIBBLES['D'] = 0xd; + NIBBLES['E'] = 0xe; + NIBBLES['F'] = 0xf; + } + + private FastUUID() { + } + + /** + * Static factory to retrieve a type 4 (pseudo randomly generated) UUID. + * Copied from JDK15 + * + * @param random the randomizer to use for generating the UUID. + * @return A randomly generated {@code UUID} + */ + public static UUID randomUUID(Random random) { + byte[] randomBytes = new byte[16]; + random.nextBytes(randomBytes); + + randomBytes[6] &= 0x0f; /* clear version */ + randomBytes[6] |= 0x40; /* set to version 4 */ + randomBytes[8] &= 0x3f; /* clear variant */ + randomBytes[8] |= 0x80; /* set to IETF variant */ + return bytesToUUID(randomBytes); + } + + /** + * Copied from JDK15 {@code UUID} private constructor + * Anding ({@literal &}) an integer with 0xFF leaves only the least significant byte, aka masking + *

+ * least significant bit (LSB): Example for a 32-bit int + * 011101010101010101010101010110110 + * ^^^^^^^^ + *

+ * 0xff = 255 = 1 1 1 1 1 1 1 1 + *

+ * 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 + * {@literal &} 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 + * ------------------------------- + * 0 0 0 0 0 0 0 0 0 1 0 1 0 1 0 1 + * {@literal ^} + * + * @param data a 16 long byte array to construct a {@link UUID}. + * @return a {@link UUID} created by the bytes. + */ + public static UUID bytesToUUID(byte[] data) { + long msb = 0, lsb = 0; + for (int i = 0; i < 8; i++) msb = (msb << 8) | (data[i] & 0xff); + for (int i = 8; i < 16; i++) lsb = (lsb << 8) | (data[i] & 0xff); + return new UUID(msb, lsb); + } + + /** + * Avoids the null and {@link #getClass()} check of the standard + * {@link UUID#equals(Object)} method for known objects. + *

+ * Hopefully JVM will inline the method calls. + */ + public static boolean equals(UUID first, UUID other) { + return first.getMostSignificantBits() == other.getMostSignificantBits() && + first.getLeastSignificantBits() == other.getLeastSignificantBits(); + } + + /** + * Parses a UUID from the given character sequence. The character sequence must represent a UUID as described in + * {@link UUID#toString()}. + * + * @param uuid the character sequence from which to parse a UUID. + * @return the UUID represented by the given character sequence. + * @throws IllegalArgumentException if the given character sequence does not conform to the string representation as + * described in {@link UUID#toString()} + */ + public static UUID fromString(CharSequence uuid) { + try { + long mostSignificantBits = (((((((((((((((getHexValueForChar(uuid.charAt(0)) << 60) + | getHexValueForChar(uuid.charAt(1)) << 56) + | getHexValueForChar(uuid.charAt(2)) << 52) + | getHexValueForChar(uuid.charAt(3)) << 48) + | getHexValueForChar(uuid.charAt(4)) << 44) + | getHexValueForChar(uuid.charAt(5)) << 40) + | getHexValueForChar(uuid.charAt(6)) << 36) + | getHexValueForChar(uuid.charAt(7)) << 32) + + | getHexValueForChar(uuid.charAt(9)) << 28) + | getHexValueForChar(uuid.charAt(10)) << 24) + | getHexValueForChar(uuid.charAt(11)) << 20) + | getHexValueForChar(uuid.charAt(12)) << 16) + + | getHexValueForChar(uuid.charAt(14)) << 12) + | getHexValueForChar(uuid.charAt(15)) << 8) + | getHexValueForChar(uuid.charAt(16)) << 4) + | getHexValueForChar(uuid.charAt(17)); + + + long leastSignificantBits = (((((((((((((((getHexValueForChar(uuid.charAt(19)) << 60) + | getHexValueForChar(uuid.charAt(20)) << 56) + | getHexValueForChar(uuid.charAt(21)) << 52) + | getHexValueForChar(uuid.charAt(22)) << 48) + | getHexValueForChar(uuid.charAt(24)) << 44) + | getHexValueForChar(uuid.charAt(25)) << 40) + | getHexValueForChar(uuid.charAt(26)) << 36) + | getHexValueForChar(uuid.charAt(27)) << 32) + | getHexValueForChar(uuid.charAt(28)) << 28) + | getHexValueForChar(uuid.charAt(29)) << 24) + | getHexValueForChar(uuid.charAt(30)) << 20) + | getHexValueForChar(uuid.charAt(31)) << 16) + | getHexValueForChar(uuid.charAt(32)) << 12) + | getHexValueForChar(uuid.charAt(33)) << 8) + | getHexValueForChar(uuid.charAt(34)) << 4) + | getHexValueForChar(uuid.charAt(35)); + + return new UUID(mostSignificantBits, leastSignificantBits); + } catch (Throwable throwable) { + throw new MalformedUUIDException(uuid, throwable); + } + } + + /** + * Returns a string representation of the given UUID. The returned string is formatted as described in + * {@link UUID#toString()}. + * + * @param uuid the UUID to represent as a string + * @return a string representation of the given UUID + */ + public static String toString(UUID uuid) { + if (USE_JDK_UUID_TO_STRING) return uuid.toString(); + + long mostSignificantBits = uuid.getMostSignificantBits(); + long leastSignificantBits = uuid.getLeastSignificantBits(); + char[] uuidChars = new char[UUID_STRING_LENGTH]; + + uuidChars[0] = HEX_DIGITS[(int) ((mostSignificantBits & 0xf000000000000000L) >>> 60)]; + uuidChars[1] = HEX_DIGITS[(int) ((mostSignificantBits & 0x0f00000000000000L) >>> 56)]; + uuidChars[2] = HEX_DIGITS[(int) ((mostSignificantBits & 0x00f0000000000000L) >>> 52)]; + uuidChars[3] = HEX_DIGITS[(int) ((mostSignificantBits & 0x000f000000000000L) >>> 48)]; + uuidChars[4] = HEX_DIGITS[(int) ((mostSignificantBits & 0x0000f00000000000L) >>> 44)]; + uuidChars[5] = HEX_DIGITS[(int) ((mostSignificantBits & 0x00000f0000000000L) >>> 40)]; + uuidChars[6] = HEX_DIGITS[(int) ((mostSignificantBits & 0x000000f000000000L) >>> 36)]; + uuidChars[7] = HEX_DIGITS[(int) ((mostSignificantBits & 0x0000000f00000000L) >>> 32)]; + uuidChars[8] = '-'; + uuidChars[9] = HEX_DIGITS[(int) ((mostSignificantBits & 0x00000000f0000000L) >>> 28)]; + uuidChars[10] = HEX_DIGITS[(int) ((mostSignificantBits & 0x000000000f000000L) >>> 24)]; + uuidChars[11] = HEX_DIGITS[(int) ((mostSignificantBits & 0x0000000000f00000L) >>> 20)]; + uuidChars[12] = HEX_DIGITS[(int) ((mostSignificantBits & 0x00000000000f0000L) >>> 16)]; + uuidChars[13] = '-'; + uuidChars[14] = HEX_DIGITS[(int) ((mostSignificantBits & 0x000000000000f000L) >>> 12)]; + uuidChars[15] = HEX_DIGITS[(int) ((mostSignificantBits & 0x0000000000000f00L) >>> 8)]; + uuidChars[16] = HEX_DIGITS[(int) ((mostSignificantBits & 0x00000000000000f0L) >>> 4)]; + uuidChars[17] = HEX_DIGITS[(int) (mostSignificantBits & 0x000000000000000fL)]; + uuidChars[18] = '-'; + uuidChars[19] = HEX_DIGITS[(int) ((leastSignificantBits & 0xf000000000000000L) >>> 60)]; + uuidChars[20] = HEX_DIGITS[(int) ((leastSignificantBits & 0x0f00000000000000L) >>> 56)]; + uuidChars[21] = HEX_DIGITS[(int) ((leastSignificantBits & 0x00f0000000000000L) >>> 52)]; + uuidChars[22] = HEX_DIGITS[(int) ((leastSignificantBits & 0x000f000000000000L) >>> 48)]; + uuidChars[23] = '-'; + uuidChars[24] = HEX_DIGITS[(int) ((leastSignificantBits & 0x0000f00000000000L) >>> 44)]; + uuidChars[25] = HEX_DIGITS[(int) ((leastSignificantBits & 0x00000f0000000000L) >>> 40)]; + uuidChars[26] = HEX_DIGITS[(int) ((leastSignificantBits & 0x000000f000000000L) >>> 36)]; + uuidChars[27] = HEX_DIGITS[(int) ((leastSignificantBits & 0x0000000f00000000L) >>> 32)]; + uuidChars[28] = HEX_DIGITS[(int) ((leastSignificantBits & 0x00000000f0000000L) >>> 28)]; + uuidChars[29] = HEX_DIGITS[(int) ((leastSignificantBits & 0x000000000f000000L) >>> 24)]; + uuidChars[30] = HEX_DIGITS[(int) ((leastSignificantBits & 0x0000000000f00000L) >>> 20)]; + uuidChars[31] = HEX_DIGITS[(int) ((leastSignificantBits & 0x00000000000f0000L) >>> 16)]; + uuidChars[32] = HEX_DIGITS[(int) ((leastSignificantBits & 0x000000000000f000L) >>> 12)]; + uuidChars[33] = HEX_DIGITS[(int) ((leastSignificantBits & 0x0000000000000f00L) >>> 8)]; + uuidChars[34] = HEX_DIGITS[(int) ((leastSignificantBits & 0x00000000000000f0L) >>> 4)]; + uuidChars[35] = HEX_DIGITS[(int) (leastSignificantBits & 0x000000000000000fL)]; + + return new String(uuidChars); + } + + private static long getHexValueForChar(char ch) { + try { + long hex = NIBBLES[ch]; + if (hex == -1) throw new IllegalArgumentException("Illegal hexadecimal digit: " + ch); + return hex; + } catch (ArrayIndexOutOfBoundsException ex) { + throw new IllegalArgumentException("Illegal hexadecimal digit: " + ch); + } + } +} diff --git a/shared/src/main/java/org/kingdoms/utils/internal/uuid/MalformedUUIDException.kt b/shared/src/main/java/org/kingdoms/utils/internal/uuid/MalformedUUIDException.kt new file mode 100644 index 000000000..a7da912aa --- /dev/null +++ b/shared/src/main/java/org/kingdoms/utils/internal/uuid/MalformedUUIDException.kt @@ -0,0 +1,4 @@ +package org.kingdoms.utils.internal.uuid + +class MalformedUUIDException(val uuid: CharSequence, throwable: Throwable) : + IllegalArgumentException("'$uuid'", throwable) \ No newline at end of file diff --git a/shared/src/main/java/org/kingdoms/utils/time/stopwatch/AbstractStopwatch.kt b/shared/src/main/java/org/kingdoms/utils/time/stopwatch/AbstractStopwatch.kt index 4768cfc0c..2e0bd27f5 100644 --- a/shared/src/main/java/org/kingdoms/utils/time/stopwatch/AbstractStopwatch.kt +++ b/shared/src/main/java/org/kingdoms/utils/time/stopwatch/AbstractStopwatch.kt @@ -16,17 +16,24 @@ abstract class AbstractStopwatch(var passed: Long = 0) : Stopwatch { abstract fun getCurrentTime(): Long final override fun start(): AbstractStopwatch { - checkStopped() + ensureRunning() resume() return this } final override fun stop(): AbstractStopwatch { - checkStopped() + ensureRunning() state = StopwatchState.STOPPED return this } + override fun reset(): Stopwatch { + ensureRunning() + passed = 0 + lastCheckedTicks = getCurrentTime() + return this + } + private fun updateTicks() { if (state == StopwatchState.NOT_STARTED) return @@ -36,13 +43,13 @@ abstract class AbstractStopwatch(var passed: Long = 0) : Stopwatch { this.lastCheckedTicks = currTicks } - private fun checkStopped() { + private fun ensureRunning() { if (state == StopwatchState.STOPPED) throw IllegalStateException("Counter has stopped") updateTicks() } final override fun resume(): AbstractStopwatch { - checkStopped() + ensureRunning() if (state == StopwatchState.TICKING) throw IllegalStateException("Already ticking") state = StopwatchState.TICKING this.lastCheckedTicks = getCurrentTime() @@ -52,7 +59,7 @@ abstract class AbstractStopwatch(var passed: Long = 0) : Stopwatch { final override fun getState(): StopwatchState = state final override fun pause(): AbstractStopwatch { - checkStopped() + ensureRunning() if (state == StopwatchState.PAUSED) throw IllegalStateException("Already paused") state = StopwatchState.PAUSED updateTicks() diff --git a/shared/src/main/java/org/kingdoms/utils/time/stopwatch/Stopwatch.kt b/shared/src/main/java/org/kingdoms/utils/time/stopwatch/Stopwatch.kt index b4807fc91..ed6871165 100644 --- a/shared/src/main/java/org/kingdoms/utils/time/stopwatch/Stopwatch.kt +++ b/shared/src/main/java/org/kingdoms/utils/time/stopwatch/Stopwatch.kt @@ -8,16 +8,49 @@ enum class StopwatchState { fun hasStarted() = this != NOT_STARTED } +/** + * A simple timer to keep track of the amount of time that has passed. + */ interface Stopwatch { fun getState(): StopwatchState + + /** + * The amount of time passed. + */ fun elapsed(): Duration + + /** + * Sets [elapsed] to zero. + */ + fun reset(): Stopwatch + + /** + * Start the timer. + * @throws IllegalStateException if the timer is already started. + */ fun start(): Stopwatch + + /** + * Stop the timer so it cannot be used anymore. + * @throws IllegalStateException if the timer is already stopped. + */ fun stop(): Stopwatch + + /** + * Resumes the timer after it was paused with [pause]. + * @throws IllegalStateException if the timer is not paused or started at all. + */ fun resume(): Stopwatch + + /** + * Pauses the timer to be resumed again with [resume] + * @throws IllegalStateException if the timer is not started yet or is stopped. + */ fun pause(): Stopwatch companion object { - @JvmStatic fun withTickAccuracy(passedTicks: Int = 0): Stopwatch = TickStopwatch(passedTicks) - @JvmStatic fun withMillisAccuracy(passed: Duration = Duration.ZERO): Stopwatch = MillisStopwatch(passed) + @JvmStatic @JvmOverloads fun withTickAccuracy(passedTicks: Int = 0): Stopwatch = TickStopwatch(passedTicks) + @JvmStatic @JvmOverloads fun withMillisAccuracy(passed: Duration = Duration.ZERO): Stopwatch = + MillisStopwatch(passed) } } \ No newline at end of file