diff --git a/build.gradle.kts b/build.gradle.kts index 00b7702e..8c68b3ad 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -58,6 +58,13 @@ repositories { includeGroup("de.themoep") } } + + maven { + url = uri("https://repo.glaremasters.me/repository/towny") + content { + includeGroup("com.palmergames.bukkit.towny") + } + } } paperweight.reobfArtifactConfiguration = ReobfArtifactConfiguration.MOJANG_PRODUCTION @@ -71,6 +78,8 @@ dependencies { compileOnly(files("libs/Residence5.1.4.0.jar")) compileOnly(files("libs/TAB v4.1.2.jar")) + compileOnly("com.palmergames.bukkit.towny:towny:${project.property("towny_version")}") + compileOnly("com.ticxo.modelengine:ModelEngine:${project.property("me_version")}") //compileOnly("com.github.Gecolay:GSit:${project.property("gsit_version")}") @@ -127,6 +136,8 @@ bukkit { register("play-action") + register("toggle-town-morph-flight") + val featherMorphCommand = register("feathermorph").get() featherMorphCommand.aliases = listOf("fm"); } diff --git a/gradle.properties b/gradle.properties index dee20956..53987aea 100644 --- a/gradle.properties +++ b/gradle.properties @@ -16,3 +16,4 @@ gsit_version=1.6.0 papi_version=2.11.5 bstats_version=3.0.2 me_version = R4.0.4 +towny_version = 0.100.4.0 \ No newline at end of file diff --git a/src/main/java/xyz/nifeather/morph/MorphManager.java b/src/main/java/xyz/nifeather/morph/MorphManager.java index d5f6a863..0ff7bfac 100644 --- a/src/main/java/xyz/nifeather/morph/MorphManager.java +++ b/src/main/java/xyz/nifeather/morph/MorphManager.java @@ -946,9 +946,6 @@ private void postBuildDisguise(DisguiseBuildResult result, // 切换CD skillHandler.switchCooldown(player.getUniqueId(), cdInfo); - - // 调用事件 - new PlayerMorphEvent(player, state).callEvent(); } private boolean applyDisguise(MorphParameters parameters, @@ -1032,6 +1029,9 @@ private boolean applyDisguise(MorphParameters parameters, clientHandler.sendCommand(player, new S2CSetAvailableAnimationsCommand(availableAnimations)); + // 调用事件 + new PlayerMorphEvent(player, state).callEvent(); + return true; } diff --git a/src/main/java/xyz/nifeather/morph/MorphPlugin.java b/src/main/java/xyz/nifeather/morph/MorphPlugin.java index 4c294348..53739d6e 100644 --- a/src/main/java/xyz/nifeather/morph/MorphPlugin.java +++ b/src/main/java/xyz/nifeather/morph/MorphPlugin.java @@ -19,6 +19,7 @@ import xyz.nifeather.morph.messages.vanilla.VanillaMessageStore; import xyz.nifeather.morph.misc.NetworkingHelper; import xyz.nifeather.morph.misc.PlayerOperationSimulator; +import xyz.nifeather.morph.misc.integrations.towny.TownyAdapter; import xyz.nifeather.morph.misc.recipe.RecipeManager; import xyz.nifeather.morph.misc.disguiseProperty.DisguiseProperties; import xyz.nifeather.morph.misc.gui.IconLookup; @@ -174,6 +175,12 @@ public void onEnable() this.registerListener(new ResidenceEventProcessor()); }, true); + softDeps.setHandle("Towny", plugin -> + { + logger.info("Towny detected, applying integrations..."); + this.registerListener(new TownyAdapter()); + }, true); + softDeps.setHandle("TAB", r -> { logger.info("Applying TAB integrations..."); diff --git a/src/main/java/xyz/nifeather/morph/abilities/impl/FlyAbility.java b/src/main/java/xyz/nifeather/morph/abilities/impl/FlyAbility.java index 178ed73e..505bab2a 100644 --- a/src/main/java/xyz/nifeather/morph/abilities/impl/FlyAbility.java +++ b/src/main/java/xyz/nifeather/morph/abilities/impl/FlyAbility.java @@ -28,6 +28,7 @@ import java.util.Map; import java.util.Stack; +import java.util.concurrent.ConcurrentHashMap; public class FlyAbility extends MorphAbility { @@ -248,7 +249,7 @@ public void onGameModeChange(PlayerGameModeChangeEvent e) } } - private static final Map> blockedPlayersMap = new Object2ObjectOpenHashMap<>(); + private static final Map> blockedPlayersMap = new ConcurrentHashMap<>(); public static boolean playerBlocked(Player player) { diff --git a/src/main/java/xyz/nifeather/morph/commands/subcommands/plugin/OptionSubCommand.java b/src/main/java/xyz/nifeather/morph/commands/subcommands/plugin/OptionSubCommand.java index 2575b28e..29021ed0 100644 --- a/src/main/java/xyz/nifeather/morph/commands/subcommands/plugin/OptionSubCommand.java +++ b/src/main/java/xyz/nifeather/morph/commands/subcommands/plugin/OptionSubCommand.java @@ -83,6 +83,8 @@ public OptionSubCommand() subCommands.add(getList("blacklist_nbt_pattern", ConfigOption.BLACKLIST_PATTERNS, null)); subCommands.add(getToggle("ability_check_permissions", ConfigOption.DO_CHECK_ABILITY_PERMISSIONS, null)); + + subCommands.add(getToggle("towny_allow_flight_in_wilderness", ConfigOption.TOWNY_ALLOW_FLY_IN_WILDERNESS)); } private ISubCommand getList(String optionName, ConfigOption option, diff --git a/src/main/java/xyz/nifeather/morph/config/ConfigOption.java b/src/main/java/xyz/nifeather/morph/config/ConfigOption.java index 8c486fa1..aca9acb4 100644 --- a/src/main/java/xyz/nifeather/morph/config/ConfigOption.java +++ b/src/main/java/xyz/nifeather/morph/config/ConfigOption.java @@ -124,6 +124,8 @@ public enum ConfigOption // SRR -> ServerRenderer SR_SHOW_PLAYER_DISGUISES_IN_TAB(serverRendererNode().append("show_player_disguises_in_tab"), false), + TOWNY_ALLOW_FLY_IN_WILDERNESS(townyNode().append("allow_fly_in_wilderness"), false), + VERSION(ConfigNode.create().append("version"), 0); @@ -202,4 +204,12 @@ private static ConfigNode serverRendererNode() { return ConfigNode.create().append("server_renderer"); } + private static ConfigNode integrationNode() + { + return ConfigNode.create().append("integrations"); + } + public static ConfigNode townyNode() + { + return integrationNode().append("towny"); + } } diff --git a/src/main/java/xyz/nifeather/morph/events/CommonEventProcessor.java b/src/main/java/xyz/nifeather/morph/events/CommonEventProcessor.java index 397d0a6e..88d3243c 100644 --- a/src/main/java/xyz/nifeather/morph/events/CommonEventProcessor.java +++ b/src/main/java/xyz/nifeather/morph/events/CommonEventProcessor.java @@ -1,5 +1,6 @@ package xyz.nifeather.morph.events; +import com.destroystokyo.paper.event.entity.EntityAddToWorldEvent; import com.destroystokyo.paper.event.player.PlayerClientOptionsChangeEvent; import com.destroystokyo.paper.event.player.PlayerPostRespawnEvent; import de.themoep.inventorygui.InventoryGui; diff --git a/src/main/java/xyz/nifeather/morph/messages/CommandNameStrings.java b/src/main/java/xyz/nifeather/morph/messages/CommandNameStrings.java index 50f06bc1..f3002403 100644 --- a/src/main/java/xyz/nifeather/morph/messages/CommandNameStrings.java +++ b/src/main/java/xyz/nifeather/morph/messages/CommandNameStrings.java @@ -54,6 +54,11 @@ public static FormattableMessage mirrorIgnoreDisguised() return getFormattable(getKey("mirror_ignore_disguised"), "使反向控制忽略已伪装的目标"); } + public static FormattableMessage morphFlightForTownX() + { + return getFormattable(getKey("morph_flight_for_town_x"), "[Fallback] 的伪装飞行"); + } + private static String getKey(String key) { return "commands.option.name." + key; diff --git a/src/main/java/xyz/nifeather/morph/messages/CommandStrings.java b/src/main/java/xyz/nifeather/morph/messages/CommandStrings.java index 21053be0..c5c7105e 100644 --- a/src/main/java/xyz/nifeather/morph/messages/CommandStrings.java +++ b/src/main/java/xyz/nifeather/morph/messages/CommandStrings.java @@ -241,6 +241,24 @@ public static FormattableMessage grantItemSuccess() return getFormattable(getKey("grant_item_success"), "[Fallback] 成功给与物品。如果没有请检查是否背包已满"); } + public static FormattableMessage unknownError() + { + return getFormattable(getKey("unknown_error"), "[Fallback] 执行时发生了意外事故"); + } + + // towny + + public static FormattableMessage townyDoesntHaveTown() + { + return getFormattable(getKey("towny_dont_belong_to_any_town"), "[Fallback] 你不属于任何城镇!"); + } + + public static FormattableMessage townyPlayerNotMayor() + { + return getFormattable(getKey("towny_not_mayor"), "[Fallback] 此操作只适用于镇长"); + } + + private static String getKey(String key) { return "commands." + key; diff --git a/src/main/java/xyz/nifeather/morph/messages/CommonStrings.java b/src/main/java/xyz/nifeather/morph/messages/CommonStrings.java index e27d7fb4..4bb30313 100644 --- a/src/main/java/xyz/nifeather/morph/messages/CommonStrings.java +++ b/src/main/java/xyz/nifeather/morph/messages/CommonStrings.java @@ -36,6 +36,16 @@ public static FormattableMessage commandNotFoundString() "未找到此指令"); } + public static FormattableMessage on() + { + return getFormattable(getKey("on"), "[Fallback] ON"); + } + + public static FormattableMessage off() + { + return getFormattable(getKey("off"), "[Fallback] OFF"); + } + private static String getKey(String key) { return "common." + key; diff --git a/src/main/java/xyz/nifeather/morph/misc/integrations/towny/TownyAdapter.java b/src/main/java/xyz/nifeather/morph/misc/integrations/towny/TownyAdapter.java new file mode 100644 index 00000000..75d3f984 --- /dev/null +++ b/src/main/java/xyz/nifeather/morph/misc/integrations/towny/TownyAdapter.java @@ -0,0 +1,222 @@ +package xyz.nifeather.morph.misc.integrations.towny; + +import com.destroystokyo.paper.event.entity.EntityAddToWorldEvent; +import com.palmergames.bukkit.towny.TownyAPI; +import com.palmergames.bukkit.towny.event.player.PlayerEntersIntoTownBorderEvent; +import com.palmergames.bukkit.towny.event.player.PlayerExitsFromTownBorderEvent; +import com.palmergames.bukkit.towny.object.Town; +import com.palmergames.bukkit.towny.object.metadata.BooleanDataField; +import com.palmergames.bukkit.towny.utils.CombatUtil; +import com.palmergames.bukkit.towny.utils.MetaDataUtil; +import it.unimi.dsi.fastutil.objects.ObjectArrayList; +import it.unimi.dsi.fastutil.objects.ObjectLists; +import org.bukkit.Bukkit; +import org.bukkit.World; +import org.bukkit.command.PluginCommand; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.player.*; +import org.jetbrains.annotations.Nullable; +import xiamomc.pluginbase.Annotations.Initializer; +import xiamomc.pluginbase.Bindables.Bindable; +import xiamomc.pluginbase.Command.IPluginCommand; +import xyz.nifeather.morph.MorphPluginObject; +import xyz.nifeather.morph.abilities.AbilityType; +import xyz.nifeather.morph.abilities.impl.FlyAbility; +import xyz.nifeather.morph.config.ConfigOption; +import xyz.nifeather.morph.config.MorphConfigManager; +import xyz.nifeather.morph.events.api.gameplay.PlayerMorphEvent; +import xyz.nifeather.morph.events.api.gameplay.PlayerUnMorphEvent; + +import java.util.List; +import java.util.Objects; + +public class TownyAdapter extends MorphPluginObject implements Listener +{ + private final TownyAPI townyAPI = TownyAPI.getInstance(); + + private final Bindable allowFlyInWilderness = new Bindable<>(false); + + private final List blockedPlayers = ObjectLists.synchronize(new ObjectArrayList<>()); + + public static final BooleanDataField allowMorphFlight = new BooleanDataField("allow_morph_flight"); + + public boolean registerCommand(IPluginCommand command) + { + if (Objects.equals(command.getCommandName(), "")) + { + return false; + } + else + { + PluginCommand cmd = Bukkit.getPluginCommand(command.getCommandName()); + + if (cmd != null && cmd.getExecutor().equals(plugin)) + { + cmd.setExecutor(command); + cmd.setTabCompleter(command); + return true; + } + else + { + return false; + } + } + } + + @Initializer + private void load(MorphConfigManager configManager) + { + configManager.bind(allowFlyInWilderness, ConfigOption.TOWNY_ALLOW_FLY_IN_WILDERNESS); + + allowFlyInWilderness.onValueChanged((o, n) -> + { + Bukkit.getOnlinePlayers().forEach(p -> + this.scheduleOn(p, () -> updatePlayer(p, null))); + }); + + if (!registerCommand(new TownyToggleFlightCommand(this))) + logger.warn("Can't register flight toggle command for towny integration, expect problems!"); + } + + private boolean allowFlightAt(Player player, @Nullable Town town) + { + // Town == null -> Wilderness + if (town == null) + return allowFlyInWilderness.get(); + + var resident = townyAPI.getResident(player); + if (resident == null) return false; + + // 玩家城镇 + var playerTown = resident.getTownOrNull(); + + // 如果这个town不支持飞行 + if (MetaDataUtil.hasMeta(town, allowMorphFlight) && !MetaDataUtil.getBoolean(town, allowMorphFlight)) + return false; + + // MetaDataUtil.setBoolean(town, allowMorphFlight, true, true); + + // 如果这个town信任这个玩家,那么true + if (town.getTrustedResidents().contains(resident)) + return true; + + // 玩家城镇 + + // 如果玩家没有城镇,返回false + // 因为上面检查了野外和城镇的信任,这里应该没有问题。 + if (playerTown == null) + return false; + + // 玩家就是城镇成员 + if (playerTown.getUUID() == town.getUUID()) + return true; + + // 盟友 + if (CombatUtil.isAlly(town, playerTown)) + return true; + + // 国家 + if (CombatUtil.isSameNation(town, playerTown)) + return true; + + return false; + } + + @EventHandler + public void onEnterPlot(PlayerEntersIntoTownBorderEvent e) + { + var player = e.getPlayer(); + updatePlayer(player, e.getEnteredTown()); + } + + @EventHandler + public void onLeavePlot(PlayerExitsFromTownBorderEvent e) + { + if (allowFlyInWilderness.get()) return; + + updatePlayer(e.getPlayer(), null, true); + } + + @EventHandler + public void onPlayerMorph(PlayerMorphEvent e) + { + if (e.getState().containsAbility(AbilityType.CAN_FLY)) + updatePlayer(e.getPlayer(), null); + } + + @EventHandler + public void onPlayerUnmorph(PlayerUnMorphEvent e) + { + var player = e.getPlayer(); + this.blockedPlayers.remove(player); + FlyAbility.unBlockPlayer(player, this); + } + + // Folia没有提供监听玩家改变世界的事件,所以我们只能监听EntityAddToWorldEvent + @EventHandler + public void onPlayerChangeWorld(EntityAddToWorldEvent e) + { + if (!(e.getEntity() instanceof Player player)) return; + + updatePlayer(player, null); + } + + @EventHandler + public void onPlayerJoin(PlayerJoinEvent e) + { + updatePlayer(e.getPlayer(), null); + } + + @EventHandler + public void onPlayerExit(PlayerQuitEvent e) + { + this.unblockPlayer(e.getPlayer()); + } + + public void updatePlayer(Player player, @Nullable Town town) + { + this.updatePlayer(player, town, false); + } + + public void updatePlayer(Player player, @Nullable Town town, boolean noLookup) + { + if (!worldUsingTowny(player.getWorld())) + { + unblockPlayer(player); + return; + } + + if (town == null && !noLookup) + town = townyAPI.getTown(player.getLocation()); + + if (allowFlightAt(player, town)) + unblockPlayer(player); + else + blockPlayer(player); + } + + private boolean worldUsingTowny(World world) + { + var townyWorld = townyAPI.getTownyWorld(world); + if (townyWorld == null) return false; + + return townyWorld.isUsingTowny(); + } + + private void blockPlayer(Player player) + { + var playerAlreadyBlocked = blockedPlayers.contains(player); + if (playerAlreadyBlocked) return; + + blockedPlayers.add(player); + FlyAbility.blockPlayer(player, this); + } + + private void unblockPlayer(Player player) + { + FlyAbility.unBlockPlayer(player, this); + blockedPlayers.remove(player); + } +} diff --git a/src/main/java/xyz/nifeather/morph/misc/integrations/towny/TownyToggleFlightCommand.java b/src/main/java/xyz/nifeather/morph/misc/integrations/towny/TownyToggleFlightCommand.java new file mode 100644 index 00000000..e4239703 --- /dev/null +++ b/src/main/java/xyz/nifeather/morph/misc/integrations/towny/TownyToggleFlightCommand.java @@ -0,0 +1,144 @@ +package xyz.nifeather.morph.misc.integrations.towny; + +import com.palmergames.bukkit.towny.TownyAPI; +import com.palmergames.bukkit.towny.object.Town; +import com.palmergames.bukkit.towny.utils.MetaDataUtil; +import org.bukkit.Bukkit; +import org.bukkit.command.Command; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; +import xiamomc.pluginbase.Command.IPluginCommand; +import xiamomc.pluginbase.Messages.FormattableMessage; +import xyz.nifeather.morph.MorphPluginObject; +import xyz.nifeather.morph.messages.CommandNameStrings; +import xyz.nifeather.morph.messages.CommandStrings; +import xyz.nifeather.morph.messages.CommonStrings; +import xyz.nifeather.morph.messages.MessageUtils; + +import java.util.List; + +public class TownyToggleFlightCommand extends MorphPluginObject implements IPluginCommand +{ + private final TownyAdapter adapter; + + public TownyToggleFlightCommand(TownyAdapter adapter) + { + this.adapter = adapter; + } + + @Override + public String getCommandName() + { + return "toggle-town-morph-flight"; + } + + @Override + public FormattableMessage getHelpMessage() + { + return new FormattableMessage(plugin, "Toggle town flight"); + } + + private final List validOptions = List.of("on", "off"); + + @Override + public List onTabComplete(List args, CommandSender source) + { + if (args.size() > 1) return List.of(); + + var argZero = args.isEmpty() ? "" : args.get(0); + var zeroFinal = argZero.toUpperCase(); + + return validOptions.stream().filter(str -> str.toUpperCase().startsWith(zeroFinal)).toList(); + } + + @Override + public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String baseName, @NotNull String[] args) + { + if (!(sender instanceof Player player)) + return false; + + var towny = TownyAPI.getInstance(); + var town = towny.getTown(player); + var resident = towny.getResident(player); + + if (town == null) + { + sender.sendMessage(MessageUtils.prefixes(sender, CommandStrings.townyDoesntHaveTown())); + return true; + } + + if (resident == null) + { + sender.sendMessage(MessageUtils.prefixes(sender, CommandStrings.unknownError())); + return true; + } + + if (!town.isMayor(resident)) + { + sender.sendMessage(MessageUtils.prefixes(sender, CommandStrings.townyPlayerNotMayor())); + return true; + } + + var playerLocale = MessageUtils.getLocale(sender); + var message = CommandStrings.optionValueString() + .resolve("what", CommandNameStrings.morphFlightForTownX() + .withLocale(playerLocale) + .resolve("which", town.getName())); + + boolean allow; + + if (args.length < 1) + { + boolean currentAllow = true; + + var bdf = TownyAdapter.allowMorphFlight; + if (MetaDataUtil.hasMeta(town, bdf)) + currentAllow = !MetaDataUtil.getBoolean(town, bdf); + + allow = currentAllow; + } + else + { + allow = parse(args[0]); + } + + setTownFlightStatus(town, allow); + + message.resolve("value", (allow ? CommonStrings.on() : CommonStrings.off()).withLocale(playerLocale)); + + sender.sendMessage(MessageUtils.prefixes(sender, message)); + + return true; + } + + private boolean parse(String str) + { + if (str.equalsIgnoreCase("on")) return true; + else if (str.equalsIgnoreCase("off")) return false; + else return Boolean.parseBoolean(str); + } + + private void setTownFlightStatus(Town town, boolean newStatus) + { + MetaDataUtil.setBoolean(town, TownyAdapter.allowMorphFlight, newStatus, true); + + refreshPlayersIn(town); + } + + private void refreshPlayersIn(Town town) + { + // Towny没有API来告诉我们一个Town里进了多少玩家 + // 因此我们只能遍历所有玩家实例 + Bukkit.getOnlinePlayers().forEach(player -> + { + // 获取玩家爱所在的Town + var currentTown = TownyAPI.getInstance().getTown(player.getLocation()); + + // 在野外或者不是目标town + if (currentTown == null || currentTown != town) return; + + adapter.updatePlayer(player, currentTown); + }); + } +} diff --git a/src/main/resources/assets/feathermorph/lang/en_us.json b/src/main/resources/assets/feathermorph/lang/en_us.json index dc9cfa21..8a5aad66 100644 --- a/src/main/resources/assets/feathermorph/lang/en_us.json +++ b/src/main/resources/assets/feathermorph/lang/en_us.json @@ -50,6 +50,7 @@ "commands.option.name.mirror_interaction": "Interaction control", "commands.option.name.mirror_sneak": "Sneaking control", "commands.option.name.mirror_swaphand": "Swaphand control", + "commands.option.name.morph_flight_for_town_x": "Morph flight for town ", "commands.option_get": "Option \"\" is currently set to: ", "commands.option_set": "Successfully set \"\" to ", "commands.queayall_offline": " (Offline)", @@ -88,9 +89,14 @@ "commands.going_to_play_animation": "Going to play animation ", "commands.grant_item_success": "Successfully grant item. If the item is not found please check if the inventory is full.", "commands.operation_success": "Operation success", + "commands.towny_dont_belong_to_any_town": "You don't belong to any town!", + "commands.unknown_error": "Unknown error occurred while processing the request", + "commands.towny_not_mayor": "This operation is only available for the mayor", "common.command_not_found": "Command not found", "common.player_not_defined": "No player specified", "common.player_not_found": "No such player or they are offline", + "common.on": "On", + "common.off": "Off", "help.avaliable_cmd_header": "Available commands (Click to send/check)", "help.click_to_complete": "Click to complete", "help.click_to_view": "Click to check", diff --git a/src/main/resources/assets/feathermorph/lang/zh_cn.json b/src/main/resources/assets/feathermorph/lang/zh_cn.json index 29f5df98..55f64996 100644 --- a/src/main/resources/assets/feathermorph/lang/zh_cn.json +++ b/src/main/resources/assets/feathermorph/lang/zh_cn.json @@ -52,6 +52,7 @@ "commands.option.name.mirror_interaction" : "交互控制", "commands.option.name.mirror_sneak" : "潜行控制", "commands.option.name.mirror_swaphand" : "副手交换控制", + "commands.option.name.morph_flight_for_town_x": "的伪装飞行", "commands.option_get" : "已设置为", "commands.option_set" : "已将选项设置为", "commands.queayall_offline" : "(离线)", @@ -88,9 +89,14 @@ "commands.going_to_play_animation": "即将播放动作 ", "commands.grant_item_success": "成功给与物品。如果没有请检查是否背包已满", "commands.operation_success": "操作成功执行", + "commands.towny_dont_belong_to_any_town": "你不属于任何城镇!", + "commands.unknown_error": "处理请求时发生了未知错误", + "commands.towny_not_mayor": "此操作只适用于镇长", "common.command_not_found" : "未找到此指令", "common.player_not_defined" : "未指定玩家", "common.player_not_found" : "未找到目标玩家或对方已离线", + "common.on": "启用", + "common.off": "禁用", "help.avaliable_cmd_header" : "当前可用的指令(单击补全/查看)", "help.click_to_complete" : "点击补全", "help.click_to_view" : "点击查看", diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index a398fe39..1dd6f272 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -73,6 +73,16 @@ root: # Some features may not be affected by this option. single_language: false + # Integration options + integrations: + # For Towny >= 0.100.4.0 + towny: + # Should we allow flight in wilderness? + # + # Note that this doesn't affect worlds that are not managed by Towny + # To disable flying in these worlds, head to `flying > nofly_worlds`! + allow_flight_in_wilderness: false + # Should we enable the "Disguise Revealing" feature? # # This was made to nerf the ability for players to not get targeted by hostile mobs.