Search in sources :

Example 1 with LootTable

use of org.bukkit.loot.LootTable in project Denizen-For-Bukkit by DenizenScript.

the class ServerTagBase method serverTag.

public void serverTag(ReplaceableTagEvent event) {
    if (!event.matches("server", "global") || event.replaced()) {
        return;
    }
    if (event.matches("global")) {
        Deprecations.globalTagName.warn(event.getScriptEntry());
    }
    Attribute attribute = event.getAttributes().fulfill(1);
    if (attribute.startsWith("economy")) {
        if (Depends.economy == null) {
            attribute.echoError("No economy loaded! Have you installed Vault and a compatible economy plugin?");
            return;
        }
        attribute = attribute.fulfill(1);
        // -->
        if (attribute.startsWith("format") && attribute.hasParam()) {
            double amount = attribute.getDoubleParam();
            event.setReplacedObject(new ElementTag(Depends.economy.format(amount)).getObjectAttribute(attribute.fulfill(1)));
            return;
        }
        // -->
        if (attribute.startsWith("currency_name") && attribute.hasParam()) {
            double amount = attribute.getDoubleParam();
            event.setReplacedObject(new ElementTag(amount == 1 ? Depends.economy.currencyNameSingular() : Depends.economy.currencyNamePlural()).getObjectAttribute(attribute.fulfill(1)));
            return;
        }
        // -->
        if (attribute.startsWith("currency_plural")) {
            event.setReplacedObject(new ElementTag(Depends.economy.currencyNamePlural()).getObjectAttribute(attribute.fulfill(1)));
            return;
        }
        // -->
        if (attribute.startsWith("currency_singular")) {
            event.setReplacedObject(new ElementTag(Depends.economy.currencyNameSingular()).getObjectAttribute(attribute.fulfill(1)));
            return;
        }
        return;
    }
    // -->
    if (attribute.startsWith("slot_id") && attribute.hasParam()) {
        int slotId = SlotHelper.nameToIndex(attribute.getParam(), null);
        if (slotId != -1) {
            event.setReplacedObject(new ElementTag(slotId).getObjectAttribute(attribute.fulfill(1)));
        }
        return;
    }
    // -->
    if (attribute.startsWith("parse_bukkit_item") && attribute.hasParam()) {
        YamlConfiguration config = new YamlConfiguration();
        try {
            config.loadFromString(attribute.getParam());
            ItemStack item = config.getItemStack("item");
            if (item != null) {
                event.setReplacedObject(new ItemTag(item).getObjectAttribute(attribute.fulfill(1)));
            }
        } catch (Exception ex) {
            Debug.echoError(ex);
        }
        return;
    }
    // -->
    if (attribute.startsWith("recipe_ids") || attribute.startsWith("list_recipe_ids")) {
        listDeprecateWarn(attribute);
        String type = attribute.hasParam() ? CoreUtilities.toLowerCase(attribute.getParam()) : null;
        ListTag list = new ListTag();
        Iterator<Recipe> recipeIterator = Bukkit.recipeIterator();
        while (recipeIterator.hasNext()) {
            Recipe recipe = recipeIterator.next();
            if (Utilities.isRecipeOfType(recipe, type) && recipe instanceof Keyed) {
                list.add(((Keyed) recipe).getKey().toString());
            }
        }
        event.setReplacedObject(list.getObjectAttribute(attribute.fulfill(1)));
        return;
    }
    // -->
    if (attribute.startsWith("recipe_items") && attribute.hasParam()) {
        NamespacedKey key = Utilities.parseNamespacedKey(attribute.getParam());
        Recipe recipe = NMSHandler.getItemHelper().getRecipeById(key);
        if (recipe == null) {
            return;
        }
        ListTag result = new ListTag();
        Consumer<RecipeChoice> addChoice = (choice) -> {
            if (choice == null) {
                result.addObject(new ItemTag(Material.AIR));
            } else {
                if (choice instanceof RecipeChoice.ExactChoice) {
                    result.addObject(new ItemTag(choice.getItemStack()));
                } else {
                    result.add("material:" + choice.getItemStack().getType().name());
                }
            }
        };
        if (recipe instanceof ShapedRecipe) {
            for (String row : ((ShapedRecipe) recipe).getShape()) {
                for (char column : row.toCharArray()) {
                    addChoice.accept(((ShapedRecipe) recipe).getChoiceMap().get(column));
                }
            }
        } else if (recipe instanceof ShapelessRecipe) {
            for (RecipeChoice choice : ((ShapelessRecipe) recipe).getChoiceList()) {
                addChoice.accept(choice);
            }
        } else if (recipe instanceof CookingRecipe<?>) {
            addChoice.accept(((CookingRecipe) recipe).getInputChoice());
        }
        event.setReplacedObject(result.getObjectAttribute(attribute.fulfill(1)));
        return;
    }
    // -->
    if (attribute.startsWith("recipe_shape") && attribute.hasParam()) {
        NamespacedKey key = Utilities.parseNamespacedKey(attribute.getParam());
        Recipe recipe = NMSHandler.getItemHelper().getRecipeById(key);
        if (!(recipe instanceof ShapedRecipe)) {
            return;
        }
        String[] shape = ((ShapedRecipe) recipe).getShape();
        event.setReplacedObject(new ElementTag(shape[0].length() + "x" + shape.length).getObjectAttribute(attribute.fulfill(1)));
        return;
    }
    // -->
    if (attribute.startsWith("recipe_type") && attribute.hasParam()) {
        NamespacedKey key = Utilities.parseNamespacedKey(attribute.getParam());
        Recipe recipe = NMSHandler.getItemHelper().getRecipeById(key);
        if (recipe == null) {
            return;
        }
        event.setReplacedObject(new ElementTag(Utilities.getRecipeType(recipe)).getObjectAttribute(attribute.fulfill(1)));
        return;
    }
    // -->
    if (attribute.startsWith("recipe_result") && attribute.hasParam()) {
        NamespacedKey key = Utilities.parseNamespacedKey(attribute.getParam());
        Recipe recipe = NMSHandler.getItemHelper().getRecipeById(key);
        if (recipe == null) {
            return;
        }
        event.setReplacedObject(new ItemTag(recipe.getResult()).getObjectAttribute(attribute.fulfill(1)));
        return;
    }
    // -->
    if (attribute.startsWith("scoreboards")) {
        ListTag result = new ListTag();
        for (String board : ScoreboardHelper.scoreboardMap.keySet()) {
            result.addObject(new ElementTag(board));
        }
        event.setReplacedObject(result.getObjectAttribute(attribute.fulfill(1)));
        return;
    }
    if (attribute.startsWith("scoreboard")) {
        Scoreboard board;
        String name = "main";
        if (attribute.hasParam()) {
            name = attribute.getParam();
            board = ScoreboardHelper.getScoreboard(name);
        } else {
            board = ScoreboardHelper.getMain();
        }
        attribute = attribute.fulfill(1);
        // -->
        if (attribute.startsWith("exists")) {
            event.setReplacedObject(new ElementTag(board != null).getObjectAttribute(attribute.fulfill(1)));
            return;
        }
        if (board == null) {
            attribute.echoError("Scoreboard '" + name + "' does not exist.");
            return;
        }
        // -->
        if (attribute.startsWith("objectives")) {
            ListTag list = new ListTag();
            for (Objective objective : board.getObjectives()) {
                list.add(objective.getName());
            }
            event.setReplacedObject((list).getObjectAttribute(attribute.fulfill(1)));
        }
        if (attribute.startsWith("objective") && attribute.hasParam()) {
            Objective objective = board.getObjective(attribute.getParam());
            if (objective == null) {
                attribute.echoError("Scoreboard objective '" + attribute.getParam() + "' does not exist.");
                return;
            }
            attribute = attribute.fulfill(1);
            // -->
            if (attribute.startsWith("criteria")) {
                event.setReplacedObject(new ElementTag(objective.getCriteria()).getObjectAttribute(attribute.fulfill(1)));
            }
            // -->
            if (attribute.startsWith("display_name")) {
                event.setReplacedObject(new ElementTag(objective.getDisplayName()).getObjectAttribute(attribute.fulfill(1)));
            }
            // -->
            if (attribute.startsWith("display_slot")) {
                if (objective.getDisplaySlot() == null) {
                    return;
                }
                event.setReplacedObject(new ElementTag(objective.getDisplaySlot().name()).getObjectAttribute(attribute.fulfill(1)));
            }
        }
        // -->
        if (attribute.startsWith("team_names")) {
            ListTag result = new ListTag();
            for (Team team : board.getTeams()) {
                result.add(team.getName());
            }
            event.setReplacedObject(result.getObjectAttribute(attribute.fulfill(1)));
        }
        if (attribute.startsWith("team") && attribute.hasParam()) {
            Team team = board.getTeam(attribute.getParam());
            if (team == null) {
                attribute.echoError("Scoreboard team '" + attribute.getParam() + "' does not exist.");
                return;
            }
            attribute = attribute.fulfill(1);
            // -->
            if (attribute.startsWith("members")) {
                event.setReplacedObject(new ListTag(team.getEntries()).getObjectAttribute(attribute.fulfill(1)));
            }
            return;
        }
    }
    // -->
    if (attribute.startsWith("object_is_valid")) {
        ObjectTag o = ObjectFetcher.pickObjectFor(attribute.getParam(), new BukkitTagContext(null, null, null, false, null));
        event.setReplacedObject(new ElementTag(!(o == null || o instanceof ElementTag)).getObjectAttribute(attribute.fulfill(1)));
        return;
    }
    // -->
    if (attribute.startsWith("has_whitelist")) {
        event.setReplacedObject(new ElementTag(Bukkit.hasWhitelist()).getObjectAttribute(attribute.fulfill(1)));
        return;
    }
    // -->
    if (attribute.startsWith("whitelisted_players")) {
        ListTag result = new ListTag();
        for (OfflinePlayer player : Bukkit.getWhitelistedPlayers()) {
            result.addObject(new PlayerTag(player));
        }
        event.setReplacedObject(result.getObjectAttribute(attribute.fulfill(1)));
        return;
    }
    // -->
    if (attribute.startsWith("has_flag")) {
        event.setReplacedObject(DenizenCore.serverFlagMap.doHasFlagTag(attribute).getObjectAttribute(attribute.fulfill(1)));
        return;
    }
    // -->
    if (attribute.startsWith("flag_expiration")) {
        TimeTag exp = DenizenCore.serverFlagMap.doFlagExpirationTag(attribute);
        if (exp != null) {
            event.setReplacedObject(exp.getObjectAttribute(attribute.fulfill(1)));
        }
        return;
    }
    // -->
    if (attribute.startsWith("flag")) {
        ObjectTag flag = DenizenCore.serverFlagMap.doFlagTag(attribute);
        if (flag != null) {
            event.setReplacedObject(flag.getObjectAttribute(attribute.fulfill(1)));
        }
        return;
    }
    // -->
    if (attribute.startsWith("list_flags")) {
        event.setReplacedObject(DenizenCore.serverFlagMap.doListFlagsTag(attribute).getObjectAttribute(attribute.fulfill(1)));
        return;
    }
    // -->
    if (attribute.startsWith("flag_map")) {
        event.setReplacedObject(DenizenCore.serverFlagMap.doFlagMapTag(attribute).getObjectAttribute(attribute.fulfill(1)));
        return;
    }
    // -->
    if (attribute.startsWith("gamerules")) {
        ListTag allGameRules = new ListTag();
        for (GameRule rule : GameRule.values()) {
            allGameRules.add(rule.getName());
        }
        event.setReplacedObject(allGameRules.getObjectAttribute(attribute.fulfill(1)));
    }
    // -->
    if ((attribute.startsWith("traits") || attribute.startsWith("list_traits")) && Depends.citizens != null) {
        listDeprecateWarn(attribute);
        ListTag allTraits = new ListTag();
        for (TraitInfo trait : CitizensAPI.getTraitFactory().getRegisteredTraits()) {
            allTraits.add(trait.getTraitName());
        }
        event.setReplacedObject(allTraits.getObjectAttribute(attribute.fulfill(1)));
    }
    // -->
    if (attribute.startsWith("commands") || attribute.startsWith("list_commands")) {
        listDeprecateWarn(attribute);
        CommandScriptHelper.init();
        ListTag list = new ListTag(CommandScriptHelper.knownCommands.keySet());
        event.setReplacedObject(list.getObjectAttribute(attribute.fulfill(1)));
        return;
    }
    // -->
    if (attribute.startsWith("command_plugin")) {
        PluginCommand cmd = Bukkit.getPluginCommand(attribute.getParam());
        if (cmd != null) {
            event.setReplacedObject(new PluginTag(cmd.getPlugin()).getObjectAttribute(attribute.fulfill(1)));
        }
        return;
    }
    // -->
    if (attribute.startsWith("color_names")) {
        ListTag list = new ListTag();
        for (String color : ColorTag.colorsByName.keySet()) {
            list.add(color);
        }
        event.setReplacedObject(list.getObjectAttribute(attribute.fulfill(1)));
        return;
    }
    // -->
    if (attribute.startsWith("art_types")) {
        listDeprecateWarn(attribute);
        ListTag list = new ListTag();
        for (Art art : Art.values()) {
            list.add(art.name());
        }
        event.setReplacedObject(list.getObjectAttribute(attribute.fulfill(1)));
        return;
    }
    // -->
    if (attribute.startsWith("advancement_types") || attribute.startsWith("list_advancements")) {
        if (attribute.matches("list_advancements")) {
            Debug.echoError("list_advancements is deprecated: use advancement_types");
        }
        listDeprecateWarn(attribute);
        ListTag list = new ListTag();
        Bukkit.advancementIterator().forEachRemaining((adv) -> {
            list.add(adv.getKey().toString());
        });
        event.setReplacedObject(list.getObjectAttribute(attribute.fulfill(1)));
        return;
    }
    // -->
    if (attribute.startsWith("nbt_attribute_types") || attribute.startsWith("list_nbt_attribute_types")) {
        listDeprecateWarn(attribute);
        ListTag list = new ListTag();
        for (org.bukkit.attribute.Attribute attribType : org.bukkit.attribute.Attribute.values()) {
            list.add(attribType.name());
        }
        event.setReplacedObject(list.getObjectAttribute(attribute.fulfill(1)));
        return;
    }
    // -->
    if (attribute.startsWith("damage_causes") || attribute.startsWith("list_damage_causes")) {
        listDeprecateWarn(attribute);
        ListTag list = new ListTag();
        for (EntityDamageEvent.DamageCause damageCause : EntityDamageEvent.DamageCause.values()) {
            list.add(damageCause.name());
        }
        event.setReplacedObject(list.getObjectAttribute(attribute.fulfill(1)));
        return;
    }
    // -->
    if (attribute.startsWith("teleport_causes")) {
        ListTag list = new ListTag();
        for (PlayerTeleportEvent.TeleportCause teleportCause : PlayerTeleportEvent.TeleportCause.values()) {
            list.add(teleportCause.name());
        }
        event.setReplacedObject(list.getObjectAttribute(attribute.fulfill(1)));
        return;
    }
    // -->
    if (attribute.startsWith("biome_types") || attribute.startsWith("list_biome_types")) {
        listDeprecateWarn(attribute);
        ListTag allBiomes = new ListTag();
        for (Biome biome : Biome.values()) {
            if (biome != Biome.CUSTOM) {
                allBiomes.addObject(new BiomeTag(biome));
            }
        }
        event.setReplacedObject(allBiomes.getObjectAttribute(attribute.fulfill(1)));
    }
    if (attribute.startsWith("list_biomes")) {
        Deprecations.serverListBiomeNames.warn(attribute.context);
        ListTag allBiomes = new ListTag();
        for (Biome biome : Biome.values()) {
            allBiomes.add(biome.name());
        }
        event.setReplacedObject(allBiomes.getObjectAttribute(attribute.fulfill(1)));
    }
    // -->
    if (attribute.startsWith("enchantments")) {
        ListTag enchants = new ListTag();
        for (Enchantment ench : Enchantment.values()) {
            enchants.addObject(new EnchantmentTag(ench));
        }
        event.setReplacedObject(enchants.getObjectAttribute(attribute.fulfill(1)));
    }
    if (attribute.startsWith("enchantment_types") || attribute.startsWith("list_enchantments")) {
        Deprecations.echantmentTagUpdate.warn(attribute.context);
        if (attribute.matches("list_enchantments")) {
            Debug.echoError("list_enchantments is deprecated: use enchantment_types");
        }
        listDeprecateWarn(attribute);
        ListTag enchants = new ListTag();
        for (Enchantment e : Enchantment.values()) {
            enchants.add(e.getName());
        }
        event.setReplacedObject(enchants.getObjectAttribute(attribute.fulfill(1)));
    }
    if (attribute.startsWith("enchantment_keys") || attribute.startsWith("list_enchantment_keys")) {
        Deprecations.echantmentTagUpdate.warn(attribute.context);
        listDeprecateWarn(attribute);
        ListTag enchants = new ListTag();
        for (Enchantment e : Enchantment.values()) {
            enchants.add(e.getKey().getKey());
        }
        event.setReplacedObject(enchants.getObjectAttribute(attribute.fulfill(1)));
    }
    // -->
    if (attribute.startsWith("entity_types") || attribute.startsWith("list_entity_types")) {
        listDeprecateWarn(attribute);
        ListTag allEnt = new ListTag();
        for (EntityType entity : EntityType.values()) {
            if (entity != EntityType.UNKNOWN) {
                allEnt.add(entity.name());
            }
        }
        event.setReplacedObject(allEnt.getObjectAttribute(attribute.fulfill(1)));
    }
    // -->
    if (attribute.startsWith("material_types") || attribute.startsWith("list_material_types")) {
        listDeprecateWarn(attribute);
        ListTag allMats = new ListTag();
        for (Material mat : Material.values()) {
            allMats.addObject(new MaterialTag(mat));
        }
        event.setReplacedObject(allMats.getObjectAttribute(attribute.fulfill(1)));
    }
    if (attribute.startsWith("list_materials")) {
        Deprecations.serverListMaterialNames.warn(attribute.context);
        ListTag allMats = new ListTag();
        for (Material mat : Material.values()) {
            allMats.add(mat.name());
        }
        event.setReplacedObject(allMats.getObjectAttribute(attribute.fulfill(1)));
    }
    // -->
    if (attribute.startsWith("sound_types") || attribute.startsWith("list_sounds")) {
        if (attribute.matches("list_sounds")) {
            Debug.echoError("list_sounds is deprecated: use sound_types");
        }
        listDeprecateWarn(attribute);
        ListTag sounds = new ListTag();
        for (Sound s : Sound.values()) {
            sounds.add(s.toString());
        }
        event.setReplacedObject(sounds.getObjectAttribute(attribute.fulfill(1)));
    }
    // -->
    if (attribute.startsWith("particle_types") || attribute.startsWith("list_particles")) {
        if (attribute.matches("list_particles")) {
            Debug.echoError("list_particles is deprecated: use particle_types");
        }
        listDeprecateWarn(attribute);
        ListTag particleTypes = new ListTag();
        for (Particle particle : Particle.values()) {
            particleTypes.add(particle.toString());
        }
        event.setReplacedObject(particleTypes.getObjectAttribute(attribute.fulfill(1)));
    }
    // -->
    if (attribute.startsWith("effect_types") || attribute.startsWith("list_effects")) {
        if (attribute.matches("list_effects")) {
            Debug.echoError("list_effects is deprecated: use effect_types");
        }
        listDeprecateWarn(attribute);
        ListTag effectTypes = new ListTag();
        for (Effect effect : Effect.values()) {
            effectTypes.add(effect.toString());
        }
        event.setReplacedObject(effectTypes.getObjectAttribute(attribute.fulfill(1)));
    }
    // -->
    if (attribute.startsWith("pattern_types") || attribute.startsWith("list_patterns")) {
        if (attribute.matches("list_patterns")) {
            Debug.echoError("list_patterns is deprecated: use pattern_types");
        }
        listDeprecateWarn(attribute);
        ListTag allPatterns = new ListTag();
        for (PatternType pat : PatternType.values()) {
            allPatterns.add(pat.toString());
        }
        event.setReplacedObject(allPatterns.getObjectAttribute(attribute.fulfill(1)));
    }
    // -->
    if (attribute.startsWith("potion_effect_types") || attribute.startsWith("list_potion_effects")) {
        if (attribute.matches("list_potion_effects")) {
            Debug.echoError("list_potion_effects is deprecated: use potion_effect_types");
        }
        listDeprecateWarn(attribute);
        ListTag statuses = new ListTag();
        for (PotionEffectType effect : PotionEffectType.values()) {
            if (effect != null) {
                statuses.add(effect.getName());
            }
        }
        event.setReplacedObject(statuses.getObjectAttribute(attribute.fulfill(1)));
    }
    // -->
    if (attribute.startsWith("potion_types") || attribute.startsWith("list_potion_types")) {
        listDeprecateWarn(attribute);
        ListTag potionTypes = new ListTag();
        for (PotionType type : PotionType.values()) {
            potionTypes.add(type.toString());
        }
        event.setReplacedObject(potionTypes.getObjectAttribute(attribute.fulfill(1)));
    }
    // -->
    if (attribute.startsWith("tree_types") || attribute.startsWith("list_tree_types")) {
        listDeprecateWarn(attribute);
        ListTag allTrees = new ListTag();
        for (TreeType tree : TreeType.values()) {
            allTrees.add(tree.name());
        }
        event.setReplacedObject(allTrees.getObjectAttribute(attribute.fulfill(1)));
    }
    // -->
    if (attribute.startsWith("map_cursor_types") || attribute.startsWith("list_map_cursor_types")) {
        listDeprecateWarn(attribute);
        ListTag mapCursors = new ListTag();
        for (MapCursor.Type cursor : MapCursor.Type.values()) {
            mapCursors.add(cursor.toString());
        }
        event.setReplacedObject(mapCursors.getObjectAttribute(attribute.fulfill(1)));
    }
    // -->
    if (attribute.startsWith("world_types") || attribute.startsWith("list_world_types")) {
        listDeprecateWarn(attribute);
        ListTag worldTypes = new ListTag();
        for (WorldType world : WorldType.values()) {
            worldTypes.add(world.toString());
        }
        event.setReplacedObject(worldTypes.getObjectAttribute(attribute.fulfill(1)));
    }
    // -->
    if (attribute.startsWith("statistic_types") || attribute.startsWith("list_statistics")) {
        listDeprecateWarn(attribute);
        if (attribute.matches("list_statistics")) {
            Debug.echoError("list_statistics is deprecated: use statistic_types");
        }
        Statistic.Type type = null;
        if (attribute.hasParam()) {
            type = Statistic.Type.valueOf(attribute.getParam().toUpperCase());
        }
        ListTag statisticTypes = new ListTag();
        for (Statistic stat : Statistic.values()) {
            if (type == null || type == stat.getType()) {
                statisticTypes.add(stat.toString());
            }
        }
        event.setReplacedObject(statisticTypes.getObjectAttribute(attribute.fulfill(1)));
    }
    // -->
    if (attribute.startsWith("structure_types") || attribute.startsWith("list_structure_types")) {
        listDeprecateWarn(attribute);
        event.setReplacedObject(new ListTag(StructureType.getStructureTypes().keySet()).getObjectAttribute(attribute.fulfill(1)));
    }
    // -->
    if (attribute.startsWith("notes") || attribute.startsWith("list_notables") || attribute.startsWith("notables")) {
        listDeprecateWarn(attribute);
        ListTag allNotables = new ListTag();
        if (attribute.hasParam()) {
            String type = CoreUtilities.toLowerCase(attribute.getParam());
            for (Map.Entry<String, Class> typeClass : NoteManager.namesToTypes.entrySet()) {
                if (type.equals(CoreUtilities.toLowerCase(typeClass.getKey()))) {
                    for (Object notable : NoteManager.getAllType(typeClass.getValue())) {
                        allNotables.addObject((ObjectTag) notable);
                    }
                    break;
                }
            }
        } else {
            for (Notable notable : NoteManager.nameToObject.values()) {
                allNotables.addObject((ObjectTag) notable);
            }
        }
        event.setReplacedObject(allNotables.getObjectAttribute(attribute.fulfill(1)));
    }
    // -->
    if (attribute.startsWith("statistic_type") && attribute.hasParam()) {
        Statistic statistic;
        try {
            statistic = Statistic.valueOf(attribute.getParam().toUpperCase());
        } catch (IllegalArgumentException ex) {
            attribute.echoError("Statistic '" + attribute.getParam() + "' does not exist: " + ex.getMessage());
            return;
        }
        event.setReplacedObject(new ElementTag(statistic.getType().name()).getObjectAttribute(attribute.fulfill(1)));
    }
    if (attribute.startsWith("enchantment_max_level") && attribute.hasParam()) {
        Deprecations.echantmentTagUpdate.warn(attribute.context);
        EnchantmentTag ench = EnchantmentTag.valueOf(attribute.getParam(), attribute.context);
        if (ench == null) {
            attribute.echoError("Enchantment '" + attribute.getParam() + "' does not exist.");
            return;
        }
        event.setReplacedObject(new ElementTag(ench.enchantment.getMaxLevel()).getObjectAttribute(attribute.fulfill(1)));
    }
    if (attribute.startsWith("enchantment_start_level") && attribute.hasParam()) {
        Deprecations.echantmentTagUpdate.warn(attribute.context);
        EnchantmentTag ench = EnchantmentTag.valueOf(attribute.getParam(), attribute.context);
        if (ench == null) {
            attribute.echoError("Enchantment '" + attribute.getParam() + "' does not exist.");
            return;
        }
        event.setReplacedObject(new ElementTag(ench.enchantment.getStartLevel()).getObjectAttribute(attribute.fulfill(1)));
    }
    // -->
    if (attribute.startsWith("started_time")) {
        event.setReplacedObject(new TimeTag(DenizenCore.startTime).getObjectAttribute(attribute.fulfill(1)));
    }
    if (attribute.startsWith("start_time")) {
        Deprecations.timeTagRewrite.warn(attribute.context);
        event.setReplacedObject(new DurationTag(DenizenCore.startTime / 50).getObjectAttribute(attribute.fulfill(1)));
    }
    // -->
    if (attribute.startsWith("disk_free")) {
        File folder = Denizen.getInstance().getDataFolder();
        event.setReplacedObject(new ElementTag(folder.getUsableSpace()).getObjectAttribute(attribute.fulfill(1)));
    }
    // -->
    if (attribute.startsWith("disk_total")) {
        File folder = Denizen.getInstance().getDataFolder();
        event.setReplacedObject(new ElementTag(folder.getTotalSpace()).getObjectAttribute(attribute.fulfill(1)));
    }
    // -->
    if (attribute.startsWith("disk_usage")) {
        File folder = Denizen.getInstance().getDataFolder();
        event.setReplacedObject(new ElementTag(folder.getTotalSpace() - folder.getFreeSpace()).getObjectAttribute(attribute.fulfill(1)));
    }
    // -->
    if (attribute.startsWith("ram_allocated")) {
        event.setReplacedObject(new ElementTag(Runtime.getRuntime().totalMemory()).getObjectAttribute(attribute.fulfill(1)));
    }
    // -->
    if (attribute.startsWith("ram_max")) {
        event.setReplacedObject(new ElementTag(Runtime.getRuntime().maxMemory()).getObjectAttribute(attribute.fulfill(1)));
    }
    // -->
    if (attribute.startsWith("ram_free")) {
        event.setReplacedObject(new ElementTag(Runtime.getRuntime().freeMemory()).getObjectAttribute(attribute.fulfill(1)));
    }
    // -->
    if (attribute.startsWith("ram_usage")) {
        event.setReplacedObject(new ElementTag(Runtime.getRuntime().maxMemory() - Runtime.getRuntime().freeMemory()).getObjectAttribute(attribute.fulfill(1)));
    }
    // -->
    if (attribute.startsWith("available_processors")) {
        event.setReplacedObject(new ElementTag(Runtime.getRuntime().availableProcessors()).getObjectAttribute(attribute.fulfill(1)));
    }
    // -->
    if (attribute.startsWith("current_tick")) {
        event.setReplacedObject(new ElementTag(TickScriptEvent.instance.ticks).getObjectAttribute(attribute.fulfill(1)));
    }
    // -->
    if (attribute.startsWith("delta_time_since_start")) {
        event.setReplacedObject(new DurationTag(TickScriptEvent.instance.ticks).getObjectAttribute(attribute.fulfill(1)));
    }
    // -->
    if (attribute.startsWith("real_time_since_start")) {
        event.setReplacedObject(new DurationTag((System.currentTimeMillis() - serverStartTimeMillis) / 1000.0).getObjectAttribute(attribute.fulfill(1)));
    }
    // -->
    if (attribute.startsWith("current_time_millis")) {
        event.setReplacedObject(new ElementTag(System.currentTimeMillis()).getObjectAttribute(attribute.fulfill(1)));
    }
    // -->
    if (attribute.startsWith("selected_npc")) {
        NPC npc = ((Citizens) Bukkit.getPluginManager().getPlugin("Citizens")).getNPCSelector().getSelected(Bukkit.getConsoleSender());
        if (npc == null) {
            return;
        } else {
            event.setReplacedObject(new NPCTag(npc).getObjectAttribute(attribute.fulfill(1)));
        }
        return;
    }
    // -->
    if ((attribute.startsWith("npcs_named") || attribute.startsWith("list_npcs_named")) && Depends.citizens != null && attribute.hasParam()) {
        listDeprecateWarn(attribute);
        ListTag npcs = new ListTag();
        String name = attribute.getParam();
        for (NPC npc : CitizensAPI.getNPCRegistry()) {
            if (CoreUtilities.equalsIgnoreCase(npc.getName(), name)) {
                npcs.addObject(new NPCTag(npc));
            }
        }
        event.setReplacedObject(npcs.getObjectAttribute(attribute.fulfill(1)));
        return;
    }
    // -->
    if (attribute.startsWith("has_file") && attribute.hasParam()) {
        File f = new File(Denizen.getInstance().getDataFolder(), attribute.getParam());
        try {
            if (!Utilities.canReadFile(f)) {
                Debug.echoError("Cannot read from that file path due to security settings in Denizen/config.yml.");
                return;
            }
        } catch (Exception e) {
            Debug.echoError(e);
            return;
        }
        event.setReplacedObject(new ElementTag(f.exists()).getObjectAttribute(attribute.fulfill(1)));
        return;
    }
    // -->
    if (attribute.startsWith("list_files") && attribute.hasParam()) {
        File folder = new File(Denizen.getInstance().getDataFolder(), attribute.getParam());
        try {
            if (!Utilities.canReadFile(folder)) {
                Debug.echoError("Cannot read from that file path due to security settings in Denizen/config.yml.");
                return;
            }
            if (!folder.exists() || !folder.isDirectory()) {
                attribute.echoError("Invalid path specified. No directory exists at that path.");
                return;
            }
        } catch (Exception e) {
            Debug.echoError(e);
            return;
        }
        File[] files = folder.listFiles();
        if (files == null) {
            return;
        }
        ListTag list = new ListTag();
        for (File file : files) {
            list.add(file.getName());
        }
        event.setReplacedObject(list.getObjectAttribute(attribute.fulfill(1)));
        return;
    }
    // -->
    if (attribute.startsWith("has_permissions")) {
        event.setReplacedObject(new ElementTag(Depends.permissions != null && Depends.permissions.isEnabled()).getObjectAttribute(attribute.fulfill(1)));
        return;
    }
    // -->
    if (attribute.startsWith("has_economy")) {
        event.setReplacedObject(new ElementTag(Depends.economy != null && Depends.economy.isEnabled()).getObjectAttribute(attribute.fulfill(1)));
        return;
    }
    // -->
    if (attribute.startsWith("denizen_version")) {
        event.setReplacedObject(new ElementTag(Denizen.getInstance().getDescription().getVersion()).getObjectAttribute(attribute.fulfill(1)));
        return;
    }
    // -->
    if (attribute.startsWith("bukkit_version")) {
        event.setReplacedObject(new ElementTag(Bukkit.getBukkitVersion()).getObjectAttribute(attribute.fulfill(1)));
        return;
    }
    // -->
    if (attribute.startsWith("version")) {
        event.setReplacedObject(new ElementTag(Bukkit.getServer().getVersion()).getObjectAttribute(attribute.fulfill(1)));
        return;
    }
    // -->
    if (attribute.startsWith("java_version")) {
        event.setReplacedObject(new ElementTag(System.getProperty("java.version")).getObjectAttribute(attribute.fulfill(1)));
        return;
    }
    // -->
    if (attribute.startsWith("max_players")) {
        event.setReplacedObject(new ElementTag(Bukkit.getServer().getMaxPlayers()).getObjectAttribute(attribute.fulfill(1)));
        return;
    }
    // -->
    if (attribute.startsWith("sql_connections") || attribute.startsWith("list_sql_connections")) {
        listDeprecateWarn(attribute);
        ListTag list = new ListTag();
        for (Map.Entry<String, Connection> entry : SQLCommand.connections.entrySet()) {
            try {
                if (!entry.getValue().isClosed()) {
                    list.add(entry.getKey());
                } else {
                    SQLCommand.connections.remove(entry.getKey());
                }
            } catch (SQLException e) {
                Debug.echoError(attribute.getScriptEntry(), e);
            }
        }
        event.setReplacedObject(list.getObjectAttribute(attribute.fulfill(1)));
        return;
    }
    // -->
    if (attribute.startsWith("group_prefix")) {
        if (Depends.permissions == null) {
            attribute.echoError("No permission system loaded! Have you installed Vault and a compatible permissions plugin?");
            return;
        }
        String group = attribute.getParam();
        if (!Arrays.asList(Depends.permissions.getGroups()).contains(group)) {
            attribute.echoError("Invalid group! '" + (group != null ? group : "") + "' could not be found.");
            return;
        }
        // -->
        if (attribute.startsWith("world", 2)) {
            WorldTag world = attribute.contextAsType(2, WorldTag.class);
            if (world != null) {
                event.setReplacedObject(new ElementTag(Depends.chat.getGroupPrefix(world.getWorld(), group)).getObjectAttribute(attribute.fulfill(2)));
            }
            return;
        }
        // Prefix in default world
        event.setReplacedObject(new ElementTag(Depends.chat.getGroupPrefix(Bukkit.getWorlds().get(0), group)).getObjectAttribute(attribute.fulfill(1)));
        return;
    }
    // -->
    if (attribute.startsWith("group_suffix")) {
        if (Depends.permissions == null) {
            attribute.echoError("No permission system loaded! Have you installed Vault and a compatible permissions plugin?");
            return;
        }
        String group = attribute.getParam();
        if (!Arrays.asList(Depends.permissions.getGroups()).contains(group)) {
            attribute.echoError("Invalid group! '" + (group != null ? group : "") + "' could not be found.");
            return;
        }
        // -->
        if (attribute.startsWith("world", 2)) {
            WorldTag world = attribute.contextAsType(2, WorldTag.class);
            if (world != null) {
                event.setReplacedObject(new ElementTag(Depends.chat.getGroupSuffix(world.getWorld(), group)).getObjectAttribute(attribute.fulfill(2)));
            }
            return;
        }
        // Suffix in default world
        event.setReplacedObject(new ElementTag(Depends.chat.getGroupSuffix(Bukkit.getWorlds().get(0), group)).getObjectAttribute(attribute.fulfill(1)));
        return;
    }
    // -->
    if (attribute.startsWith("permission_groups") || attribute.startsWith("list_permission_groups")) {
        if (Depends.permissions == null) {
            attribute.echoError("No permission system loaded! Have you installed Vault and a compatible permissions plugin?");
            return;
        }
        listDeprecateWarn(attribute);
        event.setReplacedObject(new ListTag(Arrays.asList(Depends.permissions.getGroups())).getObjectAttribute(attribute.fulfill(1)));
        return;
    }
    if (attribute.startsWith("list_plugin_names")) {
        Deprecations.serverPluginNamesTag.warn(attribute.context);
        ListTag plugins = new ListTag();
        for (Plugin plugin : Bukkit.getServer().getPluginManager().getPlugins()) {
            plugins.add(plugin.getName());
        }
        event.setReplacedObject(plugins.getObjectAttribute(attribute.fulfill(1)));
        return;
    }
    // -->
    if (attribute.startsWith("scripts") || attribute.startsWith("list_scripts")) {
        listDeprecateWarn(attribute);
        ListTag scripts = new ListTag();
        for (ScriptContainer script : ScriptRegistry.scriptContainers.values()) {
            scripts.addObject(new ScriptTag(script));
        }
        event.setReplacedObject(scripts.getObjectAttribute(attribute.fulfill(1)));
        return;
    }
    // -->
    if (attribute.startsWith("match_player") && attribute.hasParam()) {
        Player matchPlayer = null;
        String matchInput = CoreUtilities.toLowerCase(attribute.getParam());
        if (matchInput.isEmpty()) {
            return;
        }
        for (Player player : Bukkit.getOnlinePlayers()) {
            String nameLow = CoreUtilities.toLowerCase(player.getName());
            if (nameLow.equals(matchInput)) {
                matchPlayer = player;
                break;
            } else if (nameLow.contains(matchInput)) {
                if (matchPlayer == null || nameLow.startsWith(matchInput)) {
                    matchPlayer = player;
                }
            }
        }
        if (matchPlayer != null) {
            event.setReplacedObject(new PlayerTag(matchPlayer).getObjectAttribute(attribute.fulfill(1)));
        }
        return;
    }
    // -->
    if (attribute.startsWith("match_offline_player") && attribute.hasParam()) {
        PlayerTag matchPlayer = null;
        String matchInput = CoreUtilities.toLowerCase(attribute.getParam());
        if (matchInput.isEmpty()) {
            return;
        }
        for (Map.Entry<String, UUID> entry : PlayerTag.getAllPlayers().entrySet()) {
            String nameLow = CoreUtilities.toLowerCase(entry.getKey());
            if (nameLow.equals(matchInput)) {
                matchPlayer = new PlayerTag(entry.getValue());
                break;
            } else if (nameLow.contains(matchInput)) {
                PlayerTag newMatch = new PlayerTag(entry.getValue());
                if (matchPlayer == null) {
                    matchPlayer = newMatch;
                } else if (newMatch.isOnline() && !matchPlayer.isOnline()) {
                    matchPlayer = newMatch;
                } else if (nameLow.startsWith(matchInput) && (newMatch.isOnline() == matchPlayer.isOnline())) {
                    matchPlayer = newMatch;
                }
            }
        }
        if (matchPlayer != null) {
            event.setReplacedObject(matchPlayer.getObjectAttribute(attribute.fulfill(1)));
        }
        return;
    }
    // -->
    if ((attribute.startsWith("npcs_assigned") || attribute.startsWith("list_npcs_assigned")) && Depends.citizens != null && attribute.hasParam()) {
        listDeprecateWarn(attribute);
        ScriptTag script = attribute.paramAsType(ScriptTag.class);
        if (script == null || !(script.getContainer() instanceof AssignmentScriptContainer)) {
            attribute.echoError("Invalid script specified.");
        } else {
            AssignmentScriptContainer container = (AssignmentScriptContainer) script.getContainer();
            ListTag npcs = new ListTag();
            for (NPC npc : CitizensAPI.getNPCRegistry()) {
                if (npc.hasTrait(AssignmentTrait.class) && npc.getOrAddTrait(AssignmentTrait.class).isAssigned(container)) {
                    npcs.addObject(new NPCTag(npc));
                }
            }
            event.setReplacedObject(npcs.getObjectAttribute(attribute.fulfill(1)));
            return;
        }
    }
    // -->
    if ((attribute.startsWith("online_players_flagged") || attribute.startsWith("list_online_players_flagged")) && attribute.hasParam()) {
        listDeprecateWarn(attribute);
        String flag = attribute.getParam();
        ListTag players = new ListTag();
        for (Player player : Bukkit.getOnlinePlayers()) {
            PlayerTag plTag = new PlayerTag(player);
            if (plTag.getFlagTracker().hasFlag(flag)) {
                players.addObject(plTag);
            }
        }
        event.setReplacedObject(players.getObjectAttribute(attribute.fulfill(1)));
        return;
    }
    // -->
    if ((attribute.startsWith("players_flagged") || attribute.startsWith("list_players_flagged")) && attribute.hasParam()) {
        listDeprecateWarn(attribute);
        String flag = attribute.getParam();
        ListTag players = new ListTag();
        for (Map.Entry<String, UUID> entry : PlayerTag.getAllPlayers().entrySet()) {
            PlayerTag plTag = new PlayerTag(entry.getValue());
            if (plTag.getFlagTracker().hasFlag(flag)) {
                players.addObject(plTag);
            }
        }
        event.setReplacedObject(players.getObjectAttribute(attribute.fulfill(1)));
        return;
    }
    // -->
    if ((attribute.startsWith("spawned_npcs_flagged") || attribute.startsWith("list_spawned_npcs_flagged")) && Depends.citizens != null && attribute.hasParam()) {
        listDeprecateWarn(attribute);
        String flag = attribute.getParam();
        ListTag npcs = new ListTag();
        for (NPC npc : CitizensAPI.getNPCRegistry()) {
            NPCTag dNpc = new NPCTag(npc);
            if (dNpc.isSpawned() && dNpc.hasFlag(flag)) {
                npcs.addObject(dNpc);
            }
        }
        event.setReplacedObject(npcs.getObjectAttribute(attribute.fulfill(1)));
        return;
    }
    // -->
    if ((attribute.startsWith("npcs_flagged") || attribute.startsWith("list_npcs_flagged")) && Depends.citizens != null && attribute.hasParam()) {
        listDeprecateWarn(attribute);
        String flag = attribute.getParam();
        ListTag npcs = new ListTag();
        for (NPC npc : CitizensAPI.getNPCRegistry()) {
            NPCTag dNpc = new NPCTag(npc);
            if (dNpc.hasFlag(flag)) {
                npcs.addObject(dNpc);
            }
        }
        event.setReplacedObject(npcs.getObjectAttribute(attribute.fulfill(1)));
        return;
    }
    // -->
    if (attribute.startsWith("npc_registries") && Depends.citizens != null) {
        ListTag result = new ListTag();
        for (NPCRegistry registry : CitizensAPI.getNPCRegistries()) {
            result.add(registry.getName());
        }
        event.setReplacedObject(result.getObjectAttribute(attribute.fulfill(1)));
        return;
    }
    // -->
    if ((attribute.startsWith("npcs") || attribute.startsWith("list_npcs")) && Depends.citizens != null) {
        listDeprecateWarn(attribute);
        ListTag npcs = new ListTag();
        NPCRegistry registry = CitizensAPI.getNPCRegistry();
        if (attribute.hasParam()) {
            registry = NPCTag.getRegistryByName(attribute.getParam());
            if (registry == null) {
                attribute.echoError("NPC Registry '" + attribute.getParam() + "' does not exist.");
                return;
            }
        }
        for (NPC npc : registry) {
            npcs.addObject(new NPCTag(npc));
        }
        event.setReplacedObject(npcs.getObjectAttribute(attribute.fulfill(1)));
        return;
    }
    // -->
    if (attribute.startsWith("worlds") || attribute.startsWith("list_worlds")) {
        listDeprecateWarn(attribute);
        ListTag worlds = new ListTag();
        for (World world : Bukkit.getWorlds()) {
            worlds.addObject(WorldTag.mirrorBukkitWorld(world));
        }
        event.setReplacedObject(worlds.getObjectAttribute(attribute.fulfill(1)));
        return;
    }
    // -->
    if (attribute.startsWith("plugins") || attribute.startsWith("list_plugins")) {
        listDeprecateWarn(attribute);
        ListTag plugins = new ListTag();
        for (Plugin plugin : Bukkit.getServer().getPluginManager().getPlugins()) {
            plugins.addObject(new PluginTag(plugin));
        }
        event.setReplacedObject(plugins.getObjectAttribute(attribute.fulfill(1)));
        return;
    }
    // -->
    if (attribute.startsWith("players") || attribute.startsWith("list_players")) {
        listDeprecateWarn(attribute);
        ListTag players = new ListTag();
        for (OfflinePlayer player : Bukkit.getOfflinePlayers()) {
            players.addObject(PlayerTag.mirrorBukkitPlayer(player));
        }
        event.setReplacedObject(players.getObjectAttribute(attribute.fulfill(1)));
        return;
    }
    // -->
    if (attribute.startsWith("online_players") || attribute.startsWith("list_online_players")) {
        listDeprecateWarn(attribute);
        ListTag players = new ListTag();
        for (Player player : Bukkit.getOnlinePlayers()) {
            players.addObject(PlayerTag.mirrorBukkitPlayer(player));
        }
        event.setReplacedObject(players.getObjectAttribute(attribute.fulfill(1)));
        return;
    }
    // -->
    if (attribute.startsWith("offline_players") || attribute.startsWith("list_offline_players")) {
        listDeprecateWarn(attribute);
        ListTag players = new ListTag();
        for (OfflinePlayer player : Bukkit.getOfflinePlayers()) {
            if (!player.isOnline()) {
                players.addObject(PlayerTag.mirrorBukkitPlayer(player));
            }
        }
        event.setReplacedObject(players.getObjectAttribute(attribute.fulfill(1)));
        return;
    }
    // -->
    if (attribute.startsWith("banned_players") || attribute.startsWith("list_banned_players")) {
        listDeprecateWarn(attribute);
        ListTag banned = new ListTag();
        for (OfflinePlayer player : Bukkit.getBannedPlayers()) {
            banned.addObject(PlayerTag.mirrorBukkitPlayer(player));
        }
        event.setReplacedObject(banned.getObjectAttribute(attribute.fulfill(1)));
        return;
    }
    // -->
    if (attribute.startsWith("banned_addresses") || attribute.startsWith("list_banned_addresses")) {
        listDeprecateWarn(attribute);
        ListTag list = new ListTag();
        list.addAll(Bukkit.getIPBans());
        event.setReplacedObject(list.getObjectAttribute(attribute.fulfill(1)));
        return;
    }
    // -->
    if (attribute.startsWith("is_banned") && attribute.hasParam()) {
        // BanList contains an isBanned method that doesn't check expiration time
        BanEntry ban = Bukkit.getBanList(BanList.Type.IP).getBanEntry(attribute.getParam());
        if (ban == null) {
            event.setReplacedObject(new ElementTag(false).getObjectAttribute(attribute.fulfill(1)));
        } else if (ban.getExpiration() == null) {
            event.setReplacedObject(new ElementTag(true).getObjectAttribute(attribute.fulfill(1)));
        } else {
            event.setReplacedObject(new ElementTag(ban.getExpiration().after(new Date())).getObjectAttribute(attribute.fulfill(1)));
        }
        return;
    }
    if (attribute.startsWith("ban_info") && attribute.hasParam()) {
        BanEntry ban = Bukkit.getBanList(BanList.Type.IP).getBanEntry(attribute.getParam());
        attribute.fulfill(1);
        if (ban == null || (ban.getExpiration() != null && ban.getExpiration().before(new Date()))) {
            return;
        }
        // -->
        if (attribute.startsWith("expiration_time") && ban.getExpiration() != null) {
            event.setReplacedObject(new TimeTag(ban.getExpiration().getTime()).getObjectAttribute(attribute.fulfill(1)));
        } else if (attribute.startsWith("expiration") && ban.getExpiration() != null) {
            Deprecations.timeTagRewrite.warn(attribute.context);
            event.setReplacedObject(new DurationTag(ban.getExpiration().getTime() / 50).getObjectAttribute(attribute.fulfill(1)));
        } else // -->
        if (attribute.startsWith("reason")) {
            event.setReplacedObject(new ElementTag(ban.getReason()).getObjectAttribute(attribute.fulfill(1)));
        } else // -->
        if (attribute.startsWith("created_time")) {
            event.setReplacedObject(new TimeTag(ban.getCreated().getTime()).getObjectAttribute(attribute.fulfill(1)));
        } else if (attribute.startsWith("created")) {
            Deprecations.timeTagRewrite.warn(attribute.context);
            event.setReplacedObject(new DurationTag(ban.getCreated().getTime() / 50).getObjectAttribute(attribute.fulfill(1)));
        } else // -->
        if (attribute.startsWith("source")) {
            event.setReplacedObject(new ElementTag(ban.getSource()).getObjectAttribute(attribute.fulfill(1)));
        }
        return;
    }
    // -->
    if (attribute.startsWith("ops") || attribute.startsWith("list_ops")) {
        listDeprecateWarn(attribute);
        ListTag players = new ListTag();
        for (OfflinePlayer player : Bukkit.getOperators()) {
            players.addObject(PlayerTag.mirrorBukkitPlayer(player));
        }
        event.setReplacedObject(players.getObjectAttribute(attribute.fulfill(1)));
        return;
    }
    // -->
    if (attribute.startsWith("online_ops") || attribute.startsWith("list_online_ops")) {
        listDeprecateWarn(attribute);
        ListTag players = new ListTag();
        for (OfflinePlayer player : Bukkit.getOperators()) {
            if (player.isOnline()) {
                players.addObject(PlayerTag.mirrorBukkitPlayer(player));
            }
        }
        event.setReplacedObject(players.getObjectAttribute(attribute.fulfill(1)));
        return;
    }
    // -->
    if (attribute.startsWith("offline_ops") || attribute.startsWith("list_offline_ops")) {
        listDeprecateWarn(attribute);
        ListTag players = new ListTag();
        for (OfflinePlayer player : Bukkit.getOfflinePlayers()) {
            if (player.isOp() && !player.isOnline()) {
                players.addObject(PlayerTag.mirrorBukkitPlayer(player));
            }
        }
        event.setReplacedObject(players.getObjectAttribute(attribute.fulfill(1)));
        return;
    }
    // -->
    if (attribute.startsWith("motd")) {
        event.setReplacedObject(new ElementTag(Bukkit.getServer().getMotd()).getObjectAttribute(attribute.fulfill(1)));
        return;
    }
    // -->
    if (attribute.startsWith("view_distance")) {
        event.setReplacedObject(new ElementTag(Bukkit.getServer().getViewDistance()).getObjectAttribute(attribute.fulfill(1)));
        return;
    } else if (attribute.startsWith("entity_is_spawned") && attribute.hasParam()) {
        Deprecations.isValidTag.warn(attribute.context);
        EntityTag ent = EntityTag.valueOf(attribute.getParam(), new BukkitTagContext(null, null, null, false, null));
        event.setReplacedObject(new ElementTag((ent != null && ent.isUnique() && ent.isSpawnedOrValidForTag()) ? "true" : "false").getObjectAttribute(attribute.fulfill(1)));
    } else if (attribute.startsWith("player_is_valid") && attribute.hasParam()) {
        Deprecations.isValidTag.warn(attribute.context);
        event.setReplacedObject(new ElementTag(PlayerTag.playerNameIsValid(attribute.getParam())).getObjectAttribute(attribute.fulfill(1)));
    } else if (attribute.startsWith("npc_is_valid") && attribute.hasParam()) {
        Deprecations.isValidTag.warn(attribute.context);
        NPCTag npc = NPCTag.valueOf(attribute.getParam(), new BukkitTagContext(null, null, null, false, null));
        event.setReplacedObject(new ElementTag((npc != null && npc.isValid())).getObjectAttribute(attribute.fulfill(1)));
    } else // -->
    if (attribute.startsWith("current_bossbars")) {
        ListTag dl = new ListTag(BossBarCommand.bossBarMap.keySet());
        event.setReplacedObject(dl.getObjectAttribute(attribute.fulfill(1)));
    } else // -->
    if (attribute.startsWith("bossbar_viewers") && attribute.hasParam()) {
        BossBar bar = BossBarCommand.bossBarMap.get(CoreUtilities.toLowerCase(attribute.getParam()));
        if (bar != null) {
            ListTag list = new ListTag();
            for (Player player : bar.getPlayers()) {
                list.addObject(new PlayerTag(player));
            }
            event.setReplacedObject(list.getObjectAttribute(attribute.fulfill(1)));
        }
    } else // -->
    if (attribute.startsWith("recent_tps")) {
        ListTag list = new ListTag();
        for (double tps : NMSHandler.getInstance().getRecentTps()) {
            list.addObject(new ElementTag(tps));
        }
        event.setReplacedObject(list.getObjectAttribute(attribute.fulfill(1)));
    } else // -->
    if (attribute.startsWith("port")) {
        event.setReplacedObject(new ElementTag(NMSHandler.getInstance().getPort()).getObjectAttribute(attribute.fulfill(1)));
    } else // -->
    if (attribute.startsWith("debug_enabled")) {
        event.setReplacedObject(new ElementTag(com.denizenscript.denizen.utilities.debugging.Debug.showDebug).getObjectAttribute(attribute.fulfill(1)));
    } else // -->
    if (attribute.startsWith("last_reload")) {
        event.setReplacedObject(new TimeTag(DenizenCore.lastReloadTime).getObjectAttribute(attribute.fulfill(1)));
    } else // -->
    if (attribute.startsWith("idle_timeout")) {
        event.setReplacedObject(new DurationTag(Bukkit.getIdleTimeout() * 60).getObjectAttribute(attribute.fulfill(1)));
    } else // -->
    if (attribute.startsWith("vanilla_tags")) {
        event.setReplacedObject(new ListTag(VanillaTagHelper.tagsByKey.keySet()).getObjectAttribute(attribute.fulfill(1)));
    } else // -->
    if (attribute.startsWith("vanilla_tagged_materials")) {
        if (!attribute.hasParam()) {
            return;
        }
        HashSet<Material> materials = VanillaTagHelper.tagsByKey.get(CoreUtilities.toLowerCase(attribute.getParam()));
        if (materials == null) {
            return;
        }
        ListTag list = new ListTag();
        for (Material material : materials) {
            list.addObject(new MaterialTag(material));
        }
        event.setReplacedObject(list.getObjectAttribute(attribute.fulfill(1)));
    } else // -->
    if ((attribute.matches("plugins_handling_event") || attribute.matches("list_plugins_handling_event")) && attribute.hasParam()) {
        listDeprecateWarn(attribute);
        String eventName = attribute.getParam();
        if (CoreUtilities.contains(eventName, '.')) {
            try {
                Class clazz = Class.forName(eventName, false, ServerTagBase.class.getClassLoader());
                ListTag result = getHandlerPluginList(clazz);
                if (result != null) {
                    event.setReplacedObject(result.getObjectAttribute(attribute.fulfill(1)));
                }
            } catch (ClassNotFoundException ex) {
                if (!attribute.hasAlternative()) {
                    Debug.echoError(ex);
                }
            }
        } else {
            ScriptEvent scriptEvent = ScriptEvent.eventLookup.get(CoreUtilities.toLowerCase(eventName));
            if (scriptEvent instanceof Listener) {
                Plugin plugin = Denizen.getInstance();
                for (Class eventClass : plugin.getPluginLoader().createRegisteredListeners((Listener) scriptEvent, plugin).keySet()) {
                    ListTag result = getHandlerPluginList(eventClass);
                    // Return results for the first valid match.
                    if (result != null && result.size() > 0) {
                        event.setReplacedObject(result.getObjectAttribute(attribute.fulfill(1)));
                        return;
                    }
                }
                event.setReplacedObject(new ListTag().getObjectAttribute(attribute.fulfill(1)));
            }
        }
    } else // -->
    if (attribute.startsWith("generate_loot_table") && attribute.hasParam()) {
        MapTag map = attribute.paramAsType(MapTag.class);
        ObjectTag idObj = map.getObject("id");
        ObjectTag locationObj = map.getObject("location");
        if (idObj == null || locationObj == null) {
            return;
        }
        NamespacedKey key = NamespacedKey.fromString(CoreUtilities.toLowerCase(idObj.toString()));
        if (key == null) {
            return;
        }
        LootTable table = Bukkit.getLootTable(key);
        LootContext.Builder context = new LootContext.Builder(locationObj.asType(LocationTag.class, attribute.context));
        ObjectTag killer = map.getObject("killer");
        ObjectTag luck = map.getObject("luck");
        ObjectTag bonus = map.getObject("loot_bonus");
        ObjectTag entity = map.getObject("entity");
        if (entity != null) {
            context = context.lootedEntity(entity.asType(EntityTag.class, attribute.context).getBukkitEntity());
        }
        if (killer != null) {
            context = context.killer((HumanEntity) killer.asType(EntityTag.class, attribute.context).getLivingEntity());
        }
        if (luck != null) {
            context = context.luck(luck.asElement().asFloat());
        }
        if (bonus != null) {
            context = context.lootingModifier(bonus.asElement().asInt());
        }
        Collection<ItemStack> items;
        try {
            items = table.populateLoot(CoreUtilities.getRandom(), context.build());
        } catch (Throwable ex) {
            attribute.echoError("Loot table failed to generate: " + ex.getMessage());
            if (Debug.verbose) {
                attribute.echoError(ex);
            }
            return;
        }
        ListTag result = new ListTag();
        for (ItemStack item : items) {
            if (item != null && item.getType() != Material.AIR) {
                result.addObject(new ItemTag(item));
            }
        }
        event.setReplacedObject(result.getObjectAttribute(attribute.fulfill(1)));
    } else // -->
    if (attribute.startsWith("stack_trace")) {
        String trace = com.denizenscript.denizen.utilities.debugging.Debug.getFullExceptionMessage(new RuntimeException("TRACE"), false);
        event.setReplacedObject(new ElementTag(trace).getObjectAttribute(attribute.fulfill(1)));
    }
}
Also used : Plugin(org.bukkit.plugin.Plugin) PatternType(org.bukkit.block.banner.PatternType) Utilities(com.denizenscript.denizen.utilities.Utilities) AssignmentScriptContainer(com.denizenscript.denizen.scripts.containers.core.AssignmentScriptContainer) Connection(java.sql.Connection) TagManager(com.denizenscript.denizencore.tags.TagManager) Enchantment(org.bukkit.enchantments.Enchantment) NMSHandler(com.denizenscript.denizen.nms.NMSHandler) SQLCommand(com.denizenscript.denizencore.scripts.commands.core.SQLCommand) CommandScriptHelper(com.denizenscript.denizen.scripts.containers.core.CommandScriptHelper) TagRunnable(com.denizenscript.denizencore.tags.TagRunnable) Event(org.bukkit.event.Event) BossBarCommand(com.denizenscript.denizen.scripts.commands.server.BossBarCommand) Player(org.bukkit.entity.Player) NoteManager(com.denizenscript.denizencore.objects.notable.NoteManager) Biome(org.bukkit.block.Biome) Objective(org.bukkit.scoreboard.Objective) org.bukkit(org.bukkit) org.bukkit.inventory(org.bukkit.inventory) Scoreboard(org.bukkit.scoreboard.Scoreboard) CitizensAPI(net.citizensnpcs.api.CitizensAPI) VanillaTagHelper(com.denizenscript.denizen.utilities.VanillaTagHelper) BukkitTagContext(com.denizenscript.denizen.tags.BukkitTagContext) PotionType(org.bukkit.potion.PotionType) EntityType(org.bukkit.entity.EntityType) PluginCommand(org.bukkit.command.PluginCommand) BukkitScriptEvent(com.denizenscript.denizen.events.BukkitScriptEvent) Settings(com.denizenscript.denizen.utilities.Settings) MapCursor(org.bukkit.map.MapCursor) SlotHelper(com.denizenscript.denizen.utilities.inventory.SlotHelper) Deprecations(com.denizenscript.denizencore.utilities.Deprecations) PotionEffectType(org.bukkit.potion.PotionEffectType) HandlerList(org.bukkit.event.HandlerList) Citizens(net.citizensnpcs.Citizens) java.util(java.util) ScriptContainer(com.denizenscript.denizencore.scripts.containers.ScriptContainer) ReplaceableTagEvent(com.denizenscript.denizencore.tags.ReplaceableTagEvent) NPCRegistry(net.citizensnpcs.api.npc.NPCRegistry) Debug(com.denizenscript.denizencore.utilities.debugging.Debug) NPC(net.citizensnpcs.api.npc.NPC) LootContext(org.bukkit.loot.LootContext) DenizenCore(com.denizenscript.denizencore.DenizenCore) FutureWarning(com.denizenscript.denizencore.utilities.debugging.FutureWarning) SQLException(java.sql.SQLException) CommandContext(net.citizensnpcs.api.command.CommandContext) TickScriptEvent(com.denizenscript.denizencore.events.core.TickScriptEvent) ScriptRegistry(com.denizenscript.denizencore.scripts.ScriptRegistry) Attribute(com.denizenscript.denizencore.tags.Attribute) EntityDamageEvent(org.bukkit.event.entity.EntityDamageEvent) Listener(org.bukkit.event.Listener) HumanEntity(org.bukkit.entity.HumanEntity) AssignmentTrait(com.denizenscript.denizen.npc.traits.AssignmentTrait) LootTable(org.bukkit.loot.LootTable) Team(org.bukkit.scoreboard.Team) ScriptEvent(com.denizenscript.denizencore.events.ScriptEvent) com.denizenscript.denizencore.objects.core(com.denizenscript.denizencore.objects.core) Depends(com.denizenscript.denizen.utilities.depends.Depends) Notable(com.denizenscript.denizencore.objects.notable.Notable) File(java.io.File) com.denizenscript.denizen.objects(com.denizenscript.denizen.objects) Denizen(com.denizenscript.denizen.Denizen) RegisteredListener(org.bukkit.plugin.RegisteredListener) Consumer(java.util.function.Consumer) TraitInfo(net.citizensnpcs.api.trait.TraitInfo) BossBar(org.bukkit.boss.BossBar) PlayerTeleportEvent(org.bukkit.event.player.PlayerTeleportEvent) YamlConfiguration(org.bukkit.configuration.file.YamlConfiguration) ScoreboardHelper(com.denizenscript.denizen.utilities.ScoreboardHelper) com.denizenscript.denizencore.objects(com.denizenscript.denizencore.objects) CoreUtilities(com.denizenscript.denizencore.utilities.CoreUtilities) BossBar(org.bukkit.boss.BossBar) TraitInfo(net.citizensnpcs.api.trait.TraitInfo) HumanEntity(org.bukkit.entity.HumanEntity) PatternType(org.bukkit.block.banner.PatternType) PotionEffectType(org.bukkit.potion.PotionEffectType) MapCursor(org.bukkit.map.MapCursor) PotionType(org.bukkit.potion.PotionType) Attribute(com.denizenscript.denizencore.tags.Attribute) YamlConfiguration(org.bukkit.configuration.file.YamlConfiguration) Biome(org.bukkit.block.Biome) Team(org.bukkit.scoreboard.Team) EntityDamageEvent(org.bukkit.event.entity.EntityDamageEvent) NPCRegistry(net.citizensnpcs.api.npc.NPCRegistry) LootTable(org.bukkit.loot.LootTable) Connection(java.sql.Connection) AssignmentTrait(com.denizenscript.denizen.npc.traits.AssignmentTrait) BukkitScriptEvent(com.denizenscript.denizen.events.BukkitScriptEvent) TickScriptEvent(com.denizenscript.denizencore.events.core.TickScriptEvent) ScriptEvent(com.denizenscript.denizencore.events.ScriptEvent) AssignmentScriptContainer(com.denizenscript.denizen.scripts.containers.core.AssignmentScriptContainer) Player(org.bukkit.entity.Player) LootContext(org.bukkit.loot.LootContext) PlayerTeleportEvent(org.bukkit.event.player.PlayerTeleportEvent) Notable(com.denizenscript.denizencore.objects.notable.Notable) EntityType(org.bukkit.entity.EntityType) Objective(org.bukkit.scoreboard.Objective) AssignmentScriptContainer(com.denizenscript.denizen.scripts.containers.core.AssignmentScriptContainer) ScriptContainer(com.denizenscript.denizencore.scripts.containers.ScriptContainer) BukkitTagContext(com.denizenscript.denizen.tags.BukkitTagContext) Enchantment(org.bukkit.enchantments.Enchantment) File(java.io.File) NPC(net.citizensnpcs.api.npc.NPC) Listener(org.bukkit.event.Listener) RegisteredListener(org.bukkit.plugin.RegisteredListener) SQLException(java.sql.SQLException) Scoreboard(org.bukkit.scoreboard.Scoreboard) PluginCommand(org.bukkit.command.PluginCommand) SQLException(java.sql.SQLException) Plugin(org.bukkit.plugin.Plugin)

Example 2 with LootTable

use of org.bukkit.loot.LootTable in project Denizen-For-Bukkit by DenizenScript.

the class LocationTag method registerTags.

public static void registerTags() {
    AbstractFlagTracker.registerFlagHandlers(tagProcessor);
    // <--[tag]
    // @attribute <LocationTag.block_facing>
    // @returns LocationTag
    // @mechanism LocationTag.block_facing
    // @group world
    // @description
    // Returns the relative location vector of where this block is facing.
    // Only works for block types that have directionality (such as signs, chests, stairs, etc.).
    // This can return for example "1,0,0" to mean the block is facing towards the positive X axis.
    // You can use <some_block_location.add[<some_block_location.block_facing>]> to get the block directly in front of this block (based on its facing direction).
    // -->
    tagProcessor.registerTag(LocationTag.class, "block_facing", (attribute, object) -> {
        Block block = object.getBlockForTag(attribute);
        MaterialTag material = new MaterialTag(block);
        if (!MaterialDirectional.describes(material)) {
            return null;
        }
        Vector vec = MaterialDirectional.getFrom(material).getDirectionVector();
        if (vec == null) {
            return null;
        }
        return new LocationTag(object.getWorld(), vec);
    });
    // <--[tag]
    // @attribute <LocationTag.with_facing_direction>
    // @returns LocationTag
    // @group world
    // @description
    // Returns the location with its direction set to the block's facing direction.
    // Only works for block types that have directionality (such as signs, chests, stairs, etc.).
    // You can use <some_block_location.with_facing_direction.forward[1]> to get the block directly in front of this block (based on its facing direction).
    // -->
    tagProcessor.registerTag(LocationTag.class, "with_facing_direction", (attribute, object) -> {
        Block block = object.getBlockForTag(attribute);
        MaterialTag material = new MaterialTag(block);
        if (!MaterialDirectional.describes(material)) {
            return null;
        }
        Vector facing = MaterialDirectional.getFrom(material).getDirectionVector();
        if (facing == null) {
            return null;
        }
        LocationTag result = object.clone();
        result.setDirection(facing);
        return result;
    });
    // <--[tag]
    // @attribute <LocationTag.above[(<#.#>)]>
    // @returns LocationTag
    // @group math
    // @description
    // Returns the location above this location. Optionally specify a number of blocks to go up.
    // This just moves straight along the Y axis, equivalent to <@link tag LocationTag.add> with input 0,1,0 (or the input value instead of '1').
    // -->
    tagProcessor.registerTag(LocationTag.class, "above", (attribute, object) -> {
        return new LocationTag(object.clone().add(0, attribute.hasParam() ? attribute.getDoubleParam() : 1, 0));
    });
    // <--[tag]
    // @attribute <LocationTag.below[(<#.#>)]>
    // @returns LocationTag
    // @group math
    // @description
    // Returns the location below this location. Optionally specify a number of blocks to go down.
    // This just moves straight along the Y axis, equivalent to <@link tag LocationTag.sub> with input 0,1,0 (or the input value instead of '1').
    // -->
    tagProcessor.registerTag(LocationTag.class, "below", (attribute, object) -> {
        return new LocationTag(object.clone().subtract(0, attribute.hasParam() ? attribute.getDoubleParam() : 1, 0));
    });
    // <--[tag]
    // @attribute <LocationTag.forward_flat[(<#.#>)]>
    // @returns LocationTag
    // @group math
    // @description
    // Returns the location in front of this location based on yaw but not pitch. Optionally specify a number of blocks to go forward.
    // -->
    tagProcessor.registerTag(LocationTag.class, "forward_flat", (attribute, object) -> {
        Location loc = object.clone();
        loc.setPitch(0);
        Vector vector = loc.getDirection().multiply(attribute.hasParam() ? attribute.getDoubleParam() : 1);
        return new LocationTag(object.clone().add(vector));
    });
    // <--[tag]
    // @attribute <LocationTag.backward_flat[(<#.#>)]>
    // @returns LocationTag
    // @group math
    // @description
    // Returns the location behind this location based on yaw but not pitch. Optionally specify a number of blocks to go backward.
    // This is equivalent to <@link tag LocationTag.forward_flat> in the opposite direction.
    // -->
    tagProcessor.registerTag(LocationTag.class, "backward_flat", (attribute, object) -> {
        Location loc = object.clone();
        loc.setPitch(0);
        Vector vector = loc.getDirection().multiply(attribute.hasParam() ? attribute.getDoubleParam() : 1);
        return new LocationTag(object.clone().subtract(vector));
    });
    // <--[tag]
    // @attribute <LocationTag.forward[(<#.#>)]>
    // @returns LocationTag
    // @group math
    // @description
    // Returns the location in front of this location based on pitch and yaw. Optionally specify a number of blocks to go forward.
    // -->
    tagProcessor.registerTag(LocationTag.class, "forward", (attribute, object) -> {
        Vector vector = object.getDirection().multiply(attribute.hasParam() ? attribute.getDoubleParam() : 1);
        return new LocationTag(object.clone().add(vector));
    });
    // <--[tag]
    // @attribute <LocationTag.backward[(<#.#>)]>
    // @returns LocationTag
    // @group math
    // @description
    // Returns the location behind this location based on pitch and yaw. Optionally specify a number of blocks to go backward.
    // This is equivalent to <@link tag LocationTag.forward> in the opposite direction.
    // -->
    tagProcessor.registerTag(LocationTag.class, "backward", (attribute, object) -> {
        Vector vector = object.getDirection().multiply(attribute.hasParam() ? attribute.getDoubleParam() : 1);
        return new LocationTag(object.clone().subtract(vector));
    });
    // <--[tag]
    // @attribute <LocationTag.left[(<#.#>)]>
    // @returns LocationTag
    // @group math
    // @description
    // Returns the location to the left of this location based on pitch and yaw. Optionally specify a number of blocks to go left.
    // This is equivalent to <@link tag LocationTag.forward> with a +90 degree rotation to the yaw and the pitch set to 0.
    // -->
    tagProcessor.registerTag(LocationTag.class, "left", (attribute, object) -> {
        Location loc = object.clone();
        loc.setPitch(0);
        Vector vector = loc.getDirection().rotateAroundY(Math.PI / 2).multiply(attribute.hasParam() ? attribute.getDoubleParam() : 1);
        return new LocationTag(object.clone().add(vector));
    });
    // <--[tag]
    // @attribute <LocationTag.right[(<#.#>)]>
    // @returns LocationTag
    // @group math
    // @description
    // Returns the location to the right of this location based on pitch and yaw. Optionally specify a number of blocks to go right.
    // This is equivalent to <@link tag LocationTag.forward> with a -90 degree rotation to the yaw and the pitch set to 0.
    // -->
    tagProcessor.registerTag(LocationTag.class, "right", (attribute, object) -> {
        Location loc = object.clone();
        loc.setPitch(0);
        Vector vector = loc.getDirection().rotateAroundY(Math.PI / 2).multiply(attribute.hasParam() ? attribute.getDoubleParam() : 1);
        return new LocationTag(object.clone().subtract(vector));
    });
    // <--[tag]
    // @attribute <LocationTag.up[(<#.#>)]>
    // @returns LocationTag
    // @group math
    // @description
    // Returns the location above this location based on pitch and yaw. Optionally specify a number of blocks to go up.
    // This is equivalent to <@link tag LocationTag.forward> with a +90 degree rotation to the pitch.
    // To just get the location above this location, use <@link tag LocationTag.above> instead.
    // -->
    tagProcessor.registerTag(LocationTag.class, "up", (attribute, object) -> {
        Location loc = object.clone();
        loc.setPitch(loc.getPitch() - 90);
        Vector vector = loc.getDirection().multiply(attribute.hasParam() ? attribute.getDoubleParam() : 1);
        return new LocationTag(object.clone().add(vector));
    });
    // <--[tag]
    // @attribute <LocationTag.down[(<#.#>)]>
    // @returns LocationTag
    // @group math
    // @description
    // Returns the location below this location based on pitch and yaw. Optionally specify a number of blocks to go down.
    // This is equivalent to <@link tag LocationTag.forward> with a -90 degree rotation to the pitch.
    // To just get the location above this location, use <@link tag LocationTag.below> instead.
    // -->
    tagProcessor.registerTag(LocationTag.class, "down", (attribute, object) -> {
        Location loc = object.clone();
        loc.setPitch(loc.getPitch() - 90);
        Vector vector = loc.getDirection().multiply(attribute.hasParam() ? attribute.getDoubleParam() : 1);
        return new LocationTag(object.clone().subtract(vector));
    });
    // <--[tag]
    // @attribute <LocationTag.relative[<location>]>
    // @returns LocationTag
    // @group math
    // @description
    // Returns the location relative to this location. Input is a vector location of the form left,up,forward.
    // For example, input -1,1,1 will return a location 1 block to the right, 1 block up, and 1 block forward.
    // To just get the location relative to this without rotation math, use <@link tag LocationTag.add> instead.
    // -->
    tagProcessor.registerTag(LocationTag.class, "relative", (attribute, object) -> {
        if (!attribute.hasParam()) {
            return null;
        }
        LocationTag offsetLoc = attribute.paramAsType(LocationTag.class);
        if (offsetLoc == null) {
            return null;
        }
        Location loc = object.clone();
        Vector offset = loc.getDirection().multiply(offsetLoc.getZ());
        loc.setPitch(loc.getPitch() - 90);
        offset = offset.add(loc.getDirection().multiply(offsetLoc.getY()));
        loc.setPitch(0);
        offset = offset.add(loc.getDirection().rotateAroundY(Math.PI / 2).multiply(offsetLoc.getX()));
        return new LocationTag(object.clone().add(offset));
    });
    // <--[tag]
    // @attribute <LocationTag.block>
    // @returns LocationTag
    // @group math
    // @description
    // Returns the location of the block this location is on,
    // i.e. returns a location without decimals or direction.
    // Note that you almost never actually need this tag. This does not "get the block", this just rounds coordinates down.
    // If you have this in a script, it is more likely to be a mistake than actually needed.
    // Consider using <@link tag LocationTag.round_down> instead.
    // -->
    tagProcessor.registerTag(LocationTag.class, "block", (attribute, object) -> {
        return new LocationTag(object.getWorld(), object.getBlockX(), object.getBlockY(), object.getBlockZ());
    });
    // <--[tag]
    // @attribute <LocationTag.center>
    // @returns LocationTag
    // @group math
    // @description
    // Returns the location at the center of the block this location is on.
    // -->
    tagProcessor.registerTag(LocationTag.class, "center", (attribute, object) -> {
        return new LocationTag(object.getWorld(), object.getBlockX() + 0.5, object.getBlockY() + 0.5, object.getBlockZ() + 0.5);
    });
    // <--[tag]
    // @attribute <LocationTag.random_offset[<limit>]>
    // @returns LocationTag
    // @group math
    // @description
    // Returns a copy of this location, with the X/Y/Z offset by a random decimal value up to a given limit.
    // The limit can either be an X,Y,Z location vector like [3,1,3] or a single value like [3] (which is equivalent to [3,3,3]).
    // For example, for a location at 0,100,0, ".random_offset[1,2,3]" can return any decimal location within the cuboid from -1,98,-3 to 1,102,3.
    // -->
    tagProcessor.registerTag(LocationTag.class, "random_offset", (attribute, object) -> {
        if (!attribute.hasParam()) {
            attribute.echoError("LocationTag.random_offset[...] must have an input.");
            return null;
        }
        Vector offsetLimit;
        if (ArgumentHelper.matchesDouble(attribute.getParam())) {
            double val = attribute.getDoubleParam();
            offsetLimit = new Vector(val, val, val);
        } else {
            LocationTag val = attribute.paramAsType(LocationTag.class);
            if (val == null) {
                return null;
            }
            offsetLimit = val.toVector();
        }
        offsetLimit.setX(offsetLimit.getX() * (CoreUtilities.getRandom().nextDouble() * 2 - 1));
        offsetLimit.setY(offsetLimit.getY() * (CoreUtilities.getRandom().nextDouble() * 2 - 1));
        offsetLimit.setZ(offsetLimit.getZ() * (CoreUtilities.getRandom().nextDouble() * 2 - 1));
        LocationTag output = object.clone();
        output.add(offsetLimit);
        return output;
    });
    // <--[tag]
    // @attribute <LocationTag.highest>
    // @returns LocationTag
    // @group world
    // @description
    // Returns the location of the highest solid block at the location.
    // -->
    tagProcessor.registerTag(LocationTag.class, "highest", (attribute, object) -> {
        Location result = object.getHighestBlockForTag(attribute);
        return new LocationTag(result);
    });
    // <--[tag]
    // @attribute <LocationTag.has_inventory>
    // @returns ElementTag(Boolean)
    // @group world
    // @description
    // Returns whether the block at the location has an inventory.
    // -->
    tagProcessor.registerTag(ElementTag.class, "has_inventory", (attribute, object) -> {
        return new ElementTag(object.getBlockStateForTag(attribute) instanceof InventoryHolder);
    });
    // <--[tag]
    // @attribute <LocationTag.inventory>
    // @returns InventoryTag
    // @group world
    // @description
    // Returns the InventoryTag of the block at the location. If the
    // block is not a container, returns null.
    // -->
    tagProcessor.registerTag(InventoryTag.class, "inventory", (attribute, object) -> {
        if (!object.isChunkLoadedSafe()) {
            return null;
        }
        return ElementTag.handleNull(object.identify() + ".inventory", object.getInventory(), "InventoryTag", attribute.hasAlternative());
    });
    // <--[tag]
    // @attribute <LocationTag.material>
    // @returns MaterialTag
    // @group world
    // @description
    // Returns the material of the block at the location.
    // -->
    tagProcessor.registerTag(MaterialTag.class, "material", (attribute, object) -> {
        Block block = object.getBlockForTag(attribute);
        if (block == null) {
            return null;
        }
        return new MaterialTag(block);
    });
    // <--[tag]
    // @attribute <LocationTag.patterns>
    // @returns ListTag
    // @mechanism LocationTag.patterns
    // @group world
    // @description
    // Lists the patterns of the banner at this location in the form "COLOR/PATTERN|COLOR/PATTERN" etc.
    // For the list of possible colors, see <@link url https://hub.spigotmc.org/javadocs/spigot/org/bukkit/DyeColor.html>.
    // For the list of possible patterns, see <@link url https://hub.spigotmc.org/javadocs/spigot/org/bukkit/block/banner/PatternType.html>.
    // -->
    tagProcessor.registerTag(ListTag.class, "patterns", (attribute, object) -> {
        ListTag list = new ListTag();
        for (org.bukkit.block.banner.Pattern pattern : ((Banner) object.getBlockStateForTag(attribute)).getPatterns()) {
            list.add(pattern.getColor().name() + "/" + pattern.getPattern().name());
        }
        return list;
    });
    // <--[tag]
    // @attribute <LocationTag.head_rotation>
    // @returns ElementTag(Number)
    // @mechanism LocationTag.head_rotation
    // @group world
    // @description
    // Gets the rotation of the head at this location. Can be 1-16.
    // -->
    tagProcessor.registerTag(ElementTag.class, "head_rotation", (attribute, object) -> {
        return new ElementTag(object.getSkullRotation(((Skull) object.getBlockStateForTag(attribute)).getRotation()) + 1);
    });
    // <--[tag]
    // @attribute <LocationTag.switched>
    // @returns ElementTag(Boolean)
    // @group world
    // @description
    // Returns whether the block at the location is considered to be switched on.
    // (For buttons, levers, etc.)
    // To change this, see <@link command Switch>
    // -->
    tagProcessor.registerTag(ElementTag.class, "switched", (attribute, object) -> {
        return new ElementTag(SwitchCommand.switchState(object.getBlockForTag(attribute)));
    });
    // <--[tag]
    // @attribute <LocationTag.sign_contents>
    // @returns ListTag
    // @mechanism LocationTag.sign_contents
    // @group world
    // @description
    // Returns a list of lines on a sign.
    // -->
    tagProcessor.registerTag(ListTag.class, "sign_contents", (attribute, object) -> {
        if (object.getBlockStateForTag(attribute) instanceof Sign) {
            return new ListTag(Arrays.asList(AdvancedTextImpl.instance.getSignLines(((Sign) object.getBlockStateForTag(attribute)))));
        } else {
            return null;
        }
    });
    // <--[tag]
    // @attribute <LocationTag.spawner_type>
    // @returns EntityTag
    // @mechanism LocationTag.spawner_type
    // @group world
    // @description
    // Returns the type of entity spawned by a mob spawner.
    // -->
    tagProcessor.registerTag(EntityTag.class, "spawner_type", (attribute, object) -> {
        if (!(object.getBlockStateForTag(attribute) instanceof CreatureSpawner)) {
            return null;
        }
        return new EntityTag(DenizenEntityType.getByName(((CreatureSpawner) object.getBlockStateForTag(attribute)).getSpawnedType().name()));
    });
    // <--[tag]
    // @attribute <LocationTag.spawner_display_entity>
    // @returns EntityTag
    // @group world
    // @description
    // Returns the full "display entity" for the spawner. This can contain more data than just a type.
    // -->
    tagProcessor.registerTag(EntityTag.class, "spawner_display_entity", (attribute, object) -> {
        if (!(object.getBlockStateForTag(attribute) instanceof CreatureSpawner)) {
            return null;
        }
        return NMSHandler.getEntityHelper().getMobSpawnerDisplayEntity(((CreatureSpawner) object.getBlockStateForTag(attribute))).describe(attribute.context);
    });
    // <--[tag]
    // @attribute <LocationTag.spawner_spawn_delay>
    // @returns ElementTag(Number)
    // @mechanism LocationTag.spawner_delay_data
    // @group world
    // @description
    // Returns the current spawn delay for the spawner.
    // This changes over time between <@link tag LocationTag.spawner_minimum_spawn_delay> and <@link tag LocationTag.spawner_maximum_spawn_delay>.
    // -->
    tagProcessor.registerTag(ElementTag.class, "spawner_spawn_delay", (attribute, object) -> {
        if (!(object.getBlockStateForTag(attribute) instanceof CreatureSpawner)) {
            return null;
        }
        return new ElementTag(((CreatureSpawner) object.getBlockStateForTag(attribute)).getDelay());
    });
    // <--[tag]
    // @attribute <LocationTag.spawner_minimum_spawn_delay>
    // @returns ElementTag(Number)
    // @mechanism LocationTag.spawner_delay_data
    // @group world
    // @description
    // Returns the minimum spawn delay for the mob spawner.
    // -->
    tagProcessor.registerTag(ElementTag.class, "spawner_minimum_spawn_delay", (attribute, object) -> {
        if (!(object.getBlockStateForTag(attribute) instanceof CreatureSpawner)) {
            return null;
        }
        return new ElementTag(((CreatureSpawner) object.getBlockStateForTag(attribute)).getMinSpawnDelay());
    });
    // <--[tag]
    // @attribute <LocationTag.spawner_maximum_spawn_delay>
    // @returns ElementTag(Number)
    // @mechanism LocationTag.spawner_delay_data
    // @group world
    // @description
    // Returns the maximum spawn delay for the mob spawner.
    // -->
    tagProcessor.registerTag(ElementTag.class, "spawner_maximum_spawn_delay", (attribute, object) -> {
        if (!(object.getBlockStateForTag(attribute) instanceof CreatureSpawner)) {
            return null;
        }
        return new ElementTag(((CreatureSpawner) object.getBlockStateForTag(attribute)).getMaxSpawnDelay());
    });
    // <--[tag]
    // @attribute <LocationTag.spawner_player_range>
    // @returns ElementTag(Number)
    // @mechanism LocationTag.spawner_player_range
    // @group world
    // @description
    // Returns the maximum player range for the spawner (ie how close a player must be for this spawner to be active).
    // -->
    tagProcessor.registerTag(ElementTag.class, "spawner_player_range", (attribute, object) -> {
        if (!(object.getBlockStateForTag(attribute) instanceof CreatureSpawner)) {
            return null;
        }
        return new ElementTag(((CreatureSpawner) object.getBlockStateForTag(attribute)).getRequiredPlayerRange());
    });
    // <--[tag]
    // @attribute <LocationTag.spawner_range>
    // @returns ElementTag(Number)
    // @mechanism LocationTag.spawner_range
    // @group world
    // @description
    // Returns the spawn range for the spawner (the radius mobs will spawn in).
    // -->
    tagProcessor.registerTag(ElementTag.class, "spawner_range", (attribute, object) -> {
        if (!(object.getBlockStateForTag(attribute) instanceof CreatureSpawner)) {
            return null;
        }
        return new ElementTag(((CreatureSpawner) object.getBlockStateForTag(attribute)).getSpawnRange());
    });
    // <--[tag]
    // @attribute <LocationTag.spawner_max_nearby_entities>
    // @returns ElementTag(Number)
    // @mechanism LocationTag.spawner_max_nearby_entities
    // @group world
    // @description
    // Returns the maximum nearby entities for the spawner (the radius mobs will spawn in).
    // -->
    tagProcessor.registerTag(ElementTag.class, "spawner_max_nearby_entities", (attribute, object) -> {
        if (!(object.getBlockStateForTag(attribute) instanceof CreatureSpawner)) {
            return null;
        }
        return new ElementTag(((CreatureSpawner) object.getBlockStateForTag(attribute)).getMaxNearbyEntities());
    });
    // <--[tag]
    // @attribute <LocationTag.spawner_count>
    // @returns ElementTag(Number)
    // @mechanism LocationTag.spawner_count
    // @group world
    // @description
    // Returns the spawn count for the spawner.
    // -->
    tagProcessor.registerTag(ElementTag.class, "spawner_count", (attribute, object) -> {
        if (!(object.getBlockStateForTag(attribute) instanceof CreatureSpawner)) {
            return null;
        }
        return new ElementTag(((CreatureSpawner) object.getBlockStateForTag(attribute)).getSpawnCount());
    });
    // <--[tag]
    // @attribute <LocationTag.lock>
    // @returns ElementTag
    // @mechanism LocationTag.lock
    // @group world
    // @description
    // Returns the password to a locked container.
    // -->
    tagProcessor.registerTag(ElementTag.class, "lock", (attribute, object) -> {
        if (!(object.getBlockStateForTag(attribute) instanceof Lockable)) {
            return null;
        }
        Lockable lock = (Lockable) object.getBlockStateForTag(attribute);
        return new ElementTag(lock.isLocked() ? lock.getLock() : null);
    });
    // <--[tag]
    // @attribute <LocationTag.is_locked>
    // @returns ElementTag(Boolean)
    // @mechanism LocationTag.lock
    // @group world
    // @description
    // Returns whether the container is locked.
    // -->
    tagProcessor.registerTag(ElementTag.class, "is_locked", (attribute, object) -> {
        if (!(object.getBlockStateForTag(attribute) instanceof Lockable)) {
            return null;
        }
        return new ElementTag(((Lockable) object.getBlockStateForTag(attribute)).isLocked());
    });
    // <--[tag]
    // @attribute <LocationTag.is_lockable>
    // @returns ElementTag(Boolean)
    // @mechanism LocationTag.lock
    // @group world
    // @description
    // Returns whether the container is lockable.
    // -->
    tagProcessor.registerTag(ElementTag.class, "is_lockable", (attribute, object) -> {
        return new ElementTag(object.getBlockStateForTag(attribute) instanceof Lockable);
    });
    // <--[tag]
    // @attribute <LocationTag.drops[(<item>)]>
    // @returns ListTag(ItemTag)
    // @group world
    // @description
    // Returns what items the block at the location would drop if broken naturally.
    // Optionally specifier a breaker item.
    // Not guaranteed to contain exactly correct or contain all possible drops (for things like plants that drop only when grown, ores that drop random amounts, etc).
    // -->
    tagProcessor.registerTag(ListTag.class, "drops", (attribute, object) -> {
        ItemStack inputItem = null;
        if (attribute.hasParam()) {
            inputItem = attribute.paramAsType(ItemTag.class).getItemStack();
        }
        ListTag list = new ListTag();
        for (ItemStack it : object.getDropsForTag(attribute, inputItem)) {
            list.addObject(new ItemTag(it));
        }
        return list;
    });
    // <--[tag]
    // @attribute <LocationTag.xp_drop[(<item>)]>
    // @returns ElementTag(Number)
    // @group world
    // @description
    // Returns how much experience, if any, the block at the location would drop if broken naturally.
    // Returns 0 if a block wouldn't drop xp.
    // Optionally specifier a breaker item.
    // Not guaranteed to contain exactly the amount that actual drops if then broken later, as the value is usually randomized.
    // -->
    tagProcessor.registerTag(ElementTag.class, "xp_drop", (attribute, object) -> {
        ItemStack inputItem = new ItemStack(Material.AIR);
        if (attribute.hasParam()) {
            inputItem = attribute.paramAsType(ItemTag.class).getItemStack();
        }
        return new ElementTag(object.getExpDropForTag(attribute, inputItem));
    });
    // <--[tag]
    // @attribute <LocationTag.hive_bee_count>
    // @returns ElementTag(Number)
    // @group world
    // @description
    // Returns the number of bees inside a hive.
    // -->
    tagProcessor.registerTag(ElementTag.class, "hive_bee_count", (attribute, object) -> {
        return new ElementTag(((Beehive) object.getBlockStateForTag(attribute)).getEntityCount());
    });
    // <--[tag]
    // @attribute <LocationTag.hive_max_bees>
    // @returns ElementTag(Number)
    // @mechanism LocationTag.hive_max_bees
    // @group world
    // @description
    // Returns the maximum number of bees allowed inside a hive.
    // -->
    tagProcessor.registerTag(ElementTag.class, "hive_max_bees", (attribute, object) -> {
        return new ElementTag(((Beehive) object.getBlockStateForTag(attribute)).getMaxEntities());
    });
    // <--[tag]
    // @attribute <LocationTag.skull_type>
    // @returns ElementTag
    // @group world
    // @description
    // Returns the type of the skull.
    // -->
    tagProcessor.registerTag(ElementTag.class, "skull_type", (attribute, object) -> {
        BlockState blockState = object.getBlockStateForTag(attribute);
        if (blockState instanceof Skull) {
            String t = ((Skull) blockState).getSkullType().name();
            return new ElementTag(t);
        }
        return null;
    });
    // <--[tag]
    // @attribute <LocationTag.skull_name>
    // @returns ElementTag
    // @mechanism LocationTag.skull_skin
    // @group world
    // @description
    // Returns the name of the skin the skull is displaying.
    // -->
    tagProcessor.registerTag(ElementTag.class, "skull_name", (attribute, object) -> {
        BlockState blockState = object.getBlockStateForTag(attribute);
        if (blockState instanceof Skull) {
            PlayerProfile profile = NMSHandler.getBlockHelper().getPlayerProfile((Skull) blockState);
            if (profile == null) {
                return null;
            }
            String n = profile.getName();
            if (n == null) {
                n = ((Skull) blockState).getOwningPlayer().getName();
            }
            return new ElementTag(n);
        }
        return null;
    });
    // <--[tag]
    // @attribute <LocationTag.skull_skin>
    // @returns ElementTag
    // @mechanism LocationTag.skull_skin
    // @group world
    // @description
    // Returns the skin the skull is displaying - just the name or UUID as text, not a player object.
    // -->
    tagProcessor.registerTag(ElementTag.class, "skull_skin", (attribute, object) -> {
        BlockState blockState = object.getBlockStateForTag(attribute);
        if (blockState instanceof Skull) {
            PlayerProfile profile = NMSHandler.getBlockHelper().getPlayerProfile((Skull) blockState);
            if (profile == null) {
                return null;
            }
            String name = profile.getName();
            UUID uuid = profile.getUniqueId();
            String texture = profile.getTexture();
            // -->
            if (attribute.startsWith("full", 2)) {
                attribute.fulfill(1);
                return new ElementTag((uuid != null ? uuid : name) + (texture != null ? "|" + texture : ""));
            }
            return new ElementTag(uuid != null ? uuid.toString() : name);
        } else {
            return null;
        }
    });
    // <--[tag]
    // @attribute <LocationTag.round>
    // @returns LocationTag
    // @group math
    // @description
    // Returns a rounded version of the LocationTag's coordinates.
    // That is, each component (X, Y, Z, Yaw, Pitch) is rounded
    // (eg, 0.1 becomes 0.0, 0.5 becomes 1.0, 0.9 becomes 1.0).
    // This is NOT equivalent to the block coordinates. For that, use <@link tag LocationTag.round_down>.
    // -->
    tagProcessor.registerTag(LocationTag.class, "round", (attribute, object) -> {
        LocationTag result = object.clone();
        result.setX(Math.round(result.getX()));
        result.setY(Math.round(result.getY()));
        result.setZ(Math.round(result.getZ()));
        result.setYaw(Math.round(result.getYaw()));
        result.setPitch(Math.round(result.getPitch()));
        return result;
    });
    // <--[tag]
    // @attribute <LocationTag.round_up>
    // @returns LocationTag
    // @group math
    // @description
    // Returns a rounded-upward version of the LocationTag's coordinates.
    // That is, each component (X, Y, Z, Yaw, Pitch) is rounded upward
    // (eg, 0.1 becomes 1.0, 0.5 becomes 1.0, 0.9 becomes 1.0).
    // -->
    tagProcessor.registerTag(LocationTag.class, "round_up", (attribute, object) -> {
        LocationTag result = object.clone();
        result.setX(Math.ceil(result.getX()));
        result.setY(Math.ceil(result.getY()));
        result.setZ(Math.ceil(result.getZ()));
        result.setYaw((float) Math.ceil((result.getYaw())));
        result.setPitch((float) Math.ceil(result.getPitch()));
        return result;
    });
    // <--[tag]
    // @attribute <LocationTag.round_down>
    // @returns LocationTag
    // @group math
    // @description
    // Returns a rounded-downward version of the LocationTag's coordinates.
    // That is, each component (X, Y, Z, Yaw, Pitch) is rounded downward
    // (eg, 0.1 becomes 0.0, 0.5 becomes 0.0, 0.9 becomes 0.0).
    // This is equivalent to the block coordinates of the location.
    // -->
    tagProcessor.registerTag(LocationTag.class, "round_down", (attribute, object) -> {
        LocationTag result = object.clone();
        result.setX(Math.floor(result.getX()));
        result.setY(Math.floor(result.getY()));
        result.setZ(Math.floor(result.getZ()));
        result.setYaw((float) Math.floor((result.getYaw())));
        result.setPitch((float) Math.floor(result.getPitch()));
        return result;
    });
    // <--[tag]
    // @attribute <LocationTag.round_to[<#>]>
    // @returns LocationTag
    // @group math
    // @description
    // Returns a rounded-to-precision version of the LocationTag's coordinates.
    // That is, each component (X, Y, Z, Yaw, Pitch) is rounded to the specified decimal place
    // (eg, 0.12345 .round_to[3] returns "0.123").
    // -->
    tagProcessor.registerTag(LocationTag.class, "round_to", (attribute, object) -> {
        if (!attribute.hasParam()) {
            attribute.echoError("The tag LocationTag.round_to[...] must have a value.");
            return null;
        }
        LocationTag result = object.clone();
        int ten = (int) Math.pow(10, attribute.getIntParam());
        result.setX(((double) Math.round(result.getX() * ten)) / ten);
        result.setY(((double) Math.round(result.getY() * ten)) / ten);
        result.setZ(((double) Math.round(result.getZ() * ten)) / ten);
        result.setYaw(((float) Math.round(result.getYaw() * ten)) / ten);
        result.setPitch(((float) Math.round(result.getPitch() * ten)) / ten);
        return result;
    });
    // <--[tag]
    // @attribute <LocationTag.round_to_precision[<#.#>]>
    // @returns LocationTag
    // @group math
    // @description
    // Returns a rounded-to-precision version of the LocationTag's coordinates.
    // That is, each component (X, Y, Z, Yaw, Pitch) is rounded to the specified precision value
    // (0.12345 .round_to_precision[0.005] returns "0.125").
    // -->
    tagProcessor.registerTag(LocationTag.class, "round_to_precision", (attribute, object) -> {
        if (!attribute.hasParam()) {
            attribute.echoError("The tag LocationTag.round_to_precision[...] must have a value.");
            return null;
        }
        LocationTag result = object.clone();
        float precision = 1f / (float) attribute.getDoubleParam();
        result.setX(((double) Math.round(result.getX() * precision)) / precision);
        result.setY(((double) Math.round(result.getY() * precision)) / precision);
        result.setZ(((double) Math.round(result.getZ() * precision)) / precision);
        result.setYaw(((float) Math.round(result.getYaw() * precision)) / precision);
        result.setPitch(((float) Math.round(result.getPitch() * precision)) / precision);
        return result;
    });
    // <--[tag]
    // @attribute <LocationTag.simple>
    // @returns ElementTag
    // @group identity
    // @description
    // Returns a simple version of the LocationTag's block coordinates.
    // In the format: x,y,z,world
    // For example: 1,2,3,world_nether
    // -->
    tagProcessor.registerTag(ElementTag.class, "simple", (attribute, object) -> {
        // -->
        if (attribute.startsWith("formatted", 2)) {
            attribute.fulfill(1);
            return new ElementTag("X '" + object.getBlockX() + "', Y '" + object.getBlockY() + "', Z '" + object.getBlockZ() + "', in world '" + object.getWorldName() + "'");
        }
        if (object.getWorldName() == null) {
            return new ElementTag(object.getBlockX() + "," + object.getBlockY() + "," + object.getBlockZ());
        } else {
            return new ElementTag(object.getBlockX() + "," + object.getBlockY() + "," + object.getBlockZ() + "," + object.getWorldName());
        }
    });
    // <--[tag]
    // @attribute <LocationTag.precise_impact_normal[(<range>)]>
    // @returns LocationTag
    // @group world
    // @description
    // Returns the exact impact normal at the location this location is pointing at.
    // In minecraft, the impact normal is generally the side of the block that the location is facing.
    // Optionally, specify a maximum range to find the location from (defaults to 200).
    // -->
    tagProcessor.registerTag(LocationTag.class, "precise_impact_normal", (attribute, object) -> {
        double range = attribute.getDoubleParam();
        if (range <= 0) {
            range = 200;
        }
        RayTraceResult traced = object.getWorld().rayTraceBlocks(object, object.getDirection(), range);
        if (traced != null && traced.getHitBlockFace() != null) {
            return new LocationTag(traced.getHitBlockFace().getDirection());
        }
        return null;
    });
    // <--[tag]
    // @attribute <LocationTag.precise_cursor_on_block[(<range>)]>
    // @returns LocationTag
    // @group world
    // @description
    // Returns the block location this location is pointing at.
    // Optionally, specify a maximum range to find the location from (defaults to 200).
    // -->
    tagProcessor.registerTag(LocationTag.class, "precise_cursor_on_block", (attribute, object) -> {
        double range = attribute.getDoubleParam();
        if (range <= 0) {
            range = 200;
        }
        RayTraceResult traced = object.getWorld().rayTraceBlocks(object, object.getDirection(), range);
        if (traced != null && traced.getHitBlock() != null) {
            return new LocationTag(traced.getHitBlock().getLocation());
        }
        return null;
    });
    // <--[tag]
    // @attribute <LocationTag.precise_cursor_on[(<range>)]>
    // @returns LocationTag
    // @group world
    // @description
    // Returns the exact location this location is pointing at.
    // Optionally, specify a maximum range to find the location from (defaults to 200).
    // -->
    tagProcessor.registerTag(LocationTag.class, "precise_cursor_on", (attribute, object) -> {
        double range = attribute.getDoubleParam();
        if (range <= 0) {
            range = 200;
        }
        RayTraceResult traced = object.getWorld().rayTraceBlocks(object, object.getDirection(), range);
        if (traced != null && traced.getHitBlock() != null) {
            return new LocationTag(traced.getHitBlock().getWorld(), traced.getHitPosition());
        }
        return null;
    });
    // <--[tag]
    // @attribute <LocationTag.precise_target_list[<range>]>
    // @returns ListTag(EntityTag)
    // @group world
    // @description
    // Returns a list of all entities this location is pointing directly at (using precise ray trace logic), up to a given range limit.
    // -->
    tagProcessor.registerTag(ListTag.class, "precise_target_list", (attribute, object) -> {
        if (!attribute.hasParam()) {
            return null;
        }
        double range = attribute.getDoubleParam();
        HashSet<UUID> hitIDs = new HashSet<>();
        ListTag result = new ListTag();
        Vector direction = object.getDirection();
        World world = object.getWorld();
        while (true) {
            RayTraceResult hit = world.rayTrace(object, direction, range, FluidCollisionMode.NEVER, true, 0, (e) -> !hitIDs.contains(e.getUniqueId()));
            if (hit == null || hit.getHitEntity() == null) {
                return result;
            }
            hitIDs.add(hit.getHitEntity().getUniqueId());
            result.addObject(new EntityTag(hit.getHitEntity()));
        }
    });
    // <--[tag]
    // @attribute <LocationTag.precise_target[(<range>)]>
    // @returns EntityTag
    // @group world
    // @description
    // Returns the entity this location is pointing at, using precise ray trace logic.
    // Optionally, specify a maximum range to find the entity from (defaults to 100).
    // -->
    tagProcessor.registerTag(EntityTag.class, "precise_target", (attribute, object) -> {
        double range = attribute.getDoubleParam();
        if (range <= 0) {
            range = 100;
        }
        RayTraceResult result;
        // -->
        if (attribute.startsWith("type", 2) && attribute.hasContext(2)) {
            attribute.fulfill(1);
            Set<EntityType> types = new HashSet<>();
            for (String str : attribute.paramAsType(ListTag.class)) {
                types.add(EntityTag.valueOf(str, attribute.context).getBukkitEntityType());
            }
            result = object.getWorld().rayTrace(object, object.getDirection(), range, FluidCollisionMode.NEVER, true, 0, (e) -> types.contains(e.getType()));
        } else {
            result = object.getWorld().rayTrace(object, object.getDirection(), range, FluidCollisionMode.NEVER, true, 0, null);
        }
        if (result != null && result.getHitEntity() != null) {
            return new EntityTag(result.getHitEntity());
        }
        return null;
    });
    // <--[tag]
    // @attribute <LocationTag.precise_target_position[(<range>)]>
    // @returns LocationTag
    // @group world
    // @description
    // Returns the precise location this location is pointing at, when tracing against entities.
    // Optionally, specify a maximum range to find the entity from (defaults to 100).
    // -->
    tagProcessor.registerTag(LocationTag.class, "precise_target_position", (attribute, object) -> {
        double range = attribute.getDoubleParam();
        if (range <= 0) {
            range = 100;
        }
        RayTraceResult result;
        // -->
        if (attribute.startsWith("type", 2) && attribute.hasContext(2)) {
            attribute.fulfill(1);
            Set<EntityType> types = new HashSet<>();
            for (String str : attribute.paramAsType(ListTag.class)) {
                types.add(EntityTag.valueOf(str, attribute.context).getBukkitEntityType());
            }
            result = object.getWorld().rayTrace(object, object.getDirection(), range, FluidCollisionMode.NEVER, true, 0, (e) -> types.contains(e.getType()));
        } else {
            result = object.getWorld().rayTrace(object, object.getDirection(), range, FluidCollisionMode.NEVER, true, 0, null);
        }
        if (result != null) {
            return new LocationTag(object.getWorld(), result.getHitPosition());
        }
        return null;
    });
    // <--[tag]
    // @attribute <LocationTag.points_between[<location>]>
    // @returns ListTag(LocationTag)
    // @group math
    // @description
    // Finds all locations between this location and another, separated by 1 block-width each.
    // -->
    tagProcessor.registerTag(ListTag.class, "points_between", (attribute, object) -> {
        LocationTag target = attribute.paramAsType(LocationTag.class);
        if (target == null) {
            return null;
        }
        // <--[tag]
        // @attribute <LocationTag.points_between[<location>].distance[<#.#>]>
        // @returns ListTag(LocationTag)
        // @group math
        // @description
        // Finds all locations between this location and another, separated by the specified distance each.
        // -->
        double rad = 1d;
        if (attribute.startsWith("distance", 2)) {
            rad = attribute.getDoubleContext(2);
            attribute.fulfill(1);
        }
        ListTag list = new ListTag();
        org.bukkit.util.Vector rel = target.toVector().subtract(object.toVector());
        double len = rel.length();
        if (len < 0.0001) {
            return new ListTag();
        }
        rel = rel.multiply(1d / len);
        for (double i = 0d; i <= len; i += rad) {
            list.addObject(new LocationTag(object.clone().add(rel.clone().multiply(i))));
        }
        return list;
    });
    // <--[tag]
    // @attribute <LocationTag.facing_blocks[(<#>)]>
    // @returns ListTag(LocationTag)
    // @group world
    // @description
    // Finds all block locations in the direction this location is facing,
    // optionally with a custom range (default is 100).
    // For example a location at 0,0,0 facing straight up
    // will include 0,1,0 0,2,0 and so on.
    // This is an imperfect block line tracer.
    // -->
    tagProcessor.registerTag(ListTag.class, "facing_blocks", (attribute, object) -> {
        int range = attribute.getIntParam();
        if (range < 1) {
            range = 100;
        }
        ListTag list = new ListTag();
        try {
            NMSHandler.getChunkHelper().changeChunkServerThread(object.getWorld());
            BlockIterator iterator = new BlockIterator(object, 0, range);
            while (iterator.hasNext()) {
                list.addObject(new LocationTag(iterator.next().getLocation()));
            }
        } finally {
            NMSHandler.getChunkHelper().restoreServerThread(object.getWorld());
        }
        return list;
    });
    // <--[tag]
    // @attribute <LocationTag.line_of_sight[<location>]>
    // @returns ElementTag(Boolean)
    // @group math
    // @description
    // Returns whether the specified location is within this location's line of sight.
    // -->
    tagProcessor.registerTag(ElementTag.class, "line_of_sight", (attribute, object) -> {
        if (!attribute.hasParam()) {
            return null;
        }
        LocationTag location = attribute.paramAsType(LocationTag.class);
        if (location != null) {
            try {
                NMSHandler.getChunkHelper().changeChunkServerThread(object.getWorld());
                return new ElementTag(NMSHandler.getEntityHelper().canTrace(object.getWorld(), object.toVector(), location.toVector()));
            } finally {
                NMSHandler.getChunkHelper().restoreServerThread(object.getWorld());
            }
        }
        return null;
    });
    // <--[tag]
    // @attribute <LocationTag.direction[(<location>)]>
    // @returns ElementTag
    // @group math
    // @description
    // Returns the compass direction between two locations.
    // If no second location is specified, returns the direction of the location.
    // Example returns include "north", "southwest", ...
    // -->
    tagProcessor.registerTag(ObjectTag.class, "direction", (attribute, object) -> {
        // -->
        if (attribute.startsWith("vector", 2)) {
            attribute.fulfill(1);
            return new LocationTag(object.getWorld(), object.getDirection());
        }
        // Get the cardinal direction from this location to another
        if (attribute.hasParam() && LocationTag.matches(attribute.getParam())) {
            // Subtract this location's vector from the other location's vector,
            // not the other way around
            LocationTag target = attribute.paramAsType(LocationTag.class);
            EntityHelper entityHelper = NMSHandler.getEntityHelper();
            // -->
            if (attribute.startsWith("yaw", 2)) {
                attribute.fulfill(1);
                return new ElementTag(EntityHelper.normalizeYaw(entityHelper.getYaw(target.toVector().subtract(object.toVector()).normalize())));
            } else {
                return new ElementTag(entityHelper.getCardinal(entityHelper.getYaw(target.toVector().subtract(object.toVector()).normalize())));
            }
        } else // Get a cardinal direction from this location's yaw
        {
            return new ElementTag(NMSHandler.getEntityHelper().getCardinal(object.getYaw()));
        }
    });
    // <--[tag]
    // @attribute <LocationTag.rotate_yaw[<#.#>]>
    // @returns LocationTag
    // @group math
    // @description
    // Returns the location with the yaw rotated the specified amount (eg 180 to face the location backwards).
    // -->
    tagProcessor.registerTag(LocationTag.class, "rotate_yaw", (attribute, object) -> {
        LocationTag loc = LocationTag.valueOf(object.identify(), attribute.context).clone();
        loc.setYaw(loc.getYaw() + (float) attribute.getDoubleParam());
        return loc;
    });
    // <--[tag]
    // @attribute <LocationTag.rotate_pitch[<#.#>]>
    // @returns LocationTag
    // @group math
    // @description
    // Returns the location with the pitch rotated the specified amount. Note that this is capped to +/- 90.
    // -->
    tagProcessor.registerTag(LocationTag.class, "rotate_pitch", (attribute, object) -> {
        LocationTag loc = LocationTag.valueOf(object.identify(), attribute.context).clone();
        loc.setPitch(Math.max(-90, Math.min(90, loc.getPitch() + (float) attribute.getDoubleParam())));
        return loc;
    });
    // <--[tag]
    // @attribute <LocationTag.face[<location>]>
    // @returns LocationTag
    // @group math
    // @description
    // Returns a location containing a yaw/pitch that point from the current location
    // to the target location.
    // -->
    tagProcessor.registerTag(LocationTag.class, "face", (attribute, object) -> {
        if (!attribute.hasParam()) {
            return null;
        }
        Location two = attribute.paramAsType(LocationTag.class);
        return new LocationTag(NMSHandler.getEntityHelper().faceLocation(object, two));
    });
    // <--[tag]
    // @attribute <LocationTag.facing[<entity>/<location>]>
    // @returns ElementTag(Boolean)
    // @group math
    // @description
    // Returns whether the location's yaw is facing another entity or location, within a limit of 45 degrees of yaw.
    // -->
    tagProcessor.registerTag(ElementTag.class, "facing", (attribute, object) -> {
        if (attribute.hasParam()) {
            // The default number of degrees if there is no degrees attribute
            int degrees = 45;
            LocationTag facingLoc;
            if (LocationTag.matches(attribute.getParam())) {
                facingLoc = attribute.paramAsType(LocationTag.class);
            } else if (EntityTag.matches(attribute.getParam())) {
                facingLoc = attribute.paramAsType(EntityTag.class).getLocation();
            } else {
                if (!attribute.hasAlternative()) {
                    Debug.echoError("Tag location.facing[...] was given an invalid facing target.");
                }
                return null;
            }
            // -->
            if (attribute.startsWith("degrees", 2) && attribute.hasContext(2)) {
                String context = attribute.getContext(2);
                attribute.fulfill(1);
                if (context.contains(",")) {
                    String yaw = context.substring(0, context.indexOf(','));
                    String pitch = context.substring(context.indexOf(',') + 1);
                    degrees = Integer.parseInt(yaw);
                    int pitchDegrees = Integer.parseInt(pitch);
                    return new ElementTag(NMSHandler.getEntityHelper().isFacingLocation(object, facingLoc, degrees, pitchDegrees));
                } else {
                    degrees = Integer.parseInt(context);
                }
            }
            return new ElementTag(NMSHandler.getEntityHelper().isFacingLocation(object, facingLoc, degrees));
        }
        return null;
    });
    // <--[tag]
    // @attribute <LocationTag.pitch>
    // @returns ElementTag(Decimal)
    // @group identity
    // @description
    // Returns the pitch of the object at the location.
    // -->
    tagProcessor.registerTag(ElementTag.class, "pitch", (attribute, object) -> {
        return new ElementTag(object.getPitch());
    });
    // <--[tag]
    // @attribute <LocationTag.with_pose[<entity>/<pitch>,<yaw>]>
    // @returns LocationTag
    // @group identity
    // @description
    // Returns the location with pitch and yaw.
    // -->
    tagProcessor.registerTag(LocationTag.class, "with_pose", (attribute, object) -> {
        String context = attribute.getParam();
        float pitch = 0f;
        float yaw = 0f;
        if (EntityTag.matches(context)) {
            EntityTag ent = EntityTag.valueOf(context, attribute.context);
            if (ent.isSpawnedOrValidForTag()) {
                pitch = ent.getBukkitEntity().getLocation().getPitch();
                yaw = ent.getBukkitEntity().getLocation().getYaw();
            }
        } else if (context.split(",").length == 2) {
            String[] split = context.split(",");
            pitch = Float.parseFloat(split[0]);
            yaw = Float.parseFloat(split[1]);
        }
        LocationTag loc = object.clone();
        loc.setPitch(pitch);
        loc.setYaw(yaw);
        return loc;
    });
    // <--[tag]
    // @attribute <LocationTag.yaw>
    // @returns ElementTag(Decimal)
    // @group identity
    // @description
    // Returns the normalized yaw of the object at the location.
    // -->
    tagProcessor.registerTag(ElementTag.class, "yaw", (attribute, object) -> {
        // -->
        if (attribute.startsWith("simple", 2)) {
            attribute.fulfill(1);
            float yaw = EntityHelper.normalizeYaw(object.getYaw());
            if (yaw < 45) {
                return new ElementTag("South");
            } else if (yaw < 135) {
                return new ElementTag("West");
            } else if (yaw < 225) {
                return new ElementTag("North");
            } else if (yaw < 315) {
                return new ElementTag("East");
            } else {
                return new ElementTag("South");
            }
        }
        // -->
        if (attribute.startsWith("raw", 2)) {
            attribute.fulfill(1);
            return new ElementTag(object.getYaw());
        }
        return new ElementTag(EntityHelper.normalizeYaw(object.getYaw()));
    });
    // <--[tag]
    // @attribute <LocationTag.rotate_around_x[<#.#>]>
    // @returns LocationTag
    // @group math
    // @description
    // Returns the location-vector rotated around the x axis by a specified angle in radians.
    // Generally used in a format like <player.location.add[<location[0,1,0].rotate_around_x[<[some_angle]>]>]>.
    // -->
    tagProcessor.registerTag(LocationTag.class, "rotate_around_x", (attribute, object) -> {
        if (!attribute.hasParam()) {
            return null;
        }
        double[] values = getRotatedAroundX(attribute.getDoubleParam(), object.getY(), object.getZ());
        Location location = object.clone();
        location.setY(values[0]);
        location.setZ(values[1]);
        return new LocationTag(location);
    });
    // <--[tag]
    // @attribute <LocationTag.rotate_around_y[<#.#>]>
    // @returns LocationTag
    // @group math
    // @description
    // Returns the location-vector rotated around the y axis by a specified angle in radians.
    // Generally used in a format like <player.location.add[<location[1,0,0].rotate_around_y[<[some_angle]>]>]>.
    // -->
    tagProcessor.registerTag(LocationTag.class, "rotate_around_y", (attribute, object) -> {
        if (!attribute.hasParam()) {
            return null;
        }
        double[] values = getRotatedAroundY(attribute.getDoubleParam(), object.getX(), object.getZ());
        Location location = object.clone();
        location.setX(values[0]);
        location.setZ(values[1]);
        return new LocationTag(location);
    });
    // <--[tag]
    // @attribute <LocationTag.rotate_around_z[<#.#>]>
    // @returns LocationTag
    // @group math
    // @description
    // Returns the location-vector rotated around the z axis by a specified angle in radians.
    // Generally used in a format like <player.location.add[<location[1,0,0].rotate_around_z[<[some_angle]>]>]>.
    // -->
    tagProcessor.registerTag(LocationTag.class, "rotate_around_z", (attribute, object) -> {
        if (!attribute.hasParam()) {
            return null;
        }
        double[] values = getRotatedAroundZ(attribute.getDoubleParam(), object.getX(), object.getY());
        Location location = object.clone();
        location.setX(values[0]);
        location.setY(values[1]);
        return new LocationTag(location);
    });
    // <--[tag]
    // @attribute <LocationTag.points_around_x[radius=<#.#>;points=<#>]>
    // @returns ListTag(LocationTag)
    // @group math
    // @description
    // Returns a list of points in a circle around a location's x axis with the specified radius and number of points.
    // For example: <player.location.points_around_x[radius=10;points=16]>
    // -->
    tagProcessor.registerTag(ListTag.class, "points_around_x", (attribute, object) -> {
        double[] values = parsePointsAroundArgs(attribute);
        if (values == null) {
            return null;
        }
        double angle = 2 * Math.PI / values[1];
        ListTag points = new ListTag();
        for (int i = 0; i < values[1]; i++) {
            double[] result = getRotatedAroundX(angle * i, values[0], 0);
            points.addObject(object.clone().add(0, result[0], result[1]));
        }
        return points;
    });
    // <--[tag]
    // @attribute <LocationTag.points_around_y[radius=<#.#>;points=<#>]>
    // @returns ListTag(LocationTag)
    // @group math
    // @description
    // Returns a list of points in a circle around a location's y axis with the specified radius and number of points.
    // For example: <player.location.points_around_y[radius=10;points=16]>
    // -->
    tagProcessor.registerTag(ListTag.class, "points_around_y", (attribute, object) -> {
        double[] values = parsePointsAroundArgs(attribute);
        if (values == null) {
            return null;
        }
        double angle = 2 * Math.PI / values[1];
        ListTag points = new ListTag();
        for (int i = 0; i < values[1]; i++) {
            double[] result = getRotatedAroundY(angle * i, values[0], 0);
            points.addObject(object.clone().add(result[0], 0, result[1]));
        }
        return points;
    });
    // <--[tag]
    // @attribute <LocationTag.points_around_z[radius=<#.#>;points=<#>]>
    // @returns ListTag(LocationTag)
    // @group math
    // @description
    // Returns a list of points in a circle around a location's z axis with the specified radius and number of points.
    // For example: <player.location.points_around_z[radius=10;points=16]>
    // -->
    tagProcessor.registerTag(ListTag.class, "points_around_z", (attribute, object) -> {
        double[] values = parsePointsAroundArgs(attribute);
        if (values == null) {
            return null;
        }
        double angle = 2 * Math.PI / values[1];
        ListTag points = new ListTag();
        for (int i = 0; i < values[1]; i++) {
            double[] result = getRotatedAroundZ(angle * i, 0, values[0]);
            points.addObject(object.clone().add(result[0], result[1], 0));
        }
        return points;
    });
    // <--[tag]
    // @attribute <LocationTag.flood_fill[<limit>]>
    // @returns ListTag(LocationTag)
    // @group world
    // @description
    // Returns the set of all blocks, starting at the given location,
    // that can be directly reached in a way that only travels through blocks of the same type as the starting block.
    // For example, if starting at an air block inside an enclosed building, this will return all air blocks inside the building (but none outside, and no non-air blocks).
    // As another example, if starting on a block of iron_ore in the ground, this will find all other blocks of iron ore that are part of the same vein.
    // This will not travel diagonally, only the 6 cardinal directions (N/E/S/W/Up/Down).
    // As this is potentially infinite should there be any opening however small, a limit must be given.
    // The limit value can be: a CuboidTag, an EllipsoidTag, or an ElementTag(Decimal) to use as a radius.
    // Note that the returned list will not be in any particular order.
    // -->
    tagProcessor.registerTag(ListTag.class, "flood_fill", (attribute, object) -> {
        if (!attribute.hasParam()) {
            return null;
        }
        AreaContainmentObject area = CuboidTag.valueOf(attribute.getParam(), CoreUtilities.noDebugContext);
        if (area == null) {
            area = EllipsoidTag.valueOf(attribute.getParam(), CoreUtilities.noDebugContext);
        }
        if (area == null) {
            double radius = attribute.getDoubleParam();
            if (radius <= 0) {
                return null;
            }
            area = new EllipsoidTag(object.clone(), new LocationTag(object.getWorld(), radius, radius, radius));
        }
        FloodFiller flooder = new FloodFiller();
        NMSHandler.getChunkHelper().changeChunkServerThread(object.getWorld());
        try {
            if (object.getWorld() == null) {
                attribute.echoError("LocationTag trying to read block, but cannot because no world is specified.");
                return null;
            }
            if (!object.isChunkLoaded()) {
                attribute.echoError("LocationTag trying to read block, but cannot because the chunk is unloaded. Use the 'chunkload' command to ensure the chunk is loaded.");
                return null;
            }
            // -->
            if (attribute.startsWith("types", 2) && attribute.hasContext(2)) {
                flooder.matcher = attribute.getContext(2);
                attribute.fulfill(1);
            } else {
                flooder.requiredMaterial = object.getBlock().getType();
            }
            flooder.run(object, area);
        } finally {
            NMSHandler.getChunkHelper().restoreServerThread(object.getWorld());
        }
        return new ListTag((Collection<LocationTag>) flooder.result);
    });
    // <--[tag]
    // @attribute <LocationTag.find_nearest_biome[<biome>]>
    // @returns LocationTag
    // @group finding
    // @description
    // Returns the location of the nearest block of the given biome type (or null).
    // Warning: may be extremely slow to process. Use with caution.
    // -->
    tagProcessor.registerTag(LocationTag.class, "find_nearest_biome", (attribute, object) -> {
        if (!attribute.hasParam()) {
            return null;
        }
        BiomeTag biome = attribute.paramAsType(BiomeTag.class);
        if (biome == null) {
            attribute.echoError("Invalid biome input.");
            return null;
        }
        Location result = NMSHandler.getWorldHelper().getNearestBiomeLocation(object, biome);
        if (result == null) {
            return null;
        }
        return new LocationTag(result);
    });
    // <--[tag]
    // @attribute <LocationTag.find_blocks_flagged[<flag_name>].within[<#>]>
    // @returns ListTag(LocationTag)
    // @group finding
    // @description
    // Returns a list of blocks that have the specified flag within a radius.
    // Note: current implementation measures the center of nearby block's distance from the exact given location.
    // Result list is sorted by closeness (1 = closest, 2 = next closest, ... last = farthest).
    // Searches the internal flag lists, rather than through all possible blocks.
    // -->
    tagProcessor.registerTag(ListTag.class, "find_blocks_flagged", (attribute, object) -> {
        if (!attribute.hasParam() || !attribute.startsWith("within", 2) || !attribute.hasContext(2)) {
            attribute.echoError("find_blocks_flagged[...].within[...] tag malformed.");
            return null;
        }
        String flagName = CoreUtilities.toLowerCase(attribute.getParam());
        attribute.fulfill(1);
        double radius = attribute.getDoubleParam();
        if (!object.isChunkLoadedSafe()) {
            attribute.echoError("LocationTag trying to read block, but cannot because the chunk is unloaded. Use the 'chunkload' command to ensure the chunk is loaded.");
            return null;
        }
        double minPossibleX = object.getX() - radius;
        double minPossibleZ = object.getZ() - radius;
        double maxPossibleX = object.getX() + radius;
        double maxPossibleZ = object.getZ() + radius;
        int minChunkX = (int) Math.floor(minPossibleX / 16);
        int minChunkZ = (int) Math.floor(minPossibleZ / 16);
        int maxChunkX = (int) Math.ceil(maxPossibleX / 16);
        int maxChunkZ = (int) Math.ceil(maxPossibleZ / 16);
        ChunkTag testChunk = new ChunkTag(object);
        final ArrayList<LocationTag> found = new ArrayList<>();
        for (int x = minChunkX; x <= maxChunkX; x++) {
            testChunk.chunkX = x;
            for (int z = minChunkZ; z <= maxChunkZ; z++) {
                testChunk.chunkZ = z;
                testChunk.cachedChunk = null;
                if (testChunk.isLoadedSafe()) {
                    LocationFlagSearchHelper.getFlaggedLocations(testChunk.getChunkForTag(attribute), flagName, (loc) -> {
                        loc.setX(loc.getX() + 0.5);
                        loc.setY(loc.getY() + 0.5);
                        loc.setZ(loc.getZ() + 0.5);
                        if (Utilities.checkLocation(object, loc, radius)) {
                            found.add(new LocationTag(loc));
                        }
                    });
                }
            }
        }
        found.sort(object::compare);
        return new ListTag(found);
    });
    // <--[tag]
    // @attribute <LocationTag.find_entities[(<matcher>)].within[<#.#>]>
    // @returns ListTag(EntityTag)
    // @group finding
    // @description
    // Returns a list of entities within a radius, with an optional search parameter for the entity type.
    // Result list is sorted by closeness (1 = closest, 2 = next closest, ... last = farthest).
    // -->
    tagProcessor.registerTag(ListTag.class, "find_entities", (attribute, object) -> {
        String matcher = attribute.hasParam() ? attribute.getParam() : null;
        if (!attribute.startsWith("within", 2) || !attribute.hasContext(2)) {
            return null;
        }
        double radius = attribute.getDoubleContext(2);
        attribute.fulfill(1);
        ListTag found = new ListTag();
        BoundingBox box = BoundingBox.of(object, radius, radius, radius);
        for (Entity entity : new WorldTag(object.getWorld()).getPossibleEntitiesForBoundary(box)) {
            if (Utilities.checkLocationWithBoundingBox(object, entity, radius)) {
                EntityTag current = new EntityTag(entity);
                if (matcher == null || BukkitScriptEvent.tryEntity(current, matcher)) {
                    found.addObject(current.getDenizenObject());
                }
            }
        }
        found.objectForms.sort((ent1, ent2) -> object.compare(((EntityFormObject) ent1).getLocation(), ((EntityFormObject) ent2).getLocation()));
        return found;
    });
    // <--[tag]
    // @attribute <LocationTag.find_blocks[(<matcher>)].within[<#.#>]>
    // @returns ListTag(LocationTag)
    // @group finding
    // @description
    // Returns a list of blocks within a radius, with an optional search parameter for the block material.
    // Note: current implementation measures the center of nearby block's distance from the exact given location.
    // Result list is sorted by closeness (1 = closest, 2 = next closest, ... last = farthest).
    // -->
    tagProcessor.registerTag(ListTag.class, "find_blocks", (attribute, object) -> {
        String matcher = attribute.hasParam() ? attribute.getParam() : null;
        if (!attribute.startsWith("within", 2) || !attribute.hasContext(2)) {
            return null;
        }
        double radius = attribute.getDoubleContext(2);
        attribute.fulfill(1);
        ListTag found = new ListTag();
        int max = Settings.blockTagsMaxBlocks();
        int index = 0;
        Location tstart = object.getBlockLocation();
        double tstartY = tstart.getY();
        int radiusInt = (int) radius;
        fullloop: for (int y = -radiusInt; y <= radiusInt; y++) {
            double newY = y + tstartY;
            if (!Utilities.isLocationYSafe(newY, object.getWorld())) {
                continue;
            }
            for (int x = -radiusInt; x <= radiusInt; x++) {
                for (int z = -radiusInt; z <= radiusInt; z++) {
                    index++;
                    if (index > max) {
                        break fullloop;
                    }
                    if (Utilities.checkLocation(object, tstart.clone().add(x + 0.5, y + 0.5, z + 0.5), radius)) {
                        if (matcher == null || BukkitScriptEvent.tryMaterial(new LocationTag(tstart.clone().add(x, y, z)).getBlockTypeForTag(attribute), matcher)) {
                            found.addObject(new LocationTag(tstart.clone().add(x, y, z)));
                        }
                    }
                }
            }
        }
        found.objectForms.sort((loc1, loc2) -> object.compare((LocationTag) loc1, (LocationTag) loc2));
        return found;
    });
    // <--[tag]
    // @attribute <LocationTag.find_spawnable_blocks_within[<#.#>]>
    // @returns ListTag(LocationTag)
    // @group finding
    // @description
    // Returns a list of blocks within a radius, that are safe for spawning, with the same logic as <@link tag LocationTag.is_spawnable>.
    // Note: current implementation measures the center of nearby block's distance from the exact given location.
    // Result list is sorted by closeness (1 = closest, 2 = next closest, ... last = farthest).
    // -->
    tagProcessor.registerTag(ListTag.class, "find_spawnable_blocks_within", (attribute, object) -> {
        if (!attribute.hasParam()) {
            return null;
        }
        double radius = attribute.getDoubleParam();
        ListTag found = new ListTag();
        int max = Settings.blockTagsMaxBlocks();
        int index = 0;
        Location tstart = object.getBlockLocation();
        double tstartY = tstart.getY();
        int radiusInt = (int) radius;
        fullloop: for (int y = -radiusInt; y <= radiusInt; y++) {
            double newY = y + tstartY;
            if (!Utilities.isLocationYSafe(newY, object.getWorld())) {
                continue;
            }
            for (int x = -radiusInt; x <= radiusInt; x++) {
                for (int z = -radiusInt; z <= radiusInt; z++) {
                    index++;
                    if (index > max) {
                        break fullloop;
                    }
                    Location loc = tstart.clone().add(x + 0.5, y + 0.5, z + 0.5);
                    if (Utilities.checkLocation(object, loc, radius) && SpawnableHelper.isSpawnable(loc)) {
                        found.addObject(new LocationTag(loc.add(0, -0.5, 0)));
                    }
                }
            }
        }
        found.objectForms.sort((loc1, loc2) -> object.compare((LocationTag) loc1, (LocationTag) loc2));
        return found;
    });
    // <--[tag]
    // @attribute <LocationTag.find_players_within[<#.#>]>
    // @returns ListTag(PlayerTag)
    // @group finding
    // @description
    // Returns a list of players within a radius.
    // Result list is sorted by closeness (1 = closest, 2 = next closest, ... last = farthest).
    // -->
    tagProcessor.registerTag(ListTag.class, "find_players_within", (attribute, object) -> {
        if (!attribute.hasParam()) {
            return null;
        }
        double radius = attribute.getDoubleParam();
        ArrayList<PlayerTag> found = new ArrayList<>();
        for (Player player : Bukkit.getOnlinePlayers()) {
            if (!player.isDead() && Utilities.checkLocationWithBoundingBox(object, player, radius)) {
                found.add(new PlayerTag(player));
            }
        }
        found.sort((pl1, pl2) -> object.compare(pl1.getLocation(), pl2.getLocation()));
        return new ListTag(found);
    });
    // <--[tag]
    // @attribute <LocationTag.find_npcs_within[<#.#>]>
    // @returns ListTag(NPCTag)
    // @group finding
    // @description
    // Returns a list of NPCs within a radius.
    // Result list is sorted by closeness (1 = closest, 2 = next closest, ... last = farthest).
    // -->
    tagProcessor.registerTag(ListTag.class, "find_npcs_within", (attribute, object) -> {
        if (!attribute.hasParam()) {
            return null;
        }
        double radius = attribute.getDoubleParam();
        ArrayList<NPCTag> found = new ArrayList<>();
        for (NPC npc : CitizensAPI.getNPCRegistry()) {
            if (npc.isSpawned() && Utilities.checkLocationWithBoundingBox(object, npc.getEntity(), radius)) {
                found.add(new NPCTag(npc));
            }
        }
        found.sort((npc1, npc2) -> object.compare(npc1.getLocation(), npc2.getLocation()));
        return new ListTag(found);
    });
    tagProcessor.registerTag(ObjectTag.class, "find", (attribute, object) -> {
        if (!attribute.startsWith("within", 3) || !attribute.hasContext(3)) {
            return null;
        }
        double radius = attribute.getDoubleContext(3);
        if (attribute.startsWith("blocks", 2)) {
            Deprecations.locationFindEntities.warn(attribute.context);
            ArrayList<LocationTag> found = new ArrayList<>();
            List<MaterialTag> materials = new ArrayList<>();
            if (attribute.hasContext(2)) {
                materials = attribute.contextAsType(2, ListTag.class).filter(MaterialTag.class, attribute.context);
            }
            // Avoid NPE from invalid materials
            if (materials == null) {
                return null;
            }
            int max = Settings.blockTagsMaxBlocks();
            int index = 0;
            attribute.fulfill(2);
            Location tstart = object.getBlockLocation();
            double tstartY = tstart.getY();
            int radiusInt = (int) radius;
            fullloop: for (int x = -radiusInt; x <= radiusInt; x++) {
                for (int y = -radiusInt; y <= radiusInt; y++) {
                    double newY = y + tstartY;
                    if (!Utilities.isLocationYSafe(newY, object.getWorld())) {
                        continue;
                    }
                    for (int z = -radiusInt; z <= radiusInt; z++) {
                        index++;
                        if (index > max) {
                            break fullloop;
                        }
                        if (Utilities.checkLocation(object, tstart.clone().add(x + 0.5, y + 0.5, z + 0.5), radius)) {
                            if (!materials.isEmpty()) {
                                for (MaterialTag material : materials) {
                                    if (material.getMaterial() == new LocationTag(tstart.clone().add(x, y, z)).getBlockTypeForTag(attribute)) {
                                        found.add(new LocationTag(tstart.clone().add(x, y, z)));
                                    }
                                }
                            } else {
                                found.add(new LocationTag(tstart.clone().add(x, y, z)));
                            }
                        }
                    }
                }
            }
            found.sort(object::compare);
            return new ListTag(found);
        } else // -->
        if (attribute.startsWith("surface_blocks", 2)) {
            ArrayList<LocationTag> found = new ArrayList<>();
            List<MaterialTag> materials = new ArrayList<>();
            if (attribute.hasContext(2)) {
                materials = attribute.contextAsType(2, ListTag.class).filter(MaterialTag.class, attribute.context);
            }
            // Avoid NPE from invalid materials
            if (materials == null) {
                return null;
            }
            int max = Settings.blockTagsMaxBlocks();
            int index = 0;
            attribute.fulfill(2);
            Location blockLoc = object.getBlockLocation();
            Location loc = blockLoc.clone().add(0.5f, 0.5f, 0.5f);
            fullloop: for (double x = -(radius); x <= radius; x++) {
                for (double y = -(radius); y <= radius; y++) {
                    for (double z = -(radius); z <= radius; z++) {
                        index++;
                        if (index > max) {
                            break fullloop;
                        }
                        if (Utilities.checkLocation(loc, blockLoc.clone().add(x + 0.5, y + 0.5, z + 0.5), radius)) {
                            LocationTag l = new LocationTag(blockLoc.clone().add(x, y, z));
                            if (!materials.isEmpty()) {
                                for (MaterialTag material : materials) {
                                    if (material.getMaterial() == l.getBlockTypeForTag(attribute)) {
                                        if (new LocationTag(l.clone().add(0, 1, 0)).getBlockTypeForTag(attribute) == Material.AIR && new LocationTag(l.clone().add(0, 2, 0)).getBlockTypeForTag(attribute) == Material.AIR && l.getBlockTypeForTag(attribute) != Material.AIR) {
                                            found.add(new LocationTag(blockLoc.clone().add(x + 0.5, y, z + 0.5)));
                                        }
                                    }
                                }
                            } else {
                                if (new LocationTag(l.clone().add(0, 1, 0)).getBlockTypeForTag(attribute) == Material.AIR && new LocationTag(l.clone().add(0, 2, 0)).getBlockTypeForTag(attribute) == Material.AIR && l.getBlockTypeForTag(attribute) != Material.AIR) {
                                    found.add(new LocationTag(blockLoc.clone().add(x + 0.5, y, z + 0.5)));
                                }
                            }
                        }
                    }
                }
            }
            found.sort(object::compare);
            return new ListTag(found);
        } else if (attribute.startsWith("players", 2)) {
            Deprecations.locationFindEntities.warn(attribute.context);
            ArrayList<PlayerTag> found = new ArrayList<>();
            attribute.fulfill(2);
            for (Player player : Bukkit.getOnlinePlayers()) {
                if (!player.isDead() && Utilities.checkLocationWithBoundingBox(object, player, radius)) {
                    found.add(new PlayerTag(player));
                }
            }
            found.sort((pl1, pl2) -> object.compare(pl1.getLocation(), pl2.getLocation()));
            return new ListTag(found);
        } else if (attribute.startsWith("npcs", 2)) {
            Deprecations.locationFindEntities.warn(attribute.context);
            ArrayList<NPCTag> found = new ArrayList<>();
            attribute.fulfill(2);
            for (NPC npc : CitizensAPI.getNPCRegistry()) {
                if (npc.isSpawned() && Utilities.checkLocationWithBoundingBox(object, npc.getEntity(), radius)) {
                    found.add(new NPCTag(npc));
                }
            }
            found.sort((npc1, npc2) -> object.compare(npc1.getLocation(), npc2.getLocation()));
            return new ListTag(found);
        } else if (attribute.startsWith("entities", 2)) {
            Deprecations.locationFindEntities.warn(attribute.context);
            ListTag ent_list = attribute.hasContext(2) ? attribute.contextAsType(2, ListTag.class) : null;
            ListTag found = new ListTag();
            attribute.fulfill(2);
            for (Entity entity : new WorldTag(object.getWorld()).getEntitiesForTag()) {
                if (Utilities.checkLocationWithBoundingBox(object, entity, radius)) {
                    EntityTag current = new EntityTag(entity);
                    if (ent_list != null) {
                        for (String ent : ent_list) {
                            if (current.comparedTo(ent)) {
                                found.addObject(current.getDenizenObject());
                                break;
                            }
                        }
                    } else {
                        found.addObject(current.getDenizenObject());
                    }
                }
            }
            found.objectForms.sort((ent1, ent2) -> object.compare(((EntityFormObject) ent1).getLocation(), ((EntityFormObject) ent2).getLocation()));
            return new ListTag(found.objectForms);
        } else // -->
        if (attribute.startsWith("living_entities", 2)) {
            ListTag found = new ListTag();
            attribute.fulfill(2);
            BoundingBox box = BoundingBox.of(object, radius, radius, radius);
            for (Entity entity : new WorldTag(object.getWorld()).getPossibleEntitiesForBoundary(box)) {
                if (entity instanceof LivingEntity && Utilities.checkLocationWithBoundingBox(object, entity, radius)) {
                    found.addObject(new EntityTag(entity).getDenizenObject());
                }
            }
            found.objectForms.sort((ent1, ent2) -> object.compare(((EntityFormObject) ent1).getLocation(), ((EntityFormObject) ent2).getLocation()));
            return new ListTag(found.objectForms);
        } else // -->
        if (attribute.startsWith("structure", 2) && attribute.hasContext(2)) {
            String typeName = attribute.getContext(2);
            StructureType type = StructureType.getStructureTypes().get(typeName);
            if (type == null) {
                attribute.echoError("Invalid structure type '" + typeName + "'.");
                return null;
            }
            attribute.fulfill(2);
            Location result = object.getWorld().locateNearestStructure(object, type, (int) radius, false);
            if (result == null) {
                return null;
            }
            return new LocationTag(result);
        } else // -->
        if (attribute.startsWith("unexplored_structure", 2) && attribute.hasContext(2)) {
            String typeName = attribute.getContext(2);
            StructureType type = StructureType.getStructureTypes().get(typeName);
            if (type == null) {
                attribute.echoError("Invalid structure type '" + typeName + "'.");
                return null;
            }
            attribute.fulfill(2);
            Location result = object.getWorld().locateNearestStructure(object, type, (int) radius, true);
            if (result == null) {
                return null;
            }
            return new LocationTag(result);
        }
        return null;
    });
    // <--[tag]
    // @attribute <LocationTag.find_path[<location>]>
    // @returns ListTag(LocationTag)
    // @group finding
    // @description
    // Returns a full list of points along the path from this location to the given location.
    // Uses a max range of 100 blocks from the start.
    // -->
    tagProcessor.registerTag(ListTag.class, "find_path", (attribute, object) -> {
        if (!attribute.hasParam()) {
            return null;
        }
        LocationTag two = attribute.paramAsType(LocationTag.class);
        if (two == null) {
            return null;
        }
        List<LocationTag> locs = PathFinder.getPath(object, two);
        ListTag list = new ListTag();
        for (LocationTag loc : locs) {
            list.addObject(loc);
        }
        return list;
    });
    // <--[tag]
    // @attribute <LocationTag.formatted>
    // @returns ElementTag
    // @group identity
    // @description
    // Returns the formatted version of the LocationTag.
    // In the format: X 'x.x', Y 'y.y', Z 'z.z', in world 'world'
    // For example: X '1.0', Y '2.0', Z '3.0', in world 'world_nether'
    // -->
    tagProcessor.registerTag(ElementTag.class, "formatted", (attribute, object) -> {
        // -->
        if (attribute.startsWith("citizens", 2)) {
            attribute.fulfill(1);
            return new ElementTag(object.getX() + ":" + object.getY() + ":" + object.getZ() + ":" + object.getWorldName());
        }
        return new ElementTag("X '" + object.getX() + "', Y '" + object.getY() + "', Z '" + object.getZ() + "', in world '" + object.getWorldName() + "'");
    });
    // <--[tag]
    // @attribute <LocationTag.chunk>
    // @returns ChunkTag
    // @group identity
    // @description
    // Returns the chunk that this location belongs to.
    // -->
    tagProcessor.registerTag(ChunkTag.class, "chunk", (attribute, object) -> {
        return new ChunkTag(object);
    }, "get_chunk");
    // <--[tag]
    // @attribute <LocationTag.raw>
    // @returns ElementTag
    // @group identity
    // @description
    // Returns the raw representation of this location, without any note name.
    // -->
    tagProcessor.registerTag(ElementTag.class, "raw", (attribute, object) -> {
        return new ElementTag(object.identifyRaw());
    });
    // <--[tag]
    // @attribute <LocationTag.world>
    // @returns WorldTag
    // @group identity
    // @description
    // Returns the world that the location is in.
    // -->
    tagProcessor.registerTag(WorldTag.class, "world", (attribute, object) -> {
        return WorldTag.mirrorBukkitWorld(object.getWorld());
    });
    // <--[tag]
    // @attribute <LocationTag.x>
    // @returns ElementTag(Decimal)
    // @group identity
    // @description
    // Returns the X coordinate of the location.
    // -->
    tagProcessor.registerTag(ElementTag.class, "x", (attribute, object) -> {
        return new ElementTag(object.getX());
    });
    // <--[tag]
    // @attribute <LocationTag.y>
    // @returns ElementTag(Decimal)
    // @group identity
    // @description
    // Returns the Y coordinate of the location.
    // -->
    tagProcessor.registerTag(ElementTag.class, "y", (attribute, object) -> {
        return new ElementTag(object.getY());
    });
    // <--[tag]
    // @attribute <LocationTag.z>
    // @returns ElementTag(Decimal)
    // @group identity
    // @description
    // Returns the Z coordinate of the location.
    // -->
    tagProcessor.registerTag(ElementTag.class, "z", (attribute, object) -> {
        return new ElementTag(object.getZ());
    });
    // <--[tag]
    // @attribute <LocationTag.xyz>
    // @returns ElementTag
    // @group identity
    // @description
    // Returns the location in "x,y,z" format.
    // For example: 1,2,3
    // World, yaw, and pitch will be excluded from this output.
    // -->
    tagProcessor.registerTag(ElementTag.class, "xyz", (attribute, object) -> {
        return new ElementTag(CoreUtilities.doubleToString(object.getX()) + "," + CoreUtilities.doubleToString(object.getY()) + "," + CoreUtilities.doubleToString(object.getZ()));
    });
    // <--[tag]
    // @attribute <LocationTag.with_x[<number>]>
    // @returns LocationTag
    // @group identity
    // @description
    // Returns a copy of the location with a changed X value.
    // -->
    tagProcessor.registerTag(LocationTag.class, "with_x", (attribute, object) -> {
        if (!attribute.hasParam()) {
            return null;
        }
        LocationTag output = object.clone();
        output.setX(attribute.getDoubleParam());
        return output;
    });
    // <--[tag]
    // @attribute <LocationTag.with_y[<number>]>
    // @returns LocationTag
    // @group identity
    // @description
    // Returns a copy of the location with a changed Y value.
    // -->
    tagProcessor.registerTag(LocationTag.class, "with_y", (attribute, object) -> {
        if (!attribute.hasParam()) {
            return null;
        }
        LocationTag output = object.clone();
        output.setY(attribute.getDoubleParam());
        return output;
    });
    // <--[tag]
    // @attribute <LocationTag.with_z[<number>]>
    // @returns LocationTag
    // @group identity
    // @description
    // Returns a copy of the location with a changed Z value.
    // -->
    tagProcessor.registerTag(LocationTag.class, "with_z", (attribute, object) -> {
        if (!attribute.hasParam()) {
            return null;
        }
        LocationTag output = object.clone();
        output.setZ(attribute.getDoubleParam());
        return output;
    });
    // <--[tag]
    // @attribute <LocationTag.with_yaw[<number>]>
    // @returns LocationTag
    // @group identity
    // @description
    // Returns a copy of the location with a changed yaw value.
    // -->
    tagProcessor.registerTag(LocationTag.class, "with_yaw", (attribute, object) -> {
        if (!attribute.hasParam()) {
            return null;
        }
        LocationTag output = object.clone();
        output.setYaw((float) attribute.getDoubleParam());
        return output;
    });
    // <--[tag]
    // @attribute <LocationTag.with_pitch[<number>]>
    // @returns LocationTag
    // @group identity
    // @description
    // Returns a copy of the location with a changed pitch value.
    // -->
    tagProcessor.registerTag(LocationTag.class, "with_pitch", (attribute, object) -> {
        if (!attribute.hasParam()) {
            return null;
        }
        LocationTag output = object.clone();
        output.setPitch((float) attribute.getDoubleParam());
        return output;
    });
    // <--[tag]
    // @attribute <LocationTag.with_world[<world>]>
    // @returns LocationTag
    // @group identity
    // @description
    // Returns a copy of the location with a changed world value.
    // -->
    tagProcessor.registerTag(LocationTag.class, "with_world", (attribute, object) -> {
        if (!attribute.hasParam()) {
            return null;
        }
        LocationTag output = object.clone();
        WorldTag world = attribute.paramAsType(WorldTag.class);
        output.setWorld(world.getWorld());
        return output;
    });
    // <--[tag]
    // @attribute <LocationTag.note_name>
    // @returns ElementTag
    // @group identity
    // @description
    // Gets the name of a noted LocationTag. If the location isn't noted, this is null.
    // -->
    tagProcessor.registerTag(ElementTag.class, "note_name", (attribute, object) -> {
        String noteName = NoteManager.getSavedId((object));
        if (noteName == null) {
            return null;
        }
        return new ElementTag(noteName);
    }, "notable_name");
    // <--[tag]
    // @attribute <LocationTag.add[<location>]>
    // @returns LocationTag
    // @group math
    // @description
    // Returns the location with the specified coordinates added to it.
    // -->
    tagProcessor.registerTag(LocationTag.class, "add", (attribute, object) -> {
        if (!attribute.hasParam()) {
            return null;
        }
        // TODO: Just LocationTag.valueOf?
        String[] ints = attribute.getParam().replace("l@", "").split(",", 4);
        if (ints.length >= 3) {
            if (ArgumentHelper.matchesDouble(ints[0]) && ArgumentHelper.matchesDouble(ints[1]) && ArgumentHelper.matchesDouble(ints[2])) {
                return new LocationTag(object.clone().add(Double.valueOf(ints[0]), Double.valueOf(ints[1]), Double.valueOf(ints[2])));
            }
        }
        if (LocationTag.matches(attribute.getParam())) {
            return object.clone().add(attribute.paramAsType(LocationTag.class));
        }
        return null;
    });
    // <--[tag]
    // @attribute <LocationTag.sub[<location>]>
    // @returns LocationTag
    // @group math
    // @description
    // Returns the location with the specified coordinates subtracted from it.
    // -->
    tagProcessor.registerTag(LocationTag.class, "sub", (attribute, object) -> {
        if (!attribute.hasParam()) {
            return null;
        }
        // TODO: Just LocationTag.valueOf?
        String[] ints = attribute.getParam().replace("l@", "").split(",", 4);
        if (ints.length == 3 || ints.length == 4) {
            if (ArgumentHelper.matchesDouble(ints[0]) && ArgumentHelper.matchesDouble(ints[1]) && ArgumentHelper.matchesDouble(ints[2])) {
                return new LocationTag(object.clone().subtract(Double.valueOf(ints[0]), Double.valueOf(ints[1]), Double.valueOf(ints[2])));
            }
        }
        if (LocationTag.matches(attribute.getParam())) {
            return new LocationTag(object.clone().subtract(attribute.paramAsType(LocationTag.class)));
        }
        return null;
    });
    // <--[tag]
    // @attribute <LocationTag.mul[<length>]>
    // @returns LocationTag
    // @group math
    // @description
    // Returns the location multiplied by the specified length.
    // -->
    tagProcessor.registerTag(LocationTag.class, "mul", (attribute, object) -> {
        if (!attribute.hasParam()) {
            return null;
        }
        return new LocationTag(object.clone().multiply(Double.parseDouble(attribute.getParam())));
    });
    // <--[tag]
    // @attribute <LocationTag.div[<length>]>
    // @returns LocationTag
    // @group math
    // @description
    // Returns the location divided by the specified length.
    // -->
    tagProcessor.registerTag(LocationTag.class, "div", (attribute, object) -> {
        if (!attribute.hasParam()) {
            return null;
        }
        return new LocationTag(object.clone().multiply(1D / Double.parseDouble(attribute.getParam())));
    });
    // <--[tag]
    // @attribute <LocationTag.normalize>
    // @returns LocationTag
    // @group math
    // @description
    // Returns a 1-length vector in the same direction as this vector location.
    // -->
    tagProcessor.registerTag(LocationTag.class, "normalize", (attribute, object) -> {
        double len = Math.sqrt(Math.pow(object.getX(), 2) + Math.pow(object.getY(), 2) + Math.pow(object.getZ(), 2));
        if (len == 0) {
            len = 1;
        }
        return new LocationTag(object.clone().multiply(1D / len));
    });
    // <--[tag]
    // @attribute <LocationTag.vector_length>
    // @returns ElementTag(Decimal)
    // @synonyms LocationTag.magnitude
    // @group math
    // @description
    // Returns the 3D length of the vector/location.
    // -->
    tagProcessor.registerTag(ElementTag.class, "vector_length", (attribute, object) -> {
        return new ElementTag(Math.sqrt(Math.pow(object.getX(), 2) + Math.pow(object.getY(), 2) + Math.pow(object.getZ(), 2)));
    });
    // <--[tag]
    // @attribute <LocationTag.vector_to_face>
    // @returns ElementTag
    // @description
    // Returns the name of the BlockFace represented by a normal vector.
    // Result can be any of the following:
    // NORTH, EAST, SOUTH, WEST, UP, DOWN, NORTH_EAST, NORTH_WEST, SOUTH_EAST, SOUTH_WEST,
    // WEST_NORTH_WEST, NORTH_NORTH_WEST, NORTH_NORTH_EAST, EAST_NORTH_EAST, EAST_SOUTH_EAST,
    // SOUTH_SOUTH_EAST, SOUTH_SOUTH_WEST, WEST_SOUTH_WEST, SELF
    // -->
    tagProcessor.registerTag(ElementTag.class, "vector_to_face", (attribute, object) -> {
        BlockFace face = Utilities.faceFor(object.toVector());
        if (face != null) {
            return new ElementTag(face.name());
        }
        return null;
    });
    // <--[tag]
    // @attribute <LocationTag.distance_squared[<location>]>
    // @returns ElementTag(Decimal)
    // @group math
    // @description
    // Returns the distance between 2 locations, squared.
    // -->
    tagProcessor.registerTag(ElementTag.class, "distance_squared", (attribute, object) -> {
        if (!attribute.hasParam()) {
            return null;
        }
        if (LocationTag.matches(attribute.getParam())) {
            LocationTag toLocation = attribute.paramAsType(LocationTag.class);
            if (!object.getWorldName().equalsIgnoreCase(toLocation.getWorldName())) {
                if (!attribute.hasAlternative()) {
                    Debug.echoError("Can't measure distance between two different worlds!");
                }
                return null;
            }
            return new ElementTag(object.distanceSquared(toLocation));
        }
        return null;
    });
    // <--[tag]
    // @attribute <LocationTag.distance[<location>]>
    // @returns ElementTag(Decimal)
    // @group math
    // @description
    // Returns the distance between 2 locations.
    // -->
    tagProcessor.registerTag(ElementTag.class, "distance", (attribute, object) -> {
        if (!attribute.hasParam()) {
            return null;
        }
        if (LocationTag.matches(attribute.getParam())) {
            LocationTag toLocation = attribute.paramAsType(LocationTag.class);
            // -->
            if (attribute.startsWith("horizontal", 2)) {
                // -->
                if (attribute.startsWith("multiworld", 3)) {
                    attribute.fulfill(2);
                    return new ElementTag(Math.sqrt(Math.pow(object.getX() - toLocation.getX(), 2) + Math.pow(object.getZ() - toLocation.getZ(), 2)));
                }
                attribute.fulfill(1);
                if (object.getWorldName().equalsIgnoreCase(toLocation.getWorldName())) {
                    return new ElementTag(Math.sqrt(Math.pow(object.getX() - toLocation.getX(), 2) + Math.pow(object.getZ() - toLocation.getZ(), 2)));
                }
            } else // -->
            if (attribute.startsWith("vertical", 2)) {
                // -->
                if (attribute.startsWith("multiworld", 3)) {
                    attribute.fulfill(2);
                    return new ElementTag(Math.abs(object.getY() - toLocation.getY()));
                }
                attribute.fulfill(1);
                if (object.getWorldName().equalsIgnoreCase(toLocation.getWorldName())) {
                    return new ElementTag(Math.abs(object.getY() - toLocation.getY()));
                }
            }
            if (!object.getWorldName().equalsIgnoreCase(toLocation.getWorldName())) {
                if (!attribute.hasAlternative()) {
                    Debug.echoError("Can't measure distance between two different worlds!");
                }
                return null;
            } else {
                return new ElementTag(object.distance(toLocation));
            }
        }
        return null;
    });
    // <--[tag]
    // @attribute <LocationTag.is_within_border>
    // @returns ElementTag(Boolean)
    // @group world
    // @description
    // Returns whether the location is within the world border.
    // -->
    tagProcessor.registerTag(ElementTag.class, "is_within_border", (attribute, object) -> {
        return new ElementTag(object.getWorld().getWorldBorder().isInside(object));
    });
    // <--[tag]
    // @attribute <LocationTag.is_within[<area>]>
    // @returns ElementTag(Boolean)
    // @group areas
    // @description
    // Returns whether the location is within the specified area (cuboid, ellipsoid, polygon, ...).
    // -->
    tagProcessor.registerTag(ElementTag.class, "is_within", (attribute, object) -> {
        if (!attribute.hasParam()) {
            return null;
        }
        if (EllipsoidTag.matches(attribute.getParam())) {
            EllipsoidTag ellipsoid = attribute.paramAsType(EllipsoidTag.class);
            if (ellipsoid != null) {
                return new ElementTag(ellipsoid.contains(object));
            }
        } else if (PolygonTag.matches(attribute.getParam())) {
            PolygonTag polygon = attribute.paramAsType(PolygonTag.class);
            if (polygon != null) {
                return new ElementTag(polygon.doesContainLocation(object));
            }
        } else {
            CuboidTag cuboid = attribute.paramAsType(CuboidTag.class);
            if (cuboid != null) {
                return new ElementTag(cuboid.isInsideCuboid(object));
            }
        }
        return null;
    });
    // <--[tag]
    // @attribute <LocationTag.to_ellipsoid[<size>]>
    // @returns EllipsoidTag
    // @group areas
    // @description
    // Returns an ellipsoid centered at this location with the specified size.
    // Size input is a vector of x,y,z size.
    // -->
    tagProcessor.registerTag(EllipsoidTag.class, "to_ellipsoid", (attribute, object) -> {
        if (!attribute.hasParam()) {
            attribute.echoError("to_ellipsoid[...] tag must have input.");
            return null;
        }
        return new EllipsoidTag(object.clone(), attribute.getParamObject().asType(LocationTag.class, attribute.context));
    });
    // <--[tag]
    // @attribute <LocationTag.to_cuboid[<location>]>
    // @returns CuboidTag
    // @group areas
    // @description
    // Returns a cuboid from this location to the specified location.
    // -->
    tagProcessor.registerTag(CuboidTag.class, "to_cuboid", (attribute, object) -> {
        if (!attribute.hasParam()) {
            attribute.echoError("to_cuboid[...] tag must have input.");
            return null;
        }
        return new CuboidTag(object.clone(), attribute.getParamObject().asType(LocationTag.class, attribute.context));
    });
    // <--[tag]
    // @attribute <LocationTag.biome>
    // @mechanism LocationTag.biome
    // @returns BiomeTag
    // @group world
    // @description
    // Returns the biome at the location.
    // -->
    tagProcessor.registerTag(ObjectTag.class, "biome", (attribute, object) -> {
        if (attribute.startsWith("formatted", 2)) {
            Deprecations.locationBiomeFormattedTag.warn(attribute.context);
            attribute.fulfill(1);
            return new ElementTag(CoreUtilities.toLowerCase(object.getBiomeForTag(attribute).getName()).replace('_', ' '));
        }
        return new BiomeTag(object.getBiomeForTag(attribute));
    });
    // <--[tag]
    // @attribute <LocationTag.cuboids>
    // @returns ListTag(CuboidTag)
    // @group areas
    // @description
    // Returns a ListTag of all noted CuboidTags that include this location.
    // -->
    tagProcessor.registerTag(ListTag.class, "cuboids", (attribute, object) -> {
        List<CuboidTag> cuboids = CuboidTag.getNotableCuboidsContaining(object);
        ListTag cuboid_list = new ListTag();
        for (CuboidTag cuboid : cuboids) {
            cuboid_list.addObject(cuboid);
        }
        return cuboid_list;
    });
    // <--[tag]
    // @attribute <LocationTag.ellipsoids>
    // @returns ListTag(EllipsoidTag)
    // @group areas
    // @description
    // Returns a ListTag of all noted EllipsoidTags that include this location.
    // -->
    tagProcessor.registerTag(ListTag.class, "ellipsoids", (attribute, object) -> {
        List<EllipsoidTag> ellipsoids = EllipsoidTag.getNotableEllipsoidsContaining(object);
        ListTag ellipsoid_list = new ListTag();
        for (EllipsoidTag ellipsoid : ellipsoids) {
            ellipsoid_list.addObject(ellipsoid);
        }
        return ellipsoid_list;
    });
    // <--[tag]
    // @attribute <LocationTag.polygons>
    // @returns ListTag(PolygonTag)
    // @group areas
    // @description
    // Returns a ListTag of all noted PolygonTags that include this location.
    // -->
    tagProcessor.registerTag(ListTag.class, "polygons", (attribute, object) -> {
        List<PolygonTag> polygons = PolygonTag.getNotedPolygonsContaining(object);
        ListTag polygon_list = new ListTag();
        for (PolygonTag polygon : polygons) {
            polygon_list.addObject(polygon);
        }
        return polygon_list;
    });
    // <--[tag]
    // @attribute <LocationTag.is_liquid>
    // @returns ElementTag(Boolean)
    // @group world
    // @description
    // Returns whether the block at the location is a liquid.
    // -->
    tagProcessor.registerTag(ElementTag.class, "is_liquid", (attribute, object) -> {
        Block b = object.getBlockForTag(attribute);
        if (b != null) {
            try {
                NMSHandler.getChunkHelper().changeChunkServerThread(object.getWorld());
                return new ElementTag(b.isLiquid());
            } finally {
                NMSHandler.getChunkHelper().restoreServerThread(object.getWorld());
            }
        }
        return null;
    });
    // <--[tag]
    // @attribute <LocationTag.light>
    // @returns ElementTag(Number)
    // @group world
    // @description
    // Returns the total amount of light on the location.
    // -->
    tagProcessor.registerTag(ElementTag.class, "light", (attribute, object) -> {
        Block b = object.getBlockForTag(attribute);
        if (b != null) {
            try {
                NMSHandler.getChunkHelper().changeChunkServerThread(object.getWorld());
                // -->
                if (attribute.startsWith("blocks", 2)) {
                    attribute.fulfill(1);
                    return new ElementTag(object.getBlockForTag(attribute).getLightFromBlocks());
                }
                // -->
                if (attribute.startsWith("sky", 2)) {
                    attribute.fulfill(1);
                    return new ElementTag(object.getBlockForTag(attribute).getLightFromSky());
                }
                return new ElementTag(object.getBlockForTag(attribute).getLightLevel());
            } finally {
                NMSHandler.getChunkHelper().restoreServerThread(object.getWorld());
            }
        }
        return null;
    });
    // <--[tag]
    // @attribute <LocationTag.power>
    // @returns ElementTag(Number)
    // @group world
    // @description
    // Returns the current redstone power level of a block.
    // -->
    tagProcessor.registerTag(ElementTag.class, "power", (attribute, object) -> {
        Block b = object.getBlockForTag(attribute);
        if (b != null) {
            try {
                NMSHandler.getChunkHelper().changeChunkServerThread(object.getWorld());
                return new ElementTag(object.getBlockForTag(attribute).getBlockPower());
            } finally {
                NMSHandler.getChunkHelper().restoreServerThread(object.getWorld());
            }
        }
        return null;
    });
    // <--[tag]
    // @attribute <LocationTag.lectern_page>
    // @returns ElementTag(Number)
    // @mechanism LocationTag.lectern_page
    // @group world
    // @description
    // Returns the current page on display in the book on this Lectern block.
    // -->
    tagProcessor.registerTag(ElementTag.class, "lectern_page", (attribute, object) -> {
        BlockState state = object.getBlockStateForTag(attribute);
        if (state instanceof Lectern) {
            return new ElementTag(((Lectern) state).getPage());
        }
        return null;
    });
    // <--[tag]
    // @attribute <LocationTag.has_loot_table>
    // @returns ElementTag(Boolean)
    // @mechanism LocationTag.clear_loot_table
    // @group world
    // @description
    // Returns an element indicating whether the chest at this location has a loot-table set.
    // -->
    tagProcessor.registerTag(ElementTag.class, "has_loot_table", (attribute, object) -> {
        BlockState state = object.getBlockStateForTag(attribute);
        if (state instanceof Lootable) {
            return new ElementTag(((Lootable) state).getLootTable() != null);
        }
        return null;
    });
    // <--[tag]
    // @attribute <LocationTag.loot_table_id>
    // @returns ElementTag
    // @mechanism LocationTag.clear_loot_table
    // @group world
    // @description
    // Returns an element indicating the minecraft key for the loot-table for the chest at this location (if any).
    // -->
    tagProcessor.registerTag(ElementTag.class, "loot_table_id", (attribute, object) -> {
        BlockState state = object.getBlockStateForTag(attribute);
        if (state instanceof Lootable) {
            LootTable table = ((Lootable) state).getLootTable();
            if (table != null) {
                return new ElementTag(table.getKey().toString());
            }
        }
        return null;
    });
    // <--[tag]
    // @attribute <LocationTag.tree_distance>
    // @returns ElementTag(Number)
    // @group world
    // @description
    // Returns a number of how many blocks away from a connected tree leaves are.
    // Defaults to 7 if not connected to a tree.
    // -->
    tagProcessor.registerTag(ElementTag.class, "tree_distance", (attribute, object) -> {
        MaterialTag material = new MaterialTag(object.getBlockForTag(attribute));
        if (MaterialPersistent.describes(material)) {
            return new ElementTag(MaterialPersistent.getFrom(material).getDistance());
        }
        return null;
    });
    // <--[tag]
    // @attribute <LocationTag.command_block_name>
    // @returns ElementTag
    // @mechanism LocationTag.command_block_name
    // @group world
    // @description
    // Returns the name a command block is set to.
    // -->
    tagProcessor.registerTag(ElementTag.class, "command_block_name", (attribute, object) -> {
        if (!(object.getBlockStateForTag(attribute) instanceof CommandBlock)) {
            return null;
        }
        return new ElementTag(((CommandBlock) object.getBlockStateForTag(attribute)).getName());
    });
    // <--[tag]
    // @attribute <LocationTag.command_block>
    // @returns ElementTag
    // @mechanism LocationTag.command_block
    // @group world
    // @description
    // Returns the command a command block is set to.
    // -->
    tagProcessor.registerTag(ElementTag.class, "command_block", (attribute, object) -> {
        if (!(object.getBlockStateForTag(attribute) instanceof CommandBlock)) {
            return null;
        }
        return new ElementTag(((CommandBlock) object.getBlockStateForTag(attribute)).getCommand());
    });
    // <--[tag]
    // @attribute <LocationTag.brewing_time>
    // @returns DurationTag
    // @mechanism LocationTag.brewing_time
    // @group world
    // @description
    // Returns the brewing time a brewing stand has left.
    // -->
    tagProcessor.registerTag(DurationTag.class, "brewing_time", (attribute, object) -> {
        return new DurationTag((long) ((BrewingStand) object.getBlockStateForTag(attribute)).getBrewingTime());
    });
    // <--[tag]
    // @attribute <LocationTag.brewing_fuel_level>
    // @returns ElementTag(Number)
    // @mechanism LocationTag.brewing_fuel_level
    // @group world
    // @description
    // Returns the level of fuel a brewing stand has. Each unit of fuel can power one brewing operation.
    // -->
    tagProcessor.registerTag(ElementTag.class, "brewing_fuel_level", (attribute, object) -> {
        return new ElementTag(((BrewingStand) object.getBlockStateForTag(attribute)).getFuelLevel());
    });
    // <--[tag]
    // @attribute <LocationTag.furnace_burn_duration>
    // @returns DurationTag
    // @mechanism LocationTag.furnace_burn_duration
    // @group world
    // @description
    // Returns the burn time a furnace has left.
    // -->
    tagProcessor.registerTag(DurationTag.class, "furnace_burn_duration", (attribute, object) -> {
        return new DurationTag((long) ((Furnace) object.getBlockStateForTag(attribute)).getBurnTime());
    });
    tagProcessor.registerTag(ElementTag.class, "furnace_burn_time", (attribute, object) -> {
        Deprecations.furnaceTimeTags.warn(attribute.context);
        return new ElementTag(((Furnace) object.getBlockStateForTag(attribute)).getBurnTime());
    });
    // <--[tag]
    // @attribute <LocationTag.furnace_cook_duration>
    // @returns DurationTag
    // @mechanism LocationTag.furnace_cook_duration
    // @group world
    // @description
    // Returns the cook time a furnace has been cooking its current item for.
    // -->
    tagProcessor.registerTag(DurationTag.class, "furnace_cook_duration", (attribute, object) -> {
        return new DurationTag((long) ((Furnace) object.getBlockStateForTag(attribute)).getCookTime());
    });
    tagProcessor.registerTag(ElementTag.class, "furnace_cook_time", (attribute, object) -> {
        Deprecations.furnaceTimeTags.warn(attribute.context);
        return new ElementTag(((Furnace) object.getBlockStateForTag(attribute)).getCookTime());
    });
    // <--[tag]
    // @attribute <LocationTag.furnace_cook_duration_total>
    // @returns DurationTag
    // @mechanism LocationTag.furnace_cook_duration_total
    // @group world
    // @description
    // Returns the total cook time a furnace has left.
    // -->
    tagProcessor.registerTag(DurationTag.class, "furnace_cook_duration_total", (attribute, object) -> {
        return new DurationTag((long) ((Furnace) object.getBlockStateForTag(attribute)).getCookTimeTotal());
    });
    tagProcessor.registerTag(ElementTag.class, "furnace_cook_time_total", (attribute, object) -> {
        Deprecations.furnaceTimeTags.warn(attribute.context);
        return new ElementTag(((Furnace) object.getBlockStateForTag(attribute)).getCookTimeTotal());
    });
    // <--[tag]
    // @attribute <LocationTag.beacon_tier>
    // @returns ElementTag(Number)
    // @group world
    // @description
    // Returns the tier level of a beacon pyramid (0-4).
    // -->
    tagProcessor.registerTag(ElementTag.class, "beacon_tier", (attribute, object) -> {
        return new ElementTag(((Beacon) object.getBlockStateForTag(attribute)).getTier());
    });
    // <--[tag]
    // @attribute <LocationTag.beacon_primary_effect>
    // @returns ElementTag
    // @mechanism LocationTag.beacon_primary_effect
    // @group world
    // @description
    // Returns the primary effect of the beacon. The return is simply a potion effect type name.
    // -->
    tagProcessor.registerTag(ElementTag.class, "beacon_primary_effect", (attribute, object) -> {
        PotionEffect effect = ((Beacon) object.getBlockStateForTag(attribute)).getPrimaryEffect();
        if (effect == null) {
            return null;
        }
        return new ElementTag(effect.getType().getName());
    });
    // <--[tag]
    // @attribute <LocationTag.beacon_secondary_effect>
    // @returns ElementTag
    // @mechanism LocationTag.beacon_secondary_effect
    // @group world
    // @description
    // Returns the secondary effect of the beacon. The return is simply a potion effect type name.
    // -->
    tagProcessor.registerTag(ElementTag.class, "beacon_secondary_effect", (attribute, object) -> {
        PotionEffect effect = ((Beacon) object.getBlockStateForTag(attribute)).getSecondaryEffect();
        if (effect == null) {
            return null;
        }
        return new ElementTag(effect.getType().getName());
    });
    // <--[tag]
    // @attribute <LocationTag.attached_to>
    // @returns LocationTag
    // @group world
    // @description
    // Returns the block this block is attached to.
    // (For buttons, levers, signs, torches, etc).
    // -->
    tagProcessor.registerTag(LocationTag.class, "attached_to", (attribute, object) -> {
        BlockFace face = BlockFace.SELF;
        MaterialTag material = new MaterialTag(object.getBlockForTag(attribute));
        if (material.getMaterial() == Material.TORCH || material.getMaterial() == Material.REDSTONE_TORCH || material.getMaterial() == Material.SOUL_TORCH) {
            face = BlockFace.DOWN;
        } else if (material.getMaterial() == Material.WALL_TORCH || material.getMaterial() == Material.REDSTONE_WALL_TORCH || material.getMaterial() == Material.SOUL_WALL_TORCH) {
            face = ((Directional) material.getModernData()).getFacing().getOppositeFace();
        } else if (MaterialSwitchFace.describes(material)) {
            face = MaterialSwitchFace.getFrom(material).getAttachedTo();
        } else if (material.hasModernData() && material.getModernData() instanceof org.bukkit.block.data.type.WallSign) {
            face = ((org.bukkit.block.data.type.WallSign) material.getModernData()).getFacing().getOppositeFace();
        } else {
            MaterialData data = object.getBlockStateForTag(attribute).getData();
            if (data instanceof Attachable) {
                face = ((Attachable) data).getAttachedFace();
            }
        }
        if (face != BlockFace.SELF) {
            return new LocationTag(object.getBlockForTag(attribute).getRelative(face).getLocation());
        }
        return null;
    });
    // <--[tag]
    // @attribute <LocationTag.other_block>
    // @returns LocationTag
    // @group world
    // @description
    // If the location is part of a double-block structure (double chests, double plants, doors, beds, etc),
    // returns the location of the other block in the double-block structure.
    // -->
    tagProcessor.registerTag(LocationTag.class, "other_block", (attribute, object) -> {
        Block b = object.getBlockForTag(attribute);
        MaterialTag material = new MaterialTag(b);
        if (MaterialHalf.describes(material)) {
            Vector vec = MaterialHalf.getFrom(material).getRelativeBlockVector();
            if (vec != null) {
                return new LocationTag(object.clone().add(vec));
            }
        }
        if (!attribute.hasAlternative()) {
            Debug.echoError("Block of type " + object.getBlockTypeForTag(attribute).name() + " isn't supported by other_block.");
        }
        return null;
    });
    // <--[tag]
    // @attribute <LocationTag.custom_name>
    // @returns ElementTag
    // @mechanism LocationTag.custom_name
    // @group world
    // @description
    // Returns the custom name of this block.
    // Only works for nameable blocks, such as chests and dispensers.
    // -->
    tagProcessor.registerTag(ElementTag.class, "custom_name", (attribute, object) -> {
        if (object.getBlockStateForTag(attribute) instanceof Nameable) {
            return new ElementTag(((Nameable) object.getBlockStateForTag(attribute)).getCustomName());
        }
        return null;
    });
    // <--[tag]
    // @attribute <LocationTag.local_difficulty>
    // @returns ElementTag(Decimal)
    // @group world
    // @description
    // Returns the local difficulty (damage scaler) at the location.
    // This is based internally on multiple factors, including <@link tag ChunkTag.inhabited_time> and <@link tag WorldTag.difficulty>.
    // -->
    tagProcessor.registerTag(ElementTag.class, "local_difficulty", (attribute, object) -> {
        return new ElementTag(NMSHandler.getWorldHelper().getLocalDifficulty(object));
    });
    // <--[tag]
    // @attribute <LocationTag.jukebox_record>
    // @returns ItemTag
    // @mechanism LocationTag.jukebox_record
    // @group world
    // @description
    // Returns the record item currently inside the jukebox.
    // If there's no record, will return air.
    // -->
    tagProcessor.registerTag(ItemTag.class, "jukebox_record", (attribute, object) -> {
        BlockState state = object.getBlockStateForTag(attribute);
        if (!(state instanceof Jukebox)) {
            attribute.echoError("'jukebox_record' tag is only valid for jukebox blocks.");
            return null;
        }
        return new ItemTag(((Jukebox) state).getRecord());
    });
    // <--[tag]
    // @attribute <LocationTag.jukebox_is_playing>
    // @returns ElementTag
    // @mechanism LocationTag.jukebox_play
    // @group world
    // @description
    // Returns whether the jukebox is currently playing a song.
    // -->
    tagProcessor.registerTag(ElementTag.class, "jukebox_is_playing", (attribute, object) -> {
        BlockState state = object.getBlockStateForTag(attribute);
        if (!(state instanceof Jukebox)) {
            attribute.echoError("'jukebox_is_playing' tag is only valid for jukebox blocks.");
            return null;
        }
        return new ElementTag(((Jukebox) state).isPlaying());
    });
    // <--[tag]
    // @attribute <LocationTag.age>
    // @returns DurationTag
    // @mechanism LocationTag.age
    // @group world
    // @description
    // Returns the age of an end gateway.
    // -->
    tagProcessor.registerTag(DurationTag.class, "age", (attribute, object) -> {
        BlockState state = object.getBlockStateForTag(attribute);
        if (!(state instanceof EndGateway)) {
            attribute.echoError("'age' tag is only valid for end_gateway blocks.");
            return null;
        }
        return new DurationTag(((EndGateway) state).getAge());
    });
    // <--[tag]
    // @attribute <LocationTag.is_exact_teleport>
    // @returns ElementTag(Boolean)
    // @mechanism LocationTag.is_exact_teleport
    // @group world
    // @description
    // Returns whether an end gateway is 'exact teleport' - if false, the destination will be randomly chosen *near* the destination.
    // -->
    tagProcessor.registerTag(ElementTag.class, "is_exact_teleport", (attribute, object) -> {
        BlockState state = object.getBlockStateForTag(attribute);
        if (!(state instanceof EndGateway)) {
            attribute.echoError("'is_exact_teleport' tag is only valid for end_gateway blocks.");
            return null;
        }
        return new ElementTag(((EndGateway) state).isExactTeleport());
    });
    // <--[tag]
    // @attribute <LocationTag.exit_location>
    // @returns LocationTag
    // @mechanism LocationTag.exit_location
    // @group world
    // @description
    // Returns the exit location of an end gateway block.
    // -->
    tagProcessor.registerTag(LocationTag.class, "exit_location", (attribute, object) -> {
        BlockState state = object.getBlockStateForTag(attribute);
        if (!(state instanceof EndGateway)) {
            attribute.echoError("'exit_location' tag is only valid for end_gateway blocks.");
            return null;
        }
        Location loc = ((EndGateway) state).getExitLocation();
        if (loc == null) {
            return null;
        }
        return new LocationTag(loc);
    });
    // <--[tag]
    // @attribute <LocationTag.is_in[<matcher>]>
    // @returns ElementTag(Boolean)
    // @group areas
    // @description
    // Returns whether the location is in an area, using the same logic as an event "in" switch.
    // Invalid input may produce odd error messages, as this is passed through the event system as a fake event.
    // -->
    tagProcessor.registerTag(ElementTag.class, "is_in", (attribute, object) -> {
        if (!attribute.hasParam()) {
            return null;
        }
        return new ElementTag(BukkitScriptEvent.inCheckInternal(attribute.context, "is_in tag", object, attribute.getParam(), "is_in tag", "is_in tag"));
    });
    // <--[tag]
    // @attribute <LocationTag.campfire_items>
    // @returns ListTag(ItemTag)
    // @mechanism LocationTag.campfire_items
    // @group world
    // @description
    // Returns a list of items currently in this campfire.
    // This list has air items in empty slots, and is always sized exactly the same as the number of spaces a campfire has.
    // (A standard campfire has exactly 4 slots).
    // -->
    tagProcessor.registerTag(ListTag.class, "campfire_items", (attribute, object) -> {
        BlockState state = object.getBlockStateForTag(attribute);
        if (!(state instanceof Campfire)) {
            return null;
        }
        Campfire fire = (Campfire) state;
        ListTag output = new ListTag();
        for (int i = 0; i < fire.getSize(); i++) {
            output.addObject(new ItemTag(fire.getItem(i)));
        }
        return output;
    });
    // <--[tag]
    // @attribute <LocationTag.is_spawnable>
    // @returns ElementTag(Boolean)
    // @group world
    // @description
    // Returns whether the location is safe to spawn at, for a player or player-like entity.
    // Specifically this verifies that:
    // - The block above this location is air.
    // - The block at this location is non-solid.
    // - The block below this location is solid.
    // - All relevant blocks are not dangerous (like fire, lava, etc.), or unstable/small/awkward (like fences, doors, etc.) or otherwise likely to go wrong (like pressure plates).
    // -->
    tagProcessor.registerTag(ElementTag.class, "is_spawnable", (attribute, object) -> {
        return new ElementTag(SpawnableHelper.isSpawnable(object));
    });
    // <--[tag]
    // @attribute <LocationTag.sign_glowing>
    // @returns ElementTag(Boolean)
    // @mechanism LocationTag.sign_glowing
    // @group world
    // @description
    // Returns whether the location is a Sign block that is glowing.
    // -->
    tagProcessor.registerTag(ElementTag.class, "sign_glowing", (attribute, object) -> {
        BlockState state = object.getBlockStateForTag(attribute);
        if (!(state instanceof Sign)) {
            attribute.echoError("Location is not a valid Sign block.");
            return null;
        }
        return new ElementTag(((Sign) state).isGlowingText());
    });
    // <--[tag]
    // @attribute <LocationTag.sign_glow_color>
    // @returns ElementTag
    // @mechanism LocationTag.sign_glow_color
    // @group world
    // @description
    // Returns the name of the glow-color of the sign at the location.
    // See also <@link tag LocationTag.sign_glowing>
    // -->
    tagProcessor.registerTag(ElementTag.class, "sign_glow_color", (attribute, object) -> {
        BlockState state = object.getBlockStateForTag(attribute);
        if (!(state instanceof Sign)) {
            attribute.echoError("Location is not a valid Sign block.");
            return null;
        }
        return new ElementTag(((Sign) state).getColor().name());
    });
}
Also used : PatternType(org.bukkit.block.banner.PatternType) MaterialPersistent(com.denizenscript.denizen.objects.properties.material.MaterialPersistent) Utilities(com.denizenscript.denizen.utilities.Utilities) MaterialDirectional(com.denizenscript.denizen.objects.properties.material.MaterialDirectional) NMSHandler(com.denizenscript.denizen.nms.NMSHandler) NoteManager(com.denizenscript.denizencore.objects.notable.NoteManager) DataPersistenceFlagTracker(com.denizenscript.denizen.utilities.flags.DataPersistenceFlagTracker) Inventory(org.bukkit.inventory.Inventory) MaterialData(org.bukkit.material.MaterialData) org.bukkit(org.bukkit) CitizensAPI(net.citizensnpcs.api.CitizensAPI) org.bukkit.block(org.bukkit.block) TagContext(com.denizenscript.denizencore.tags.TagContext) ElementTag(com.denizenscript.denizencore.objects.core.ElementTag) org.bukkit.entity(org.bukkit.entity) ListTag(com.denizenscript.denizencore.objects.core.ListTag) DurationTag(com.denizenscript.denizencore.objects.core.DurationTag) BukkitTagContext(com.denizenscript.denizen.tags.BukkitTagContext) PlayerProfile(com.denizenscript.denizen.nms.util.PlayerProfile) MapTag(com.denizenscript.denizencore.objects.core.MapTag) AbstractFlagTracker(com.denizenscript.denizencore.flags.AbstractFlagTracker) BukkitScriptEvent(com.denizenscript.denizen.events.BukkitScriptEvent) Settings(com.denizenscript.denizen.utilities.Settings) ItemStack(org.bukkit.inventory.ItemStack) Note(com.denizenscript.denizencore.objects.notable.Note) EntityHelper(com.denizenscript.denizen.nms.interfaces.EntityHelper) SwitchCommand(com.denizenscript.denizen.scripts.commands.world.SwitchCommand) Deprecations(com.denizenscript.denizencore.utilities.Deprecations) PotionEffectType(org.bukkit.potion.PotionEffectType) MaterialSwitchFace(com.denizenscript.denizen.objects.properties.material.MaterialSwitchFace) java.util(java.util) ObjectTagProcessor(com.denizenscript.denizencore.tags.ObjectTagProcessor) AdvancedTextImpl(com.denizenscript.denizen.utilities.AdvancedTextImpl) Lootable(org.bukkit.loot.Lootable) NPC(net.citizensnpcs.api.npc.NPC) MaterialHalf(com.denizenscript.denizen.objects.properties.material.MaterialHalf) Attachable(org.bukkit.material.Attachable) PathFinder(com.denizenscript.denizen.utilities.world.PathFinder) Directional(org.bukkit.block.data.Directional) Attribute(com.denizenscript.denizencore.tags.Attribute) FlaggableObject(com.denizenscript.denizencore.flags.FlaggableObject) LootTable(org.bukkit.loot.LootTable) InventoryType(org.bukkit.event.inventory.InventoryType) SpawnableHelper(com.denizenscript.denizen.utilities.blocks.SpawnableHelper) DenizenEntityType(com.denizenscript.denizen.utilities.entity.DenizenEntityType) LocationFlagSearchHelper(com.denizenscript.denizen.utilities.flags.LocationFlagSearchHelper) Notable(com.denizenscript.denizencore.objects.notable.Notable) BiomeNMS(com.denizenscript.denizen.nms.abstracts.BiomeNMS) PotionEffect(org.bukkit.potion.PotionEffect) InventoryHolder(org.bukkit.inventory.InventoryHolder) Vector(org.bukkit.util.Vector) org.bukkit.util(org.bukkit.util) com.denizenscript.denizencore.objects(com.denizenscript.denizencore.objects) Debug(com.denizenscript.denizen.utilities.debugging.Debug) CoreUtilities(com.denizenscript.denizencore.utilities.CoreUtilities) org.bukkit(org.bukkit) Lootable(org.bukkit.loot.Lootable) PotionEffect(org.bukkit.potion.PotionEffect) org.bukkit.block(org.bukkit.block) Vector(org.bukkit.util.Vector) PlayerProfile(com.denizenscript.denizen.nms.util.PlayerProfile) DurationTag(com.denizenscript.denizencore.objects.core.DurationTag) DenizenEntityType(com.denizenscript.denizen.utilities.entity.DenizenEntityType) java.util(java.util) org.bukkit.util(org.bukkit.util) ElementTag(com.denizenscript.denizencore.objects.core.ElementTag) MaterialData(org.bukkit.material.MaterialData) ItemStack(org.bukkit.inventory.ItemStack) NPC(net.citizensnpcs.api.npc.NPC) InventoryHolder(org.bukkit.inventory.InventoryHolder) LootTable(org.bukkit.loot.LootTable) EntityHelper(com.denizenscript.denizen.nms.interfaces.EntityHelper) ListTag(com.denizenscript.denizencore.objects.core.ListTag) Vector(org.bukkit.util.Vector) Attachable(org.bukkit.material.Attachable)

Example 3 with LootTable

use of org.bukkit.loot.LootTable in project Denizen-For-Bukkit by DenizenScript.

the class EntityTag method registerTags.

public static void registerTags() {
    AbstractFlagTracker.registerFlagHandlers(tagProcessor);
    PropertyParser.registerPropertyTagHandlers(EntityTag.class, tagProcessor);
    // ///////////////////
    // UNSPAWNED ATTRIBUTES
    // ///////////////
    // <--[tag]
    // @attribute <EntityTag.entity_type>
    // @returns ElementTag
    // @group data
    // @description
    // Returns the type of the entity.
    // -->
    tagProcessor.registerTag(ElementTag.class, "entity_type", (attribute, object) -> {
        return new ElementTag(object.entity_type.getName());
    });
    // <--[tag]
    // @attribute <EntityTag.translated_name>
    // @returns ElementTag
    // @description
    // Returns the localized name of the entity.
    // Note that this is a magic Denizen tool - refer to <@link language Denizen Text Formatting>.
    // -->
    tagProcessor.registerTag(ElementTag.class, "translated_name", (attribute, object) -> {
        String key = object.getEntityType().getBukkitEntityType().getKey().getKey();
        return new ElementTag(ChatColor.COLOR_CHAR + "[translate=entity.minecraft." + key + "]");
    });
    // <--[tag]
    // @attribute <EntityTag.is_spawned>
    // @returns ElementTag(Boolean)
    // @group data
    // @description
    // Returns whether the entity is spawned.
    // -->
    tagProcessor.registerTag(ElementTag.class, "is_spawned", (attribute, object) -> {
        return new ElementTag(object.isSpawned());
    });
    // <--[tag]
    // @attribute <EntityTag.eid>
    // @returns ElementTag(Number)
    // @group data
    // @description
    // Returns the entity's temporary server entity ID.
    // -->
    registerSpawnedOnlyTag(ElementTag.class, "eid", (attribute, object) -> {
        return new ElementTag(object.getBukkitEntity().getEntityId());
    });
    // <--[tag]
    // @attribute <EntityTag.uuid>
    // @returns ElementTag
    // @group data
    // @description
    // Returns the permanent unique ID of the entity.
    // Works with offline players.
    // -->
    tagProcessor.registerTag(ElementTag.class, "uuid", (attribute, object) -> {
        return new ElementTag(object.getUUID().toString());
    });
    // <--[tag]
    // @attribute <EntityTag.script>
    // @returns ScriptTag
    // @group data
    // @description
    // Returns the entity script that spawned this entity, if any.
    // -->
    tagProcessor.registerTag(ScriptTag.class, "script", (attribute, object) -> {
        if (object.entityScript == null) {
            return null;
        }
        ScriptTag tag = new ScriptTag(object.entityScript);
        if (tag.isValid()) {
            return tag;
        }
        return null;
    });
    // <--[tag]
    // @attribute <EntityTag.scriptname>
    // @returns ElementTag
    // @deprecated use ".script.name" instead.
    // @group data
    // @description
    // Use ".script.name" instead.
    // -->
    tagProcessor.registerTag(ElementTag.class, "scriptname", (attribute, object) -> {
        Deprecations.hasScriptTags.warn(attribute.context);
        if (object.entityScript == null) {
            return null;
        }
        return new ElementTag(object.entityScript);
    });
    // ///////////////////
    // IDENTIFICATION ATTRIBUTES
    // ///////////////
    registerSpawnedOnlyTag(ObjectTag.class, "custom_id", (attribute, object) -> {
        Deprecations.entityCustomIdTag.warn(attribute.context);
        if (CustomNBT.hasCustomNBT(object.getLivingEntity(), "denizen-script-id")) {
            return new ScriptTag(CustomNBT.getCustomNBT(object.getLivingEntity(), "denizen-script-id"));
        } else {
            return new ElementTag(object.getBukkitEntity().getType().name());
        }
    });
    // <--[tag]
    // @attribute <EntityTag.name>
    // @returns ElementTag
    // @group data
    // @description
    // Returns the name of the entity.
    // This can be a player name, an NPC name, a custom_name, or the entity type.
    // Works with offline players.
    // -->
    registerSpawnedOnlyTag(ElementTag.class, "name", (attribute, object) -> {
        return new ElementTag(object.getName(), true);
    });
    // ///////////////////
    // INVENTORY ATTRIBUTES
    // ///////////////
    // <--[tag]
    // @attribute <EntityTag.saddle>
    // @returns ItemTag
    // @group inventory
    // @description
    // If the entity is a horse or pig, returns the saddle as a ItemTag, or air if none.
    // -->
    registerSpawnedOnlyTag(ItemTag.class, "saddle", (attribute, object) -> {
        if (object.getLivingEntity() instanceof AbstractHorse) {
            return new ItemTag(((AbstractHorse) object.getLivingEntity()).getInventory().getSaddle());
        } else if (object.getLivingEntity() instanceof Steerable) {
            return new ItemTag(((Steerable) object.getLivingEntity()).hasSaddle() ? Material.SADDLE : Material.AIR);
        }
        return null;
    });
    // <--[tag]
    // @attribute <EntityTag.horse_armor>
    // @returns ItemTag
    // @group inventory
    // @description
    // If the entity is a horse, returns the item equipped as the horses armor, or air if none.
    // -->
    registerSpawnedOnlyTag(ItemTag.class, "horse_armor", (attribute, object) -> {
        if (object.getLivingEntity() instanceof Horse) {
            return new ItemTag(((Horse) object.getLivingEntity()).getInventory().getArmor());
        }
        return null;
    }, "horse_armour");
    // <--[tag]
    // @attribute <EntityTag.has_saddle>
    // @returns ElementTag(Boolean)
    // @group inventory
    // @description
    // If the entity is a pig or horse, returns whether it has a saddle equipped.
    // -->
    registerSpawnedOnlyTag(ElementTag.class, "has_saddle", (attribute, object) -> {
        if (object.getLivingEntity() instanceof AbstractHorse) {
            return new ElementTag(((AbstractHorse) object.getLivingEntity()).getInventory().getSaddle().getType() == Material.SADDLE);
        } else if (object.getLivingEntity() instanceof Steerable) {
            return new ElementTag(((Steerable) object.getLivingEntity()).hasSaddle());
        }
        return null;
    });
    // <--[tag]
    // @attribute <EntityTag.is_trading>
    // @returns ElementTag(Boolean)
    // @description
    // Returns whether the villager entity is trading.
    // -->
    registerSpawnedOnlyTag(ElementTag.class, "is_trading", (attribute, object) -> {
        if (object.getBukkitEntity() instanceof Merchant) {
            return new ElementTag(((Merchant) object.getBukkitEntity()).isTrading());
        }
        return null;
    });
    // <--[tag]
    // @attribute <EntityTag.trading_with>
    // @returns PlayerTag
    // @description
    // Returns the player who is trading with the villager entity, or null if it is not trading.
    // -->
    registerSpawnedOnlyTag(EntityFormObject.class, "trading_with", (attribute, object) -> {
        if (object.getBukkitEntity() instanceof Merchant && ((Merchant) object.getBukkitEntity()).getTrader() != null) {
            return new EntityTag(((Merchant) object.getBukkitEntity()).getTrader()).getDenizenObject();
        }
        return null;
    });
    // ///////////////////
    // LOCATION ATTRIBUTES
    // ///////////////
    // <--[tag]
    // @attribute <EntityTag.map_trace>
    // @returns LocationTag
    // @group location
    // @description
    // Returns a 2D location indicating where on the map the entity's looking at.
    // Each coordinate is in the range of 0 to 128.
    // -->
    registerSpawnedOnlyTag(LocationTag.class, "map_trace", (attribute, object) -> {
        EntityHelper.MapTraceResult mtr = NMSHandler.getEntityHelper().mapTrace(object.getLivingEntity(), 200);
        if (mtr != null) {
            double x = 0;
            double y;
            double basex = mtr.hitLocation.getX() - Math.floor(mtr.hitLocation.getX());
            double basey = mtr.hitLocation.getY() - Math.floor(mtr.hitLocation.getY());
            double basez = mtr.hitLocation.getZ() - Math.floor(mtr.hitLocation.getZ());
            if (mtr.angle == BlockFace.NORTH) {
                x = 128f - (basex * 128f);
            } else if (mtr.angle == BlockFace.SOUTH) {
                x = basex * 128f;
            } else if (mtr.angle == BlockFace.WEST) {
                x = basez * 128f;
            } else if (mtr.angle == BlockFace.EAST) {
                x = 128f - (basez * 128f);
            }
            y = 128f - (basey * 128f);
            return new LocationTag(null, Math.round(x), Math.round(y));
        }
        return null;
    });
    // <--[tag]
    // @attribute <EntityTag.can_see[<entity>]>
    // @returns ElementTag(Boolean)
    // @group location
    // @description
    // Returns whether the entity can see the specified other entity (has an uninterrupted line-of-sight).
    // -->
    registerSpawnedOnlyTag(ElementTag.class, "can_see", (attribute, object) -> {
        if (object.isLivingEntity() && attribute.hasParam() && EntityTag.matches(attribute.getParam())) {
            EntityTag toEntity = attribute.paramAsType(EntityTag.class);
            if (toEntity != null && toEntity.isSpawnedOrValidForTag()) {
                return new ElementTag(object.getLivingEntity().hasLineOfSight(toEntity.getBukkitEntity()));
            }
        }
        return null;
    });
    // <--[tag]
    // @attribute <EntityTag.eye_location>
    // @returns LocationTag
    // @group location
    // @description
    // Returns the location of the entity's eyes.
    // -->
    registerSpawnedOnlyTag(LocationTag.class, "eye_location", (attribute, object) -> {
        return new LocationTag(object.getEyeLocation());
    });
    // <--[tag]
    // @attribute <EntityTag.eye_height>
    // @returns ElementTag(Number)
    // @group location
    // @description
    // Returns the height of the entity's eyes above its location.
    // -->
    registerSpawnedOnlyTag(ElementTag.class, "eye_height", (attribute, object) -> {
        if (object.isLivingEntity()) {
            return new ElementTag(object.getLivingEntity().getEyeHeight());
        }
        return null;
    });
    // <--[tag]
    // @attribute <EntityTag.cursor_on_solid[(<range>)]>
    // @returns LocationTag
    // @group location
    // @description
    // Returns the location of the solid block the entity is looking at.
    // Optionally, specify a maximum range to find the location from (defaults to 200).
    // This uses logic equivalent to <@link tag LocationTag.precise_cursor_on_block[(range)]>.
    // Note that this will return null if there is no solid block in range.
    // This only uses solid blocks, ie it ignores passable blocks like tall-grass. Use <@link tag EntityTag.cursor_on> to include passable blocks.
    // -->
    registerSpawnedOnlyTag(LocationTag.class, "cursor_on_solid", (attribute, object) -> {
        double range = attribute.getDoubleParam();
        if (range <= 0) {
            range = 200;
        }
        RayTraceResult traced = object.getWorld().rayTraceBlocks(object.getEyeLocation(), object.getEyeLocation().getDirection(), range, FluidCollisionMode.NEVER, true);
        if (traced != null && traced.getHitBlock() != null) {
            return new LocationTag(traced.getHitBlock().getLocation());
        }
        return null;
    });
    // <--[tag]
    // @attribute <EntityTag.cursor_on[(<range>)]>
    // @returns LocationTag
    // @group location
    // @description
    // Returns the location of the block the entity is looking at.
    // Optionally, specify a maximum range to find the location from (defaults to 200).
    // This uses logic equivalent to <@link tag LocationTag.precise_cursor_on_block[(range)]>.
    // Note that this will return null if there is no block in range.
    // This uses all blocks, ie it includes passable blocks like tall-grass and water. Use <@link tag EntityTag.cursor_on_solid> to exclude passable blocks.
    // -->
    registerSpawnedOnlyTag(LocationTag.class, "cursor_on", (attribute, object) -> {
        double range = attribute.getDoubleParam();
        if (range <= 0) {
            range = 200;
        }
        RayTraceResult traced = object.getWorld().rayTraceBlocks(object.getEyeLocation(), object.getEyeLocation().getDirection(), range, FluidCollisionMode.ALWAYS, false);
        if (traced != null && traced.getHitBlock() != null) {
            return new LocationTag(traced.getHitBlock().getLocation());
        }
        return null;
    });
    // <--[tag]
    // @attribute <EntityTag.location>
    // @returns LocationTag
    // @group location
    // @description
    // Returns the location of the entity.
    // For living entities, this is at the center of their feet.
    // For eye location, use <@link tag EntityTag.eye_location>
    // Works with offline players.
    // -->
    registerSpawnedOnlyTag(LocationTag.class, "location", (attribute, object) -> {
        return object.doLocationTag(attribute);
    });
    // <--[tag]
    // @attribute <EntityTag.standing_on>
    // @returns LocationTag
    // @group location
    // @description
    // Returns the location of the block the entity is standing on top of (if on the ground, returns null if in the air).
    // -->
    registerSpawnedOnlyTag(LocationTag.class, "standing_on", (attribute, object) -> {
        if (!object.getBukkitEntity().isOnGround()) {
            return null;
        }
        Location loc = object.getBukkitEntity().getLocation().clone().subtract(0, 0.05f, 0);
        return new LocationTag(loc.getWorld(), loc.getBlockX(), loc.getBlockY(), loc.getBlockZ());
    });
    // <--[tag]
    // @attribute <EntityTag.body_yaw>
    // @returns ElementTag(Decimal)
    // @group location
    // @description
    // Returns the entity's body yaw (separate from head yaw).
    // -->
    registerSpawnedOnlyTag(ElementTag.class, "body_yaw", (attribute, object) -> {
        return new ElementTag(NMSHandler.getEntityHelper().getBaseYaw(object.getBukkitEntity()));
    });
    // <--[tag]
    // @attribute <EntityTag.velocity>
    // @returns LocationTag
    // @group location
    // @mechanism EntityTag.velocity
    // @description
    // Returns the movement velocity of the entity.
    // Note: Does not accurately calculate player clientside movement velocity.
    // -->
    registerSpawnedOnlyTag(LocationTag.class, "velocity", (attribute, object) -> {
        return new LocationTag(object.getBukkitEntity().getVelocity().toLocation(object.getBukkitEntity().getWorld()));
    });
    // <--[tag]
    // @attribute <EntityTag.world>
    // @returns WorldTag
    // @group location
    // @description
    // Returns the world the entity is in. Works with offline players.
    // -->
    registerSpawnedOnlyTag(WorldTag.class, "world", (attribute, object) -> {
        return new WorldTag(object.getBukkitEntity().getWorld());
    });
    // ///////////////////
    // STATE ATTRIBUTES
    // ///////////////
    // <--[tag]
    // @attribute <EntityTag.can_pickup_items>
    // @returns ElementTag(Boolean)
    // @mechanism EntityTag.can_pickup_items
    // @group attributes
    // @description
    // Returns whether the entity can pick up items.
    // -->
    registerSpawnedOnlyTag(ElementTag.class, "can_pickup_items", (attribute, object) -> {
        if (object.isLivingEntity()) {
            return new ElementTag(object.getLivingEntity().getCanPickupItems());
        }
        return null;
    });
    // <--[tag]
    // @attribute <EntityTag.fallingblock_material>
    // @returns MaterialTag
    // @mechanism EntityTag.fallingblock_type
    // @group attributes
    // @description
    // Returns the material of a fallingblock-type entity.
    // -->
    registerSpawnedOnlyTag(MaterialTag.class, "fallingblock_material", (attribute, object) -> {
        if (!(object.getBukkitEntity() instanceof FallingBlock)) {
            return null;
        }
        return new MaterialTag(((FallingBlock) object.getBukkitEntity()).getBlockData());
    });
    // <--[tag]
    // @attribute <EntityTag.fall_distance>
    // @returns ElementTag(Decimal)
    // @mechanism EntityTag.fall_distance
    // @group attributes
    // @description
    // Returns how far the entity has fallen.
    // -->
    registerSpawnedOnlyTag(ElementTag.class, "fall_distance", (attribute, object) -> {
        return new ElementTag(object.getBukkitEntity().getFallDistance());
    });
    // <--[tag]
    // @attribute <EntityTag.fire_time>
    // @returns DurationTag
    // @mechanism EntityTag.fire_time
    // @group attributes
    // @description
    // Returns the duration for which the entity will remain on fire
    // -->
    registerSpawnedOnlyTag(DurationTag.class, "fire_time", (attribute, object) -> {
        return new DurationTag(object.getBukkitEntity().getFireTicks() / 20);
    });
    // <--[tag]
    // @attribute <EntityTag.on_fire>
    // @returns ElementTag(Boolean)
    // @group attributes
    // @description
    // Returns whether the entity is currently ablaze or not.
    // -->
    registerSpawnedOnlyTag(ElementTag.class, "on_fire", (attribute, object) -> {
        return new ElementTag(object.getBukkitEntity().getFireTicks() > 0);
    });
    // <--[tag]
    // @attribute <EntityTag.leash_holder>
    // @returns EntityTag
    // @mechanism EntityTag.leash_holder
    // @group attributes
    // @description
    // Returns the leash holder of entity.
    // -->
    registerSpawnedOnlyTag(EntityFormObject.class, "leash_holder", (attribute, object) -> {
        if (object.isLivingEntity() && object.getLivingEntity().isLeashed()) {
            return new EntityTag(object.getLivingEntity().getLeashHolder()).getDenizenObject();
        }
        return null;
    }, "get_leash_holder");
    // <--[tag]
    // @attribute <EntityTag.passengers>
    // @returns ListTag(EntityTag)
    // @mechanism EntityTag.passengers
    // @group attributes
    // @description
    // Returns a list of the entity's passengers, if any.
    // -->
    registerSpawnedOnlyTag(ListTag.class, "passengers", (attribute, object) -> {
        ArrayList<EntityTag> passengers = new ArrayList<>();
        for (Entity ent : object.getBukkitEntity().getPassengers()) {
            passengers.add(new EntityTag(ent));
        }
        return new ListTag(passengers);
    }, "get_passengers");
    // <--[tag]
    // @attribute <EntityTag.passenger>
    // @returns EntityTag
    // @mechanism EntityTag.passenger
    // @group attributes
    // @description
    // Returns the entity's passenger, if any.
    // -->
    registerSpawnedOnlyTag(EntityFormObject.class, "passenger", (attribute, object) -> {
        if (!object.getBukkitEntity().isEmpty()) {
            return new EntityTag(object.getBukkitEntity().getPassenger()).getDenizenObject();
        }
        return null;
    }, "get_passenger");
    // <--[tag]
    // @attribute <EntityTag.shooter>
    // @returns EntityTag
    // @group attributes
    // @mechanism EntityTag.shooter
    // @synonyms EntityTag.arrow_firer,EntityTag.fishhook_shooter,EntityTag.snowball_thrower
    // @description
    // Returns the projectile's shooter, if any.
    // -->
    registerSpawnedOnlyTag(EntityFormObject.class, "shooter", (attribute, object) -> {
        EntityTag shooter = object.getShooter();
        if (shooter == null) {
            return null;
        }
        return shooter.getDenizenObject();
    }, "get_shooter");
    // <--[tag]
    // @attribute <EntityTag.left_shoulder>
    // @returns EntityTag
    // @mechanism EntityTag.left_shoulder
    // @description
    // Returns the entity on the entity's left shoulder.
    // Only applies to player-typed entities.
    // NOTE: The returned entity will not be spawned within the world,
    // so most operations are invalid unless the entity is first spawned in.
    // -->
    registerSpawnedOnlyTag(EntityFormObject.class, "left_shoulder", (attribute, object) -> {
        if (!(object.getLivingEntity() instanceof HumanEntity)) {
            return null;
        }
        Entity e = ((HumanEntity) object.getLivingEntity()).getShoulderEntityLeft();
        if (e == null) {
            return null;
        }
        return new EntityTag(e).getDenizenObject();
    });
    // <--[tag]
    // @attribute <EntityTag.right_shoulder>
    // @returns EntityTag
    // @mechanism EntityTag.right_shoulder
    // @description
    // Returns the entity on the entity's right shoulder.
    // Only applies to player-typed entities.
    // NOTE: The returned entity will not be spawned within the world,
    // so most operations are invalid unless the entity is first spawned in.
    // -->
    registerSpawnedOnlyTag(EntityFormObject.class, "right_shoulder", (attribute, object) -> {
        if (!(object.getLivingEntity() instanceof HumanEntity)) {
            return null;
        }
        Entity e = ((HumanEntity) object.getLivingEntity()).getShoulderEntityRight();
        if (e == null) {
            return null;
        }
        return new EntityTag(e).getDenizenObject();
    });
    // <--[tag]
    // @attribute <EntityTag.vehicle>
    // @returns EntityTag
    // @group attributes
    // @description
    // If the entity is in a vehicle, returns the vehicle as a EntityTag.
    // -->
    registerSpawnedOnlyTag(EntityFormObject.class, "vehicle", (attribute, object) -> {
        if (object.getBukkitEntity().isInsideVehicle()) {
            return new EntityTag(object.getBukkitEntity().getVehicle()).getDenizenObject();
        }
        return null;
    }, "get_vehicle");
    // <--[tag]
    // @attribute <EntityTag.can_breed>
    // @returns ElementTag(Boolean)
    // @mechanism EntityTag.can_breed
    // @group attributes
    // @description
    // Returns whether the animal entity is capable of mating with another of its kind.
    // -->
    registerSpawnedOnlyTag(ElementTag.class, "can_breed", (attribute, object) -> {
        if (!(object.getLivingEntity() instanceof Breedable)) {
            return new ElementTag(false);
        }
        return new ElementTag(((Breedable) object.getLivingEntity()).canBreed());
    });
    // <--[tag]
    // @attribute <EntityTag.breeding>
    // @returns ElementTag(Boolean)
    // @mechanism EntityTag.breed
    // @group attributes
    // @description
    // Returns whether the animal entity is trying to with another of its kind.
    // -->
    registerSpawnedOnlyTag(ElementTag.class, "breeding", (attribute, object) -> {
        if (!(object.getLivingEntity() instanceof Animals)) {
            return null;
        }
        return new ElementTag(((Animals) object.getLivingEntity()).getLoveModeTicks() > 0);
    }, "is_breeding");
    // <--[tag]
    // @attribute <EntityTag.has_passenger>
    // @returns ElementTag(Boolean)
    // @mechanism EntityTag.passenger
    // @group attributes
    // @description
    // Returns whether the entity has a passenger.
    // -->
    registerSpawnedOnlyTag(ElementTag.class, "has_passenger", (attribute, object) -> {
        return new ElementTag(!object.getBukkitEntity().isEmpty());
    });
    // <--[tag]
    // @attribute <EntityTag.is_empty>
    // @returns ElementTag(Boolean)
    // @group attributes
    // @description
    // Returns whether the entity does not have a passenger.
    // -->
    registerSpawnedOnlyTag(ElementTag.class, "is_empty", (attribute, object) -> {
        return new ElementTag(object.getBukkitEntity().isEmpty());
    }, "empty");
    // <--[tag]
    // @attribute <EntityTag.is_inside_vehicle>
    // @returns ElementTag(Boolean)
    // @group attributes
    // @description
    // Returns whether the entity is inside a vehicle.
    // -->
    registerSpawnedOnlyTag(ElementTag.class, "is_inside_vehicle", (attribute, object) -> {
        return new ElementTag(object.getBukkitEntity().isInsideVehicle());
    }, "inside_vehicle");
    // <--[tag]
    // @attribute <EntityTag.is_leashed>
    // @returns ElementTag(Boolean)
    // @group attributes
    // @description
    // Returns whether the entity is leashed.
    // -->
    registerSpawnedOnlyTag(ElementTag.class, "is_leashed", (attribute, object) -> {
        return new ElementTag(object.isLivingEntity() && object.getLivingEntity().isLeashed());
    }, "leashed");
    // <--[tag]
    // @attribute <EntityTag.is_sheared>
    // @returns ElementTag(Boolean)
    // @group attributes
    // @description
    // Returns whether a sheep is sheared.
    // -->
    registerSpawnedOnlyTag(ElementTag.class, "is_sheared", (attribute, object) -> {
        if (!(object.getBukkitEntity() instanceof Sheep)) {
            return null;
        }
        return new ElementTag(((Sheep) object.getBukkitEntity()).isSheared());
    });
    // <--[tag]
    // @attribute <EntityTag.is_on_ground>
    // @returns ElementTag(Boolean)
    // @group attributes
    // @description
    // Returns whether the entity is supported by a block.
    // This can be inaccurate for players.
    // -->
    registerSpawnedOnlyTag(ElementTag.class, "is_on_ground", (attribute, object) -> {
        return new ElementTag(object.getBukkitEntity().isOnGround());
    }, "on_ground");
    // <--[tag]
    // @attribute <EntityTag.is_persistent>
    // @returns ElementTag(Boolean)
    // @group attributes
    // @mechanism EntityTag.persistent
    // @description
    // Returns whether the entity will not be removed completely when far away from players.
    // In other words: whether the entity should be saved to file when chunks unload (otherwise, the entity is gone entirely if despawned for any reason).
    // -->
    // <--[tag]
    // @attribute <EntityTag.persistent>
    // @returns ElementTag(Boolean)
    // @group attributes
    // @mechanism EntityTag.persistent
    // @deprecated use 'is_persistent'
    // @description
    // Outdated form of <@link tag EntityTag.is_persistent>
    // -->
    registerSpawnedOnlyTag(ElementTag.class, "is_persistent", (attribute, object) -> {
        return new ElementTag(object.isLivingEntity() && !object.getLivingEntity().getRemoveWhenFarAway());
    }, "persistent");
    // <--[tag]
    // @attribute <EntityTag.is_collidable>
    // @returns ElementTag(Boolean)
    // @mechanism EntityTag.collidable
    // @group attributes
    // @description
    // Returns whether the entity is collidable.
    // Returns the persistent collidable value for NPCs.
    // -->
    registerSpawnedOnlyTag(ElementTag.class, "is_collidable", (attribute, object) -> {
        if (object.isCitizensNPC()) {
            return new ElementTag(object.getDenizenNPC().getCitizen().data().get(NPC.COLLIDABLE_METADATA, true));
        }
        return new ElementTag(object.getLivingEntity().isCollidable());
    });
    // <--[tag]
    // @attribute <EntityTag.is_sleeping>
    // @returns ElementTag(Boolean)
    // @description
    // Returns whether the player, NPC, or villager is currently sleeping.
    // -->
    registerSpawnedOnlyTag(ElementTag.class, "is_sleeping", (attribute, object) -> {
        if (object.getBukkitEntity() instanceof Player) {
            return new ElementTag(((Player) object.getBukkitEntity()).isSleeping());
        } else if (object.getBukkitEntity() instanceof Villager) {
            return new ElementTag(((Villager) object.getBukkitEntity()).isSleeping());
        }
        return null;
    });
    // <--[tag]
    // @attribute <EntityTag.killer>
    // @returns PlayerTag
    // @group attributes
    // @description
    // Returns the player that last killed the entity.
    // -->
    registerSpawnedOnlyTag(PlayerTag.class, "killer", (attribute, object) -> {
        return getPlayerFrom(object.getLivingEntity().getKiller());
    });
    registerSpawnedOnlyTag(ObjectTag.class, "last_damage", (attribute, object) -> {
        // -->
        if (attribute.startsWith("amount", 2)) {
            attribute.fulfill(1);
            return new ElementTag(object.getLivingEntity().getLastDamage());
        }
        // -->
        if (attribute.startsWith("cause", 2)) {
            attribute.fulfill(1);
            if (object.getBukkitEntity().getLastDamageCause() == null) {
                return null;
            }
            return new ElementTag(object.getBukkitEntity().getLastDamageCause().getCause().name());
        }
        // -->
        if (attribute.startsWith("duration", 2)) {
            attribute.fulfill(1);
            return new DurationTag((long) object.getLivingEntity().getNoDamageTicks());
        }
        // -->
        if (attribute.startsWith("max_duration", 2)) {
            attribute.fulfill(1);
            return new DurationTag((long) object.getLivingEntity().getMaximumNoDamageTicks());
        }
        return null;
    });
    // <--[tag]
    // @attribute <EntityTag.absorption_health>
    // @returns ElementTag(Decimal)
    // @mechanism EntityTag.absorption_health
    // @description
    // Returns the living entity's absorption health.
    // -->
    registerSpawnedOnlyTag(ElementTag.class, "absorption_health", (attribute, object) -> {
        return new ElementTag(NMSHandler.getEntityHelper().getAbsorption(object.getLivingEntity()));
    });
    // <--[tag]
    // @attribute <EntityTag.max_oxygen>
    // @returns DurationTag
    // @group attributes
    // @description
    // Returns the maximum duration of oxygen the entity can have.
    // Works with offline players.
    // -->
    registerSpawnedOnlyTag(DurationTag.class, "max_oxygen", (attribute, object) -> {
        return new DurationTag((long) object.getLivingEntity().getMaximumAir());
    });
    // <--[tag]
    // @attribute <EntityTag.oxygen>
    // @returns DurationTag
    // @mechanism EntityTag.oxygen
    // @group attributes
    // @description
    // Returns the duration of oxygen the entity has left.
    // Works with offline players.
    // -->
    registerSpawnedOnlyTag(DurationTag.class, "oxygen", (attribute, object) -> {
        if (attribute.startsWith("max", 2)) {
            Deprecations.entityMaxOxygenTag.warn(attribute.context);
            attribute.fulfill(1);
            return new DurationTag((long) object.getLivingEntity().getMaximumAir());
        }
        return new DurationTag((long) object.getLivingEntity().getRemainingAir());
    });
    registerSpawnedOnlyTag(ElementTag.class, "remove_when_far", (attribute, object) -> {
        Deprecations.entityRemoveWhenFar.warn(attribute.context);
        return new ElementTag(object.getLivingEntity().getRemoveWhenFarAway());
    });
    // <--[tag]
    // @attribute <EntityTag.target>
    // @returns EntityTag
    // @group attributes
    // @description
    // Returns the target entity of the creature or shulker_bullet, if any.
    // This is the entity that a hostile mob is currently trying to attack.
    // -->
    registerSpawnedOnlyTag(EntityFormObject.class, "target", (attribute, object) -> {
        if (object.getBukkitEntity() instanceof Creature) {
            Entity target = ((Creature) object.getLivingEntity()).getTarget();
            if (target != null) {
                return new EntityTag(target).getDenizenObject();
            }
        } else if (object.getBukkitEntity() instanceof ShulkerBullet) {
            Entity target = ((ShulkerBullet) object.getLivingEntity()).getTarget();
            if (target != null) {
                return new EntityTag(target).getDenizenObject();
            }
        }
        return null;
    });
    // <--[tag]
    // @attribute <EntityTag.precise_target[(<range>)]>
    // @returns EntityTag
    // @description
    // Returns the entity this entity is looking at, using precise ray trace logic.
    // Optionally, specify a maximum range to find the entity from (defaults to 200).
    // -->
    registerSpawnedOnlyTag(EntityFormObject.class, "precise_target", (attribute, object) -> {
        int range = attribute.getIntParam();
        if (range < 1) {
            range = 200;
        }
        Predicate<Entity> requirement;
        // -->
        if (attribute.startsWith("type", 2) && attribute.hasContext(2)) {
            attribute.fulfill(1);
            String matcher = attribute.getParam();
            requirement = (e) -> !e.equals(object.getBukkitEntity()) && BukkitScriptEvent.tryEntity(new EntityTag(e), matcher);
        } else {
            requirement = (e) -> !e.equals(object.getBukkitEntity());
        }
        RayTraceResult result = object.getWorld().rayTrace(object.getEyeLocation(), object.getEyeLocation().getDirection(), range, FluidCollisionMode.NEVER, true, 0, requirement);
        if (result != null && result.getHitEntity() != null) {
            return new EntityTag(result.getHitEntity()).getDenizenObject();
        }
        return null;
    });
    // <--[tag]
    // @attribute <EntityTag.precise_target_position[(<range>)]>
    // @returns LocationTag
    // @description
    // Returns the location this entity is looking at, using precise ray trace (against entities) logic.
    // Optionally, specify a maximum range to find the target from (defaults to 200).
    // -->
    registerSpawnedOnlyTag(LocationTag.class, "precise_target_position", (attribute, object) -> {
        int range = attribute.getIntParam();
        if (range < 1) {
            range = 200;
        }
        Predicate<Entity> requirement;
        // -->
        if (attribute.startsWith("type", 2) && attribute.hasContext(2)) {
            attribute.fulfill(1);
            String matcher = attribute.getParam();
            requirement = (e) -> !e.equals(object.getBukkitEntity()) && BukkitScriptEvent.tryEntity(new EntityTag(e), matcher);
        } else {
            requirement = (e) -> !e.equals(object.getBukkitEntity());
        }
        RayTraceResult result = object.getWorld().rayTrace(object.getEyeLocation(), object.getEyeLocation().getDirection(), range, FluidCollisionMode.NEVER, true, 0, requirement);
        if (result != null) {
            return new LocationTag(object.getWorld(), result.getHitPosition());
        }
        return null;
    });
    // <--[tag]
    // @attribute <EntityTag.time_lived>
    // @returns DurationTag
    // @mechanism EntityTag.time_lived
    // @group attributes
    // @description
    // Returns how long the entity has lived.
    // -->
    registerSpawnedOnlyTag(DurationTag.class, "time_lived", (attribute, object) -> {
        return new DurationTag(object.getBukkitEntity().getTicksLived() / 20);
    });
    // <--[tag]
    // @attribute <EntityTag.pickup_delay>
    // @returns DurationTag
    // @mechanism EntityTag.pickup_delay
    // @group attributes
    // @description
    // Returns how long before the item-type entity can be picked up by a player.
    // -->
    registerSpawnedOnlyTag(DurationTag.class, "pickup_delay", (attribute, object) -> {
        if (!(object.getBukkitEntity() instanceof Item)) {
            return null;
        }
        return new DurationTag(((Item) object.getBukkitEntity()).getPickupDelay() * 20);
    }, "pickupdelay");
    // <--[tag]
    // @attribute <EntityTag.is_in_block>
    // @returns ElementTag(Boolean)
    // @group attributes
    // @description
    // Returns whether or not the arrow/trident entity is in a block.
    // -->
    registerSpawnedOnlyTag(ElementTag.class, "is_in_block", (attribute, object) -> {
        if (object.getBukkitEntity() instanceof Arrow) {
            return new ElementTag(((Arrow) object.getBukkitEntity()).isInBlock());
        }
        return null;
    });
    // <--[tag]
    // @attribute <EntityTag.attached_block>
    // @returns LocationTag
    // @group attributes
    // @description
    // Returns the location of the block that the arrow/trident or hanging entity is attached to.
    // -->
    registerSpawnedOnlyTag(LocationTag.class, "attached_block", (attribute, object) -> {
        if (object.getBukkitEntity() instanceof Arrow) {
            Block attachedBlock = ((Arrow) object.getBukkitEntity()).getAttachedBlock();
            if (attachedBlock != null) {
                return new LocationTag(attachedBlock.getLocation());
            }
        } else if (object.getBukkitEntity() instanceof Hanging) {
            Vector dir = ((Hanging) object.getBukkitEntity()).getAttachedFace().getDirection();
            return new LocationTag(object.getLocation().clone().add(dir.multiply(0.5))).getBlockLocation();
        }
        return null;
    });
    // <--[tag]
    // @attribute <EntityTag.gliding>
    // @returns ElementTag(Boolean)
    // @mechanism EntityTag.gliding
    // @group attributes
    // @description
    // Returns whether this entity is gliding.
    // -->
    registerSpawnedOnlyTag(ElementTag.class, "gliding", (attribute, object) -> {
        return new ElementTag(object.getLivingEntity().isGliding());
    });
    // <--[tag]
    // @attribute <EntityTag.swimming>
    // @returns ElementTag(Boolean)
    // @mechanism EntityTag.swimming
    // @group attributes
    // @description
    // Returns whether this entity is swimming.
    // -->
    registerSpawnedOnlyTag(ElementTag.class, "swimming", (attribute, object) -> {
        return new ElementTag(object.getLivingEntity().isSwimming());
    });
    // <--[tag]
    // @attribute <EntityTag.visual_pose>
    // @returns ElementTag
    // @group attributes
    // @description
    // Returns the name of the entity's current visual pose.
    // See <@link url https://hub.spigotmc.org/javadocs/bukkit/org/bukkit/entity/Pose.html>
    // -->
    registerSpawnedOnlyTag(ElementTag.class, "visual_pose", (attribute, object) -> {
        return new ElementTag(object.getBukkitEntity().getPose().name());
    });
    // <--[tag]
    // @attribute <EntityTag.glowing>
    // @returns ElementTag(Boolean)
    // @mechanism EntityTag.glowing
    // @group attributes
    // @description
    // Returns whether this entity is glowing.
    // -->
    registerSpawnedOnlyTag(ElementTag.class, "glowing", (attribute, object) -> {
        return new ElementTag(object.getBukkitEntity().isGlowing());
    });
    // ///////////////////
    // TYPE ATTRIBUTES
    // ///////////////
    // <--[tag]
    // @attribute <EntityTag.is_living>
    // @returns ElementTag(Boolean)
    // @group data
    // @description
    // Returns whether the entity type is a living-type entity (eg a cow or a player or anything else that lives, as specifically opposed to non-living entities like paintings, etc).
    // Not to be confused with the idea of being alive - see <@link tag EntityTag.is_spawned>.
    // -->
    tagProcessor.registerTag(ElementTag.class, "is_living", (attribute, object) -> {
        return new ElementTag(object.isLivingEntityType());
    });
    // <--[tag]
    // @attribute <EntityTag.is_monster>
    // @returns ElementTag(Boolean)
    // @group data
    // @description
    // Returns whether the entity type is a hostile monster.
    // -->
    tagProcessor.registerTag(ElementTag.class, "is_monster", (attribute, object) -> {
        return new ElementTag(object.isMonsterType());
    });
    // <--[tag]
    // @attribute <EntityTag.is_mob>
    // @returns ElementTag(Boolean)
    // @group data
    // @description
    // Returns whether the entity type is a mob (Not a player or NPC).
    // -->
    tagProcessor.registerTag(ElementTag.class, "is_mob", (attribute, object) -> {
        return new ElementTag(object.isMobType());
    });
    // <--[tag]
    // @attribute <EntityTag.is_npc>
    // @returns ElementTag(Boolean)
    // @group data
    // @description
    // Returns whether the entity is a Citizens NPC.
    // -->
    registerSpawnedOnlyTag(ElementTag.class, "is_npc", (attribute, object) -> {
        return new ElementTag(object.isCitizensNPC());
    });
    // <--[tag]
    // @attribute <EntityTag.is_player>
    // @returns ElementTag(Boolean)
    // @group data
    // @description
    // Returns whether the entity is a player.
    // Works with offline players.
    // -->
    tagProcessor.registerTag(ElementTag.class, "is_player", (attribute, object) -> {
        return new ElementTag(object.isPlayer());
    });
    // <--[tag]
    // @attribute <EntityTag.is_projectile>
    // @returns ElementTag(Boolean)
    // @group data
    // @description
    // Returns whether the entity type is a projectile.
    // -->
    tagProcessor.registerTag(ElementTag.class, "is_projectile", (attribute, object) -> {
        if (object.getBukkitEntity() == null && object.entity_type != null) {
            return new ElementTag(Projectile.class.isAssignableFrom(object.entity_type.getBukkitEntityType().getEntityClass()));
        }
        return new ElementTag(object.isProjectile());
    });
    // ///////////////////
    // PROPERTY ATTRIBUTES
    // ///////////////
    // <--[tag]
    // @attribute <EntityTag.tameable>
    // @returns ElementTag(Boolean)
    // @group properties
    // @description
    // Returns whether the entity is tameable.
    // If this returns true, it will enable access to:
    // <@link mechanism EntityTag.tame>, <@link mechanism EntityTag.owner>,
    // <@link tag EntityTag.is_tamed>, and <@link tag EntityTag.owner>
    // -->
    registerSpawnedOnlyTag(ElementTag.class, "tameable", (attribute, object) -> {
        return new ElementTag(EntityTame.describes(object));
    }, "is_tameable");
    // <--[tag]
    // @attribute <EntityTag.ageable>
    // @returns ElementTag(Boolean)
    // @group properties
    // @description
    // Returns whether the entity is ageable.
    // If this returns true, it will enable access to:
    // <@link mechanism EntityTag.age>, <@link mechanism EntityTag.age_lock>,
    // <@link tag EntityTag.is_baby>, <@link tag EntityTag.age>,
    // and <@link tag EntityTag.is_age_locked>
    // -->
    registerSpawnedOnlyTag(ElementTag.class, "ageable", (attribute, object) -> {
        return new ElementTag(EntityAge.describes(object));
    }, "is_ageable");
    // <--[tag]
    // @attribute <EntityTag.colorable>
    // @returns ElementTag(Boolean)
    // @group properties
    // @description
    // Returns whether the entity can be colored.
    // If this returns true, it will enable access to:
    // <@link mechanism EntityTag.color> and <@link tag EntityTag.color>
    // -->
    registerSpawnedOnlyTag(ElementTag.class, "colorable", (attribute, object) -> {
        return new ElementTag(EntityColor.describes(object));
    }, "is_colorable");
    // <--[tag]
    // @attribute <EntityTag.experience>
    // @returns ElementTag(Number)
    // @mechanism EntityTag.experience
    // @group properties
    // @description
    // Returns the experience value of this experience orb entity.
    // -->
    registerSpawnedOnlyTag(ElementTag.class, "experience", (attribute, object) -> {
        if (!(object.getBukkitEntity() instanceof ExperienceOrb)) {
            return null;
        }
        return new ElementTag(((ExperienceOrb) object.getBukkitEntity()).getExperience());
    });
    // <--[tag]
    // @attribute <EntityTag.fuse_ticks>
    // @returns ElementTag(Number)
    // @mechanism EntityTag.fuse_ticks
    // @group properties
    // @description
    // Returns the number of ticks until the explosion of the primed TNT.
    // -->
    registerSpawnedOnlyTag(ElementTag.class, "fuse_ticks", (attribute, object) -> {
        if (!(object.getBukkitEntity() instanceof TNTPrimed)) {
            return null;
        }
        return new ElementTag(((TNTPrimed) object.getBukkitEntity()).getFuseTicks());
    });
    // <--[tag]
    // @attribute <EntityTag.dragon_phase>
    // @returns ElementTag
    // @mechanism EntityTag.dragon_phase
    // @group properties
    // @description
    // Returns the phase an EnderDragon is currently in.
    // Valid phases: <@link url https://hub.spigotmc.org/javadocs/spigot/org/bukkit/entity/EnderDragon.Phase.html>
    // -->
    registerSpawnedOnlyTag(ElementTag.class, "dragon_phase", (attribute, object) -> {
        if (!(object.getBukkitEntity() instanceof EnderDragon)) {
            return null;
        }
        return new ElementTag(((EnderDragon) object.getLivingEntity()).getPhase().name());
    });
    // <--[tag]
    // @attribute <EntityTag.weapon_damage[(<entity>)]>
    // @returns ElementTag(Number)
    // @group properties
    // @description
    // Returns the amount of damage the entity will do based on its held item.
    // Optionally, specify a target entity to test how much damage will be done to that specific target
    // (modified based on enchantments and that entity's armor/status/etc).
    // Note that the result will not always be completely exact, as it doesn't take into account some specific factors
    // (eg sweeping vs single-hit, etc).
    // -->
    registerSpawnedOnlyTag(ElementTag.class, "weapon_damage", (attribute, object) -> {
        Entity target = null;
        if (attribute.hasParam()) {
            target = attribute.paramAsType(EntityTag.class).getBukkitEntity();
        }
        return new ElementTag(NMSHandler.getEntityHelper().getDamageTo(object.getLivingEntity(), target));
    });
    // <--[tag]
    // @attribute <EntityTag.skin_layers>
    // @returns ListTag
    // @mechanism EntityTag.skin_layers
    // @description
    // Returns the skin layers currently visible on a player-type entity.
    // Output is a list of values from the set of:
    // CAPE, HAT, JACKET, LEFT_PANTS, LEFT_SLEEVE, RIGHT_PANTS, or RIGHT_SLEEVE.
    // -->
    registerSpawnedOnlyTag(ListTag.class, "skin_layers", (attribute, object) -> {
        byte flags = NMSHandler.getPlayerHelper().getSkinLayers((Player) object.getBukkitEntity());
        ListTag result = new ListTag();
        for (PlayerHelper.SkinLayer layer : PlayerHelper.SkinLayer.values()) {
            if ((flags & layer.flag) != 0) {
                result.add(layer.name());
            }
        }
        return result;
    });
    // <--[tag]
    // @attribute <EntityTag.is_disguised[(<player>)]>
    // @returns ElementTag(Boolean)
    // @group properties
    // @description
    // Returns whether the entity is currently disguised, either globally (if no context input given), or to the specified player.
    // Relates to <@link command disguise>.
    // -->
    registerSpawnedOnlyTag(ElementTag.class, "is_disguised", (attribute, object) -> {
        HashMap<UUID, DisguiseCommand.TrackedDisguise> map = DisguiseCommand.disguises.get(object.getUUID());
        if (map == null) {
            return new ElementTag(false);
        }
        if (attribute.hasParam()) {
            PlayerTag player = attribute.paramAsType(PlayerTag.class);
            if (player == null) {
                attribute.echoError("Invalid player for is_disguised tag.");
                return null;
            }
            return new ElementTag(map.containsKey(player.getUUID()) || map.containsKey(null));
        } else {
            return new ElementTag(map.containsKey(null));
        }
    });
    // <--[tag]
    // @attribute <EntityTag.disguised_type[(<player>)]>
    // @returns EntityTag
    // @group properties
    // @description
    // Returns the entity type the entity is disguised as, either globally (if no context input given), or to the specified player.
    // Relates to <@link command disguise>.
    // -->
    registerSpawnedOnlyTag(EntityTag.class, "disguised_type", (attribute, object) -> {
        HashMap<UUID, DisguiseCommand.TrackedDisguise> map = DisguiseCommand.disguises.get(object.getUUID());
        if (map == null) {
            return null;
        }
        DisguiseCommand.TrackedDisguise disguise;
        if (attribute.hasParam()) {
            PlayerTag player = attribute.paramAsType(PlayerTag.class);
            if (player == null) {
                attribute.echoError("Invalid player for is_disguised tag.");
                return null;
            }
            disguise = map.get(player.getUUID());
            if (disguise == null) {
                disguise = map.get(null);
            }
        } else {
            disguise = map.get(null);
        }
        if (disguise == null) {
            return null;
        }
        return disguise.as.duplicate();
    });
    // <--[tag]
    // @attribute <EntityTag.disguise_to_others[(<player>)]>
    // @returns EntityTag
    // @group properties
    // @description
    // Returns the fake entity used to disguise the entity in other's views, either globally (if no context input given), or to the specified player.
    // Relates to <@link command disguise>.
    // -->
    registerSpawnedOnlyTag(EntityTag.class, "disguise_to_others", (attribute, object) -> {
        HashMap<UUID, DisguiseCommand.TrackedDisguise> map = DisguiseCommand.disguises.get(object.getUUID());
        if (map == null) {
            return null;
        }
        DisguiseCommand.TrackedDisguise disguise;
        if (attribute.hasParam()) {
            PlayerTag player = attribute.paramAsType(PlayerTag.class);
            if (player == null) {
                attribute.echoError("Invalid player for is_disguised tag.");
                return null;
            }
            disguise = map.get(player.getUUID());
            if (disguise == null) {
                disguise = map.get(null);
            }
        } else {
            disguise = map.get(null);
        }
        if (disguise == null) {
            return null;
        }
        if (disguise.toOthers == null) {
            return null;
        }
        return disguise.toOthers.entity;
    });
    // <--[tag]
    // @attribute <EntityTag.describe>
    // @returns EntityTag
    // @group properties
    // @description
    // Returns the entity's full description, including all properties.
    // -->
    tagProcessor.registerTag(EntityTag.class, "describe", (attribute, object) -> {
        return object.describe(attribute.context);
    });
    // <--[tag]
    // @attribute <EntityTag.advanced_matches[<matcher>]>
    // @returns ElementTag(Boolean)
    // @group element checking
    // @description
    // Returns whether the entity matches some matcher text, using the system behind <@link language Advanced Script Event Matching>.
    // -->
    tagProcessor.registerTag(ElementTag.class, "advanced_matches", (attribute, object) -> {
        if (!attribute.hasParam()) {
            return null;
        }
        return new ElementTag(BukkitScriptEvent.tryEntity(object, attribute.getParam()));
    });
    // <--[tag]
    // @attribute <EntityTag.has_equipped[<item-matcher>]>
    // @returns ElementTag(Boolean)
    // @group element checking
    // @description
    // Returns whether the entity has any armor equipment item that matches the given item matcher, using the system behind <@link language Advanced Script Event Matching>.
    // For example, has_equipped[diamond_*] will return true if the entity is wearing at least one piece of diamond armor.
    // -->
    registerSpawnedOnlyTag(ElementTag.class, "has_equipped", (attribute, object) -> {
        if (!attribute.hasParam()) {
            return null;
        }
        if (!object.isLivingEntity()) {
            return null;
        }
        String matcher = attribute.getParam();
        for (ItemStack item : object.getLivingEntity().getEquipment().getArmorContents()) {
            if (BukkitScriptEvent.tryItem(new ItemTag(item), matcher)) {
                return new ElementTag(true);
            }
        }
        return new ElementTag(false);
    });
    // <--[tag]
    // @attribute <EntityTag.loot_table_id>
    // @returns ElementTag
    // @description
    // Returns an element indicating the minecraft key for the loot-table for the entity (if any).
    // -->
    registerSpawnedOnlyTag(ElementTag.class, "loot_table_id", (attribute, object) -> {
        if (object.getBukkitEntity() instanceof Lootable) {
            LootTable table = ((Lootable) object.getBukkitEntity()).getLootTable();
            if (table != null) {
                return new ElementTag(table.getKey().toString());
            }
        }
        return null;
    });
    // <--[tag]
    // @attribute <EntityTag.fish_hook_state>
    // @returns ElementTag
    // @description
    // Returns the current state of the fish hook, as any of: UNHOOKED, HOOKED_ENTITY, BOBBING (unhooked means the fishing hook is in the air or on ground).
    // -->
    registerSpawnedOnlyTag(ElementTag.class, "fish_hook_state", (attribute, object) -> {
        if (!(object.getBukkitEntity() instanceof FishHook)) {
            attribute.echoError("EntityTag.fish_hook_state is only valid for fish hooks.");
            return null;
        }
        return new ElementTag(((FishHook) object.getBukkitEntity()).getState().name());
    });
    // <--[tag]
    // @attribute <EntityTag.fish_hook_lure_time>
    // @returns DurationTag
    // @mechanism EntityTag.fish_hook_lure_time
    // @description
    // Returns the remaining time before this fish hook will lure a fish.
    // -->
    registerSpawnedOnlyTag(DurationTag.class, "fish_hook_lure_time", (attribute, object) -> {
        if (!(object.getBukkitEntity() instanceof FishHook)) {
            attribute.echoError("EntityTag.fish_hook_lure_time is only valid for fish hooks.");
            return null;
        }
        return new DurationTag((long) NMSHandler.getFishingHelper().getLureTime((FishHook) object.getBukkitEntity()));
    });
    // <--[tag]
    // @attribute <EntityTag.fish_hook_min_lure_time>
    // @returns DurationTag
    // @mechanism EntityTag.fish_hook_min_lure_time
    // @description
    // Returns the minimum possible time before this fish hook can lure a fish.
    // -->
    registerSpawnedOnlyTag(DurationTag.class, "fish_hook_min_lure_time", (attribute, object) -> {
        if (!(object.getBukkitEntity() instanceof FishHook)) {
            attribute.echoError("EntityTag.fish_hook_min_lure_time is only valid for fish hooks.");
            return null;
        }
        return new DurationTag((long) ((FishHook) object.getBukkitEntity()).getMinWaitTime());
    });
    // <--[tag]
    // @attribute <EntityTag.fish_hook_max_lure_time>
    // @returns DurationTag
    // @mechanism EntityTag.fish_hook_max_lure_time
    // @description
    // Returns the maximum possible time before this fish hook will lure a fish.
    // -->
    registerSpawnedOnlyTag(DurationTag.class, "fish_hook_max_lure_time", (attribute, object) -> {
        if (!(object.getBukkitEntity() instanceof FishHook)) {
            attribute.echoError("EntityTag.fish_hook_max_lure_time is only valid for fish hooks.");
            return null;
        }
        return new DurationTag((long) ((FishHook) object.getBukkitEntity()).getMaxWaitTime());
    });
    // <--[tag]
    // @attribute <EntityTag.fish_hook_hooked_entity>
    // @returns EntityTag
    // @mechanism EntityTag.fish_hook_hooked_entity
    // @description
    // Returns the entity this fish hook is attached to.
    // -->
    registerSpawnedOnlyTag(EntityTag.class, "fish_hook_hooked_entity", (attribute, object) -> {
        if (!(object.getBukkitEntity() instanceof FishHook)) {
            attribute.echoError("EntityTag.fish_hook_hooked_entity is only valid for fish hooks.");
            return null;
        }
        Entity entity = ((FishHook) object.getBukkitEntity()).getHookedEntity();
        return entity != null ? new EntityTag(entity) : null;
    });
    // <--[tag]
    // @attribute <EntityTag.fish_hook_apply_lure>
    // @returns ElementTag(Boolean)
    // @mechanism EntityTag.fish_hook_apply_lure
    // @description
    // Returns whether this fish hook should respect the lure enchantment.
    // Every level of lure enchantment reduces lure time by 5 seconds.
    // -->
    registerSpawnedOnlyTag(ElementTag.class, "fish_hook_apply_lure", (attribute, object) -> {
        if (!(object.getBukkitEntity() instanceof FishHook)) {
            attribute.echoError("EntityTag.fish_hook_apply_lure is only valid for fish hooks.");
            return null;
        }
        return new ElementTag(((FishHook) object.getBukkitEntity()).getApplyLure());
    });
    // <--[tag]
    // @attribute <EntityTag.fish_hook_in_open_water>
    // @returns ElementTag(Boolean)
    // @description
    // Returns whether this fish hook is in open water. Fish hooks in open water can catch treasure.
    // See <@link url https://minecraft.fandom.com/wiki/Fishing> for more info.
    // -->
    registerSpawnedOnlyTag(ElementTag.class, "fish_hook_in_open_water", (attribute, object) -> {
        if (!(object.getBukkitEntity() instanceof FishHook)) {
            attribute.echoError("EntityTag.fish_hook_in_open_water is only valid for fish hooks.");
            return null;
        }
        return new ElementTag(((FishHook) object.getBukkitEntity()).isInOpenWater());
    });
    // <--[tag]
    // @attribute <EntityTag.attached_entities[(<player>)]>
    // @returns ListTag(EntityTag)
    // @description
    // Returns the entities attached to this entity by <@link command attach>.
    // Optionally, specify a player. If specified, will return entities attached visible to that player. If not specified, returns entities globally attached.
    // -->
    registerSpawnedOnlyTag(ListTag.class, "attached_entities", (attribute, object) -> {
        PlayerTag player = attribute.hasParam() ? attribute.paramAsType(PlayerTag.class) : null;
        EntityAttachmentHelper.EntityAttachedToMap data = EntityAttachmentHelper.toEntityToData.get(object.getUUID());
        ListTag result = new ListTag();
        if (data == null) {
            return result;
        }
        for (EntityAttachmentHelper.PlayerAttachMap map : data.attachedToMap.values()) {
            if (player == null || map.getAttachment(player.getUUID()) != null) {
                result.addObject(map.attached);
            }
        }
        return result;
    });
    // <--[tag]
    // @attribute <EntityTag.attached_to[(<player>)]>
    // @returns EntityTag
    // @description
    // Returns the entity that this entity was attached to by <@link command attach>.
    // Optionally, specify a player. If specified, will return entity attachment visible to that player. If not specified, returns any entity global attachment.
    // -->
    registerSpawnedOnlyTag(EntityTag.class, "attached_to", (attribute, object) -> {
        PlayerTag player = attribute.hasParam() ? attribute.paramAsType(PlayerTag.class) : null;
        EntityAttachmentHelper.PlayerAttachMap data = EntityAttachmentHelper.attachedEntityToData.get(object.getUUID());
        if (data == null) {
            return null;
        }
        EntityAttachmentHelper.AttachmentData attached = data.getAttachment(player == null ? null : player.getUUID());
        if (attached == null) {
            return null;
        }
        return attached.to;
    });
    // <--[tag]
    // @attribute <EntityTag.attached_offset[(<player>)]>
    // @returns LocationTag
    // @description
    // Returns the offset of an attachment for this entity to another that was attached by <@link command attach>.
    // Optionally, specify a player. If specified, will return entity attachment visible to that player. If not specified, returns any entity global attachment.
    // -->
    registerSpawnedOnlyTag(LocationTag.class, "attached_offset", (attribute, object) -> {
        PlayerTag player = attribute.hasParam() ? attribute.paramAsType(PlayerTag.class) : null;
        EntityAttachmentHelper.PlayerAttachMap data = EntityAttachmentHelper.attachedEntityToData.get(object.getUUID());
        if (data == null) {
            return null;
        }
        EntityAttachmentHelper.AttachmentData attached = data.getAttachment(player == null ? null : player.getUUID());
        if (attached == null) {
            return null;
        }
        return attached.positionalOffset == null ? null : new LocationTag(attached.positionalOffset);
    });
    // <--[tag]
    // @attribute <EntityTag.attack_cooldown_duration>
    // @returns DurationTag
    // @mechanism EntityTag.attack_cooldown
    // @description
    // Returns the amount of time that passed since the start of the attack cooldown.
    // -->
    registerSpawnedOnlyTag(DurationTag.class, "attack_cooldown_duration", (attribute, object) -> {
        if (!(object.getBukkitEntity() instanceof Player)) {
            attribute.echoError("Only player-type entities can have attack_cooldowns!");
            return null;
        }
        return new DurationTag((long) NMSHandler.getPlayerHelper().ticksPassedDuringCooldown((Player) object.getLivingEntity()));
    });
    // <--[tag]
    // @attribute <EntityTag.attack_cooldown_max_duration>
    // @returns DurationTag
    // @mechanism EntityTag.attack_cooldown
    // @description
    // Returns the maximum amount of time that can pass before the player's main hand has returned
    // to its original place after the cooldown has ended.
    // NOTE: This is slightly inaccurate and may not necessarily match with the actual attack
    // cooldown progress.
    // -->
    registerSpawnedOnlyTag(DurationTag.class, "attack_cooldown_max_duration", (attribute, object) -> {
        if (!(object.getBukkitEntity() instanceof Player)) {
            attribute.echoError("Only player-type entities can have attack_cooldowns!");
            return null;
        }
        return new DurationTag((long) NMSHandler.getPlayerHelper().getMaxAttackCooldownTicks((Player) object.getLivingEntity()));
    });
    // <--[tag]
    // @attribute <EntityTag.attack_cooldown_percent>
    // @returns ElementTag(Decimal)
    // @mechanism EntityTag.attack_cooldown_percent
    // @description
    // Returns the progress of the attack cooldown. 0 means that the attack cooldown has just
    // started, while 100 means that the attack cooldown has finished.
    // NOTE: This may not match exactly with the clientside attack cooldown indicator.
    // -->
    registerSpawnedOnlyTag(ElementTag.class, "attack_cooldown_percent", (attribute, object) -> {
        if (!(object.getBukkitEntity() instanceof Player)) {
            attribute.echoError("Only player-type entities can have attack_cooldowns!");
            return null;
        }
        return new ElementTag(NMSHandler.getPlayerHelper().getAttackCooldownPercent((Player) object.getLivingEntity()) * 100);
    });
    // <--[tag]
    // @attribute <EntityTag.is_hand_raised>
    // @returns ElementTag(Boolean)
    // @mechanism EntityTag.attack_cooldown_percent
    // @description
    // Returns whether the player's hand is currently raised. Valid for players and player-type NPCs.
    // A player's hand is raised when they are blocking with a shield, aiming a crossbow, looking through a spyglass, etc.
    // -->
    registerSpawnedOnlyTag(ElementTag.class, "is_hand_raised", (attribute, object) -> {
        if (!(object.getBukkitEntity() instanceof HumanEntity)) {
            attribute.echoError("Only player-type entities can have is_hand_raised!");
            return null;
        }
        return new ElementTag(((HumanEntity) object.getLivingEntity()).isHandRaised());
    });
}
Also used : DisguiseCommand(com.denizenscript.denizen.scripts.commands.player.DisguiseCommand) Lootable(org.bukkit.loot.Lootable) Vector(org.bukkit.util.Vector) FakePlayer(com.denizenscript.denizen.nms.interfaces.FakePlayer) RayTraceResult(org.bukkit.util.RayTraceResult) PlayerHelper(com.denizenscript.denizen.nms.interfaces.PlayerHelper) Block(org.bukkit.block.Block) LootTable(org.bukkit.loot.LootTable) EntityHelper(com.denizenscript.denizen.nms.interfaces.EntityHelper)

Aggregations

BukkitScriptEvent (com.denizenscript.denizen.events.BukkitScriptEvent)2 NMSHandler (com.denizenscript.denizen.nms.NMSHandler)2 EntityHelper (com.denizenscript.denizen.nms.interfaces.EntityHelper)2 BukkitTagContext (com.denizenscript.denizen.tags.BukkitTagContext)2 Settings (com.denizenscript.denizen.utilities.Settings)2 Utilities (com.denizenscript.denizen.utilities.Utilities)2 com.denizenscript.denizencore.objects (com.denizenscript.denizencore.objects)2 Notable (com.denizenscript.denizencore.objects.notable.Notable)2 NoteManager (com.denizenscript.denizencore.objects.notable.NoteManager)2 Attribute (com.denizenscript.denizencore.tags.Attribute)2 CoreUtilities (com.denizenscript.denizencore.utilities.CoreUtilities)2 LootTable (org.bukkit.loot.LootTable)2 Lootable (org.bukkit.loot.Lootable)2 Vector (org.bukkit.util.Vector)2 Denizen (com.denizenscript.denizen.Denizen)1 BiomeNMS (com.denizenscript.denizen.nms.abstracts.BiomeNMS)1 FakePlayer (com.denizenscript.denizen.nms.interfaces.FakePlayer)1 PlayerHelper (com.denizenscript.denizen.nms.interfaces.PlayerHelper)1 PlayerProfile (com.denizenscript.denizen.nms.util.PlayerProfile)1 AssignmentTrait (com.denizenscript.denizen.npc.traits.AssignmentTrait)1