From 8e50bc4b0c2532432d6aecb83f6be5ebe157d14d Mon Sep 17 00:00:00 2001 From: AlphaKR93 Date: Fri, 3 May 2024 17:12:08 +0900 Subject: [PATCH] aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa --- patches/0001-Purpur-Server-Patches.patch | 27352 ++++++++++++++++ .../0002-Use-Gradle-Version-Catalogs.patch | 89 + ...{0003-Rebrand.patch => 0004-Rebrand.patch} | 137 +- ...and-Logo.patch => 0005-Rebrand-Logo.patch} | 0 ...ns.patch => 0006-Reduce-allocations.patch} | 480 +- ...patch => 0007-Plazma-Configurations.patch} | 205 +- ...-Setup-basic-configuration-sections.patch} | 0 ...lways-agree-EULA-on-development-mode.patch | 154 - ...h => 0009-Port-SparklyPaper-patches.patch} | 21 +- ...lways-agree-EULA-on-development-mode.patch | 18 + 10 files changed, 27871 insertions(+), 585 deletions(-) create mode 100644 patches/0001-Purpur-Server-Patches.patch create mode 100644 patches/0002-Use-Gradle-Version-Catalogs.patch rename patches/server/{0003-Rebrand.patch => 0004-Rebrand.patch} (86%) rename patches/server/{0004-Rebrand-Logo.patch => 0005-Rebrand-Logo.patch} (100%) rename patches/server/{0005-Reduce-allocations.patch => 0006-Reduce-allocations.patch} (73%) rename patches/server/{0006-Plazma-Configurations.patch => 0007-Plazma-Configurations.patch} (88%) rename patches/server/{0007-Setup-basic-configuration-sections.patch => 0008-Setup-basic-configuration-sections.patch} (100%) delete mode 100644 patches/server/0009-Always-agree-EULA-on-development-mode.patch rename patches/server/{0008-Port-SparklyPaper-patches.patch => 0009-Port-SparklyPaper-patches.patch} (90%) create mode 100644 patches/server/0010-Always-agree-EULA-on-development-mode.patch diff --git a/patches/0001-Purpur-Server-Patches.patch b/patches/0001-Purpur-Server-Patches.patch new file mode 100644 index 0000000..af38473 --- /dev/null +++ b/patches/0001-Purpur-Server-Patches.patch @@ -0,0 +1,27352 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: AlphaKR93 +Date: Tue, 30 Apr 2024 23:09:48 +0900 +Subject: [PATCH] Purpur Server Patches + + +diff --git a/build.gradle.kts b/build.gradle.kts +index 5d448d8a7cf6626a11791f30ad52baf41a099272..81996f00384674b29368e8bea944bdd14d631da3 100644 +--- a/build.gradle.kts ++++ b/build.gradle.kts +@@ -12,8 +12,12 @@ configurations.named(log4jPlugins.compileClasspathConfigurationName) { + val alsoShade: Configuration by configurations.creating + + dependencies { +- implementation(project(":paper-api")) +- implementation(project(":paper-mojangapi")) ++ // Purpur start ++ implementation(project(":purpur-api")) ++ implementation("io.papermc.paper:paper-mojangapi:${project.version}") { ++ exclude("io.papermc.paper", "paper-api") ++ } ++ // Purpur end + // Paper start + implementation("org.jline:jline-terminal-jansi:3.21.0") + implementation("net.minecrell:terminalconsoleappender:1.3.0") +@@ -47,6 +51,10 @@ dependencies { + runtimeOnly("org.apache.maven.resolver:maven-resolver-connector-basic:1.9.18") + runtimeOnly("org.apache.maven.resolver:maven-resolver-transport-http:1.9.18") + ++ implementation("org.mozilla:rhino-runtime:1.7.14") // Purpur ++ implementation("org.mozilla:rhino-engine:1.7.14") // Purpur ++ implementation("dev.omega24:upnp4j:1.0") // Purpur ++ + testImplementation("io.github.classgraph:classgraph:4.8.47") // Paper - mob goal test + testImplementation("org.junit.jupiter:junit-jupiter:5.10.2") + testImplementation("org.hamcrest:hamcrest:2.2") +@@ -79,7 +87,7 @@ tasks.jar { + attributes( + "Main-Class" to "org.bukkit.craftbukkit.Main", + "Implementation-Title" to "CraftBukkit", +- "Implementation-Version" to "git-Paper-$implementationVersion", ++ "Implementation-Version" to "git-Purpur-$implementationVersion", // Pufferfish // Purpur + "Implementation-Vendor" to date, // Paper + "Specification-Title" to "Bukkit", + "Specification-Version" to project.version, +@@ -138,7 +146,7 @@ fun TaskContainer.registerRunTask( + name: String, + block: JavaExec.() -> Unit + ): TaskProvider = register(name) { +- group = "paper" ++ group = "paperweight" // Purpur + mainClass.set("org.bukkit.craftbukkit.Main") + standardInput = System.`in` + workingDir = rootProject.layout.projectDirectory +diff --git a/src/log4jPlugins/java/org/purpurmc/purpur/gui/HighlightErrorConverter.java b/src/log4jPlugins/java/org/purpurmc/purpur/gui/HighlightErrorConverter.java +new file mode 100644 +index 0000000000000000000000000000000000000000..15a226e3854d731f7724025ea3459c8ace07630c +--- /dev/null ++++ b/src/log4jPlugins/java/org/purpurmc/purpur/gui/HighlightErrorConverter.java +@@ -0,0 +1,85 @@ ++package org.purpurmc.purpur.gui.util; ++ ++import org.apache.logging.log4j.Level; ++import org.apache.logging.log4j.core.LogEvent; ++import org.apache.logging.log4j.core.config.Configuration; ++import org.apache.logging.log4j.core.config.plugins.Plugin; ++import org.apache.logging.log4j.core.layout.PatternLayout; ++import org.apache.logging.log4j.core.pattern.ConverterKeys; ++import org.apache.logging.log4j.core.pattern.LogEventPatternConverter; ++import org.apache.logging.log4j.core.pattern.PatternConverter; ++import org.apache.logging.log4j.core.pattern.PatternFormatter; ++import org.apache.logging.log4j.core.pattern.PatternParser; ++import org.apache.logging.log4j.util.PerformanceSensitive; ++ ++import java.util.List; ++ ++@Plugin(name = "highlightGUIError", category = PatternConverter.CATEGORY) ++@ConverterKeys({"highlightGUIError"}) ++@PerformanceSensitive("allocation") ++public final class HighlightErrorConverter extends LogEventPatternConverter { ++ private static final String ERROR = "\u00A74\u00A7l"; // Bold Red ++ private static final String WARN = "\u00A7e\u00A7l"; // Bold Yellow ++ ++ private final List formatters; ++ ++ private HighlightErrorConverter(List formatters) { ++ super("highlightGUIError", null); ++ this.formatters = formatters; ++ } ++ ++ @Override ++ public void format(LogEvent event, StringBuilder toAppendTo) { ++ Level level = event.getLevel(); ++ if (level.isMoreSpecificThan(Level.ERROR)) { ++ format(ERROR, event, toAppendTo); ++ return; ++ } else if (level.isMoreSpecificThan(Level.WARN)) { ++ format(WARN, event, toAppendTo); ++ return; ++ } ++ for (PatternFormatter formatter : formatters) { ++ formatter.format(event, toAppendTo); ++ } ++ } ++ ++ private void format(String style, LogEvent event, StringBuilder toAppendTo) { ++ int start = toAppendTo.length(); ++ toAppendTo.append(style); ++ int end = toAppendTo.length(); ++ ++ for (PatternFormatter formatter : formatters) { ++ formatter.format(event, toAppendTo); ++ } ++ ++ if (toAppendTo.length() == end) { ++ toAppendTo.setLength(start); ++ } ++ } ++ ++ @Override ++ public boolean handlesThrowable() { ++ for (final PatternFormatter formatter : formatters) { ++ if (formatter.handlesThrowable()) { ++ return true; ++ } ++ } ++ return false; ++ } ++ ++ public static HighlightErrorConverter newInstance(Configuration config, String[] options) { ++ if (options.length != 1) { ++ LOGGER.error("Incorrect number of options on highlightGUIError. Expected 1 received " + options.length); ++ return null; ++ } ++ ++ if (options[0] == null) { ++ LOGGER.error("No pattern supplied on highlightGUIError"); ++ return null; ++ } ++ ++ PatternParser parser = PatternLayout.createPatternParser(config); ++ List formatters = parser.parse(options[0]); ++ return new HighlightErrorConverter(formatters); ++ } ++} +diff --git a/src/main/java/com/destroystokyo/paper/Metrics.java b/src/main/java/com/destroystokyo/paper/Metrics.java +index 4b002e8b75d117b726b0de274a76d3596fce015b..8cde30544e14f8fc2dac32966ae3c21f8cf3a551 100644 +--- a/src/main/java/com/destroystokyo/paper/Metrics.java ++++ b/src/main/java/com/destroystokyo/paper/Metrics.java +@@ -593,7 +593,7 @@ public class Metrics { + boolean logFailedRequests = config.getBoolean("logFailedRequests", false); + // Only start Metrics, if it's enabled in the config + if (config.getBoolean("enabled", true)) { +- Metrics metrics = new Metrics("Paper", serverUUID, logFailedRequests, Bukkit.getLogger()); ++ Metrics metrics = new Metrics("Purpur", serverUUID, logFailedRequests, Bukkit.getLogger()); // Pufferfish // Purpur + + metrics.addCustomChart(new Metrics.SimplePie("minecraft_version", () -> { + String minecraftVersion = Bukkit.getVersion(); +@@ -602,16 +602,8 @@ public class Metrics { + })); + + metrics.addCustomChart(new Metrics.SingleLineChart("players", () -> Bukkit.getOnlinePlayers().size())); +- metrics.addCustomChart(new Metrics.SimplePie("online_mode", () -> Bukkit.getOnlineMode() ? "online" : "offline")); +- final String paperVersion; +- final String implVersion = org.bukkit.craftbukkit.Main.class.getPackage().getImplementationVersion(); +- if (implVersion != null) { +- final String buildOrHash = implVersion.substring(implVersion.lastIndexOf('-') + 1); +- paperVersion = "git-Paper-%s-%s".formatted(Bukkit.getServer().getMinecraftVersion(), buildOrHash); +- } else { +- paperVersion = "unknown"; +- } +- metrics.addCustomChart(new Metrics.SimplePie("paper_version", () -> paperVersion)); ++ metrics.addCustomChart(new Metrics.SimplePie("online_mode", () -> Bukkit.getOnlineMode() ? "online" : (io.papermc.paper.configuration.GlobalConfiguration.get().proxies.isProxyOnlineMode() ? "bungee" : "offline"))); // Purpur ++ metrics.addCustomChart(new Metrics.SimplePie("purpur_version", () -> (org.bukkit.craftbukkit.Main.class.getPackage().getImplementationVersion() != null) ? org.bukkit.craftbukkit.Main.class.getPackage().getImplementationVersion() : "unknown")); // Purpur + + metrics.addCustomChart(new Metrics.DrilldownPie("java_version", () -> { + Map> map = new HashMap<>(); +diff --git a/src/main/java/com/destroystokyo/paper/PaperVersionFetcher.java b/src/main/java/com/destroystokyo/paper/PaperVersionFetcher.java +index 9d687da5bdf398bb3f6c84cdf1249a7213d09f2e..462a6eed350fd660ddaf25d567bb6e97b77d0b2b 100644 +--- a/src/main/java/com/destroystokyo/paper/PaperVersionFetcher.java ++++ b/src/main/java/com/destroystokyo/paper/PaperVersionFetcher.java +@@ -19,8 +19,10 @@ import java.util.stream.StreamSupport; + + public class PaperVersionFetcher implements VersionFetcher { + private static final java.util.regex.Pattern VER_PATTERN = java.util.regex.Pattern.compile("^([0-9\\.]*)\\-.*R"); // R is an anchor, will always give '-R' at end +- private static final String GITHUB_BRANCH_NAME = "master"; +- private static final String DOWNLOAD_PAGE = "https://papermc.io/downloads/paper"; ++ // Purpur start ++ private static final String DOWNLOAD_PAGE = "https://purpurmc.org/downloads"; ++ private static int distance = -2; public int distance() { return distance; } ++ // Purpur end + private static @Nullable String mcVer; + + @Override +@@ -31,11 +33,11 @@ public class PaperVersionFetcher implements VersionFetcher { + @Nonnull + @Override + public Component getVersionMessage(@Nonnull String serverVersion) { +- String[] parts = serverVersion.substring("git-Paper-".length()).split("[-\\s]"); +- final Component updateMessage = getUpdateStatusMessage("PaperMC/Paper", GITHUB_BRANCH_NAME, parts[0]); ++ String[] parts = serverVersion.substring("git-Purpur-".length()).split("[-\\s]"); // Purpur ++ final Component updateMessage = getUpdateStatusMessage("PurpurMC/Purpur", "ver/" + getMinecraftVersion(), parts[0]); // Purpur + final Component history = getHistory(); + +- return history != null ? TextComponent.ofChildren(updateMessage, Component.newline(), history) : updateMessage; ++ return history != null ? Component.join(net.kyori.adventure.text.JoinConfiguration.separator(Component.newline()), history, updateMessage) : updateMessage; // Purpur + } + + private static @Nullable String getMinecraftVersion() { +@@ -45,7 +47,7 @@ public class PaperVersionFetcher implements VersionFetcher { + String result = matcher.group(); + mcVer = result.substring(0, result.length() - 2); // strip 'R' anchor and trailing '-' + } else { +- org.bukkit.Bukkit.getLogger().warning("Unable to match version to pattern! Report to PaperMC!"); ++ org.bukkit.Bukkit.getLogger().warning("Unable to match version to pattern! Report to Purpur!"); // Purpur + org.bukkit.Bukkit.getLogger().warning("Pattern: " + VER_PATTERN.toString()); + org.bukkit.Bukkit.getLogger().warning("Version: " + org.bukkit.Bukkit.getBukkitVersion()); + } +@@ -55,7 +57,7 @@ public class PaperVersionFetcher implements VersionFetcher { + } + + private static Component getUpdateStatusMessage(@Nonnull String repo, @Nonnull String branch, @Nonnull String versionInfo) { +- int distance; ++ //int distance; // Purpur - use field + try { + int jenkinsBuild = Integer.parseInt(versionInfo); + distance = fetchDistanceFromSiteApi(jenkinsBuild, getMinecraftVersion()); +@@ -66,13 +68,13 @@ public class PaperVersionFetcher implements VersionFetcher { + + switch (distance) { + case -1: +- return Component.text("Error obtaining version information", NamedTextColor.YELLOW); ++ return Component.text("* Error obtaining version information", NamedTextColor.RED); // Purpur + case 0: +- return Component.text("You are running the latest version", NamedTextColor.GREEN); ++ return Component.text("* You are running the latest version", NamedTextColor.GREEN); // Purpur + case -2: +- return Component.text("Unknown version", NamedTextColor.YELLOW); ++ return Component.text("* Unknown version", NamedTextColor.YELLOW); // Purpur + default: +- return Component.text("You are " + distance + " version(s) behind", NamedTextColor.YELLOW) ++ return Component.text("* You are " + distance + " version(s) behind", NamedTextColor.YELLOW) // Purpur + .append(Component.newline()) + .append(Component.text("Download the new version at: ") + .append(Component.text(DOWNLOAD_PAGE, NamedTextColor.GOLD) +@@ -85,15 +87,11 @@ public class PaperVersionFetcher implements VersionFetcher { + if (siteApiVersion == null) { return -1; } + try { + try (BufferedReader reader = Resources.asCharSource( +- new URL("https://api.papermc.io/v2/projects/paper/versions/" + siteApiVersion), ++ new URL("https://api.purpurmc.org/v2/purpur/" + siteApiVersion), // Purpur + Charsets.UTF_8 + ).openBufferedStream()) { + JsonObject json = new Gson().fromJson(reader, JsonObject.class); +- JsonArray builds = json.getAsJsonArray("builds"); +- int latest = StreamSupport.stream(builds.spliterator(), false) +- .mapToInt(e -> e.getAsInt()) +- .max() +- .getAsInt(); ++ int latest = json.getAsJsonObject("builds").getAsJsonPrimitive("latest").getAsInt(); // Purpur + return latest - jenkinsBuild; + } catch (JsonSyntaxException ex) { + ex.printStackTrace(); +@@ -144,6 +142,6 @@ public class PaperVersionFetcher implements VersionFetcher { + return null; + } + +- return Component.text("Previous version: " + oldVersion, NamedTextColor.GRAY, TextDecoration.ITALIC); ++ return org.bukkit.ChatColor.parseMM("Previous: %s", oldVersion); // Purpur + } + } +diff --git a/src/main/java/com/destroystokyo/paper/console/PaperConsole.java b/src/main/java/com/destroystokyo/paper/console/PaperConsole.java +index c5d5648f4ca603ef2b1df723b58f9caf4dd3c722..3cb56595822799926a8141e60a42f5d1edfc6de5 100644 +--- a/src/main/java/com/destroystokyo/paper/console/PaperConsole.java ++++ b/src/main/java/com/destroystokyo/paper/console/PaperConsole.java +@@ -17,7 +17,7 @@ public final class PaperConsole extends SimpleTerminalConsole { + @Override + protected LineReader buildReader(LineReaderBuilder builder) { + builder +- .appName("Paper") ++ .appName("Purpur") // Purpur + .variable(LineReader.HISTORY_FILE, java.nio.file.Paths.get(".console_history")) + .completer(new ConsoleCommandCompleter(this.server)) + .option(LineReader.Option.COMPLETE_IN_WORD, true); +diff --git a/src/main/java/com/destroystokyo/paper/entity/ai/MobGoalHelper.java b/src/main/java/com/destroystokyo/paper/entity/ai/MobGoalHelper.java +index c72d6bccf7d72d08d388c65936a89c92261c7860..ee746753515c9cea8dd246f4f56e6781956726c1 100644 +--- a/src/main/java/com/destroystokyo/paper/entity/ai/MobGoalHelper.java ++++ b/src/main/java/com/destroystokyo/paper/entity/ai/MobGoalHelper.java +@@ -137,6 +137,10 @@ public class MobGoalHelper { + static { + // TODO these kinda should be checked on each release, in case obfuscation changes + deobfuscationMap.put("abstract_skeleton_1", "abstract_skeleton_melee"); ++ // Purpur start ++ deobfuscationMap.put("zombie_1", "zombie_attack_villager"); ++ deobfuscationMap.put("drowned_1", "drowned_attack_villager"); ++ // Purpur end + + ignored.add("goal_selector_1"); + ignored.add("goal_selector_2"); +diff --git a/src/main/java/com/destroystokyo/paper/gui/RAMDetails.java b/src/main/java/com/destroystokyo/paper/gui/RAMDetails.java +index 039a86034928a5eb7aaa2d7ca76a7bddcca346bd..308f67d0616e2d6bb135258f1fda53ccdee01430 100644 +--- a/src/main/java/com/destroystokyo/paper/gui/RAMDetails.java ++++ b/src/main/java/com/destroystokyo/paper/gui/RAMDetails.java +@@ -68,7 +68,7 @@ public class RAMDetails extends JList { + vector.add("Memory use: " + (data.getUsedMem() / 1024L / 1024L) + " mb (" + (data.getFree() * 100L / data.getMax()) + "% free)"); + vector.add("Heap: " + (data.getTotal() / 1024L / 1024L) + " / " + (data.getMax() / 1024L / 1024L) + " mb"); + vector.add("Avg tick: " + DECIMAL_FORMAT.format((double)this.server.getAverageTickTimeNanos() / (double) TimeUtil.NANOSECONDS_PER_MILLISECOND) + " ms"); +- vector.add("TPS from last 1m, 5m, 15m: " + String.join(", ", tpsAvg)); ++ vector.add("TPS from last 5s, 1m, 5m, 15m: " + String.join(", ", tpsAvg)); // Purpur + setListData(vector); + } + +diff --git a/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkHolderManager.java b/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkHolderManager.java +index 6bc7c6f16a1649fc9e24e7cf90fca401e5bd4875..e1ffd62f4ebceecb9bc5471df3da406cffea0483 100644 +--- a/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkHolderManager.java ++++ b/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkHolderManager.java +@@ -1316,9 +1316,9 @@ public final class ChunkHolderManager { + } + + public boolean processTicketUpdates() { +- co.aikar.timings.MinecraftTimings.distanceManagerTick.startTiming(); try { // Paper - add timings for distance manager ++ //co.aikar.timings.MinecraftTimings.distanceManagerTick.startTiming(); try { // Paper - add timings for distance manager // Purpur + return this.processTicketUpdates(true, true, null); +- } finally { co.aikar.timings.MinecraftTimings.distanceManagerTick.stopTiming(); } // Paper - add timings for distance manager ++ //} finally { co.aikar.timings.MinecraftTimings.distanceManagerTick.stopTiming(); } // Paper - add timings for distance manager // Purpur + } + + private static final ThreadLocal> CURRENT_TICKET_UPDATE_SCHEDULING = new ThreadLocal<>(); +diff --git a/src/main/java/io/papermc/paper/chunk/system/scheduling/NewChunkHolder.java b/src/main/java/io/papermc/paper/chunk/system/scheduling/NewChunkHolder.java +index 56b07a3306e5735816c8d89601b519cb0db6379a..604de7aed6db44c9c84d541765e57da48883cf00 100644 +--- a/src/main/java/io/papermc/paper/chunk/system/scheduling/NewChunkHolder.java ++++ b/src/main/java/io/papermc/paper/chunk/system/scheduling/NewChunkHolder.java +@@ -1779,7 +1779,7 @@ public final class NewChunkHolder { + boolean canSavePOI = !(chunk instanceof LevelChunk levelChunk && levelChunk.mustNotSave) && (poi != null && poi.isDirty()); + boolean canSaveEntities = entities != null; + +- try (co.aikar.timings.Timing ignored = this.world.timings.chunkSave.startTiming()) { // Paper ++ //try (co.aikar.timings.Timing ignored = this.world.timings.chunkSave.startTiming()) { // Paper // Purpur + if (canSaveChunk) { + canSaveChunk = this.saveChunk(chunk, unloading); + } +@@ -1793,7 +1793,7 @@ public final class NewChunkHolder { + this.lastEntityUnload = null; + } + } +- } ++ //} // Purpur + + return executedUnloadTask | canSaveChunk | canSaveEntities | canSavePOI ? new SaveStat(executedUnloadTask || canSaveChunk, canSaveEntities, canSavePOI): null; + } +diff --git a/src/main/java/io/papermc/paper/command/PaperPluginsCommand.java b/src/main/java/io/papermc/paper/command/PaperPluginsCommand.java +index f0fce4113fb07c64adbec029d177c236cbdcbae8..e94224ed280247ee69dfdff8dc960f2b8729be33 100644 +--- a/src/main/java/io/papermc/paper/command/PaperPluginsCommand.java ++++ b/src/main/java/io/papermc/paper/command/PaperPluginsCommand.java +@@ -78,10 +78,10 @@ public class PaperPluginsCommand extends BukkitCommand { + this.setAliases(Arrays.asList("pl")); + } + +- private static List formatProviders(TreeMap> plugins) { ++ private static List formatProviders(TreeMap> plugins, @NotNull CommandSender sender) { // Purpur + List components = new ArrayList<>(plugins.size()); + for (PluginProvider entry : plugins.values()) { +- components.add(formatProvider(entry)); ++ components.add(formatProvider(entry, sender)); // Purpur + } + + boolean isFirst = true; +@@ -109,7 +109,7 @@ public class PaperPluginsCommand extends BukkitCommand { + return formattedSublists; + } + +- private static Component formatProvider(PluginProvider provider) { ++ private static Component formatProvider(PluginProvider provider, @NotNull CommandSender sender) { // Purpur + TextComponent.Builder builder = Component.text(); + if (provider instanceof SpigotPluginProvider spigotPluginProvider && CraftMagicNumbers.isLegacy(spigotPluginProvider.getMeta())) { + builder.append(LEGACY_PLUGIN_STAR); +@@ -117,12 +117,64 @@ public class PaperPluginsCommand extends BukkitCommand { + + String name = provider.getMeta().getName(); + Component pluginName = Component.text(name, fromStatus(provider)) +- .clickEvent(ClickEvent.runCommand("/version " + name)); ++ // Purpur start ++ .clickEvent(ClickEvent.suggestCommand("/version " + name)); ++ ++ if (sender instanceof org.bukkit.entity.Player && sender.hasPermission("bukkit.command.version")) { ++ // Event components ++ String description = provider.getMeta().getDescription(); ++ TextComponent.Builder hover = Component.text(); ++ hover.append(Component.text("Version: ", NamedTextColor.WHITE)).append(Component.text(provider.getMeta().getVersion(), NamedTextColor.GREEN)); ++ ++ if (description != null) { ++ hover.append(Component.newline()) ++ .append(Component.text("Description: ", NamedTextColor.WHITE)) ++ .append(Component.text(description, NamedTextColor.GREEN)); ++ } ++ ++ if (provider.getMeta().getWebsite() != null) { ++ hover.append(Component.newline()) ++ .append(Component.text("Website: ", NamedTextColor.WHITE)) ++ .append(Component.text(provider.getMeta().getWebsite(), NamedTextColor.GREEN)); ++ } ++ ++ if (!provider.getMeta().getAuthors().isEmpty()) { ++ hover.append(Component.newline()); ++ if (provider.getMeta().getAuthors().size() == 1) { ++ hover.append(Component.text("Author: ")); ++ } else { ++ hover.append(Component.text("Authors: ")); ++ } ++ ++ hover.append(getAuthors(provider.getMeta())); ++ } ++ ++ pluginName.hoverEvent(hover.build()); ++ } + + builder.append(pluginName); ++ // Purpur end ++ ++ return builder.build(); ++ } ++ ++ // Purpur start ++ @NotNull ++ private static TextComponent getAuthors(@NotNull final PluginMeta pluginMeta) { ++ TextComponent.Builder builder = Component.text(); ++ List authors = pluginMeta.getAuthors(); ++ ++ for (int i = 0; i < authors.size(); i++) { ++ if (i > 0) { ++ builder.append(Component.text(i < authors.size() - 1 ? ", " : " and ", NamedTextColor.WHITE)); ++ } ++ ++ builder.append(Component.text(authors.get(i), NamedTextColor.GREEN)); ++ } + + return builder.build(); + } ++ // Purpur end + + private static Component asPlainComponents(String strings) { + net.kyori.adventure.text.TextComponent.Builder builder = Component.text(); +@@ -182,24 +234,24 @@ public class PaperPluginsCommand extends BukkitCommand { + } + } + +- Component infoMessage = Component.text("Server Plugins (%s):".formatted(paperPlugins.size() + spigotPlugins.size()), NamedTextColor.WHITE); ++ //Component infoMessage = Component.text("Server Plugins (%s):".formatted(paperPlugins.size() + spigotPlugins.size()), NamedTextColor.WHITE); + //.append(INFO_ICON_START.hoverEvent(SERVER_PLUGIN_INFO)); TODO: Add docs + +- sender.sendMessage(infoMessage); ++ //sender.sendMessage(infoMessage); // Purpur + + if (!paperPlugins.isEmpty()) { +- sender.sendMessage(PAPER_HEADER); ++ sender.sendMessage(PAPER_HEADER.append(Component.text(" (%s):".formatted(paperPlugins.size())))); // Purpur + } + +- for (Component component : formatProviders(paperPlugins)) { ++ for (Component component : formatProviders(paperPlugins, sender)) { // Purpur + sender.sendMessage(component); + } + + if (!spigotPlugins.isEmpty()) { +- sender.sendMessage(BUKKIT_HEADER); ++ sender.sendMessage(BUKKIT_HEADER.append(Component.text(" (%s):".formatted(spigotPlugins.size())))); // Purpur + } + +- for (Component component : formatProviders(spigotPlugins)) { ++ for (Component component : formatProviders(spigotPlugins, sender)) { // Purpur + sender.sendMessage(component); + } + +diff --git a/src/main/java/io/papermc/paper/logging/SysoutCatcher.java b/src/main/java/io/papermc/paper/logging/SysoutCatcher.java +index a8e813ca89b033f061e695288b3383bdcf128531..1ab65af9359d19530bba7f985a604d2a430ee234 100644 +--- a/src/main/java/io/papermc/paper/logging/SysoutCatcher.java ++++ b/src/main/java/io/papermc/paper/logging/SysoutCatcher.java +@@ -54,9 +54,9 @@ public final class SysoutCatcher { + final JavaPlugin plugin = JavaPlugin.getProvidingPlugin(clazz); + + // Instead of just printing the message, send it to the plugin's logger +- plugin.getLogger().log(this.level, this.prefix + line); ++ plugin.getLogger().log(this.level, /*this.prefix +*/ line); // Purpur - prefix not needed + +- if (SysoutCatcher.SUPPRESS_NAGS) { ++ if (true || SysoutCatcher.SUPPRESS_NAGS) { // Purpur - nagging is annoying + return; + } + if (SysoutCatcher.NAG_INTERVAL > 0 || SysoutCatcher.NAG_TIMEOUT > 0) { +diff --git a/src/main/java/io/papermc/paper/plugin/PluginInitializerManager.java b/src/main/java/io/papermc/paper/plugin/PluginInitializerManager.java +index 6f14cb9a73faa1d0ae2939d08809d9f6c2a99e1d..4e98745670032038f7b4f8e1adabc1e00e7f15bf 100644 +--- a/src/main/java/io/papermc/paper/plugin/PluginInitializerManager.java ++++ b/src/main/java/io/papermc/paper/plugin/PluginInitializerManager.java +@@ -112,6 +112,7 @@ public class PluginInitializerManager { + @SuppressWarnings("unchecked") + java.util.List files = ((java.util.List) optionSet.valuesOf("add-plugin")).stream().map(File::toPath).toList(); + io.papermc.paper.plugin.util.EntrypointUtil.registerProvidersFromSource(io.papermc.paper.plugin.provider.source.PluginFlagProviderSource.INSTANCE, files); ++ io.papermc.paper.plugin.util.EntrypointUtil.registerProvidersFromSource(io.papermc.paper.plugin.provider.source.SparkProviderSource.INSTANCE, new File("cache", "spark.jar").toPath()); // Purpur + } + + // This will be the end of me... +diff --git a/src/main/java/io/papermc/paper/plugin/provider/source/SparkProviderSource.java b/src/main/java/io/papermc/paper/plugin/provider/source/SparkProviderSource.java +new file mode 100644 +index 0000000000000000000000000000000000000000..cb78dac8e072b5cb3c6e52e17c9ecdf708aeedc1 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/plugin/provider/source/SparkProviderSource.java +@@ -0,0 +1,115 @@ ++package io.papermc.paper.plugin.provider.source; ++ ++import com.mojang.logging.LogUtils; ++import io.papermc.paper.plugin.entrypoint.Entrypoint; ++import io.papermc.paper.plugin.entrypoint.EntrypointHandler; ++import io.papermc.paper.plugin.entrypoint.LaunchEntryPointHandler; ++import io.papermc.paper.plugin.provider.PluginProvider; ++import java.io.BufferedReader; ++import java.io.File; ++import java.io.InputStreamReader; ++import java.math.BigInteger; ++import java.net.URL; ++import java.net.URLConnection; ++import java.nio.file.Files; ++import java.nio.file.Path; ++import java.nio.file.StandardCopyOption; ++import java.security.MessageDigest; ++import java.util.stream.Collectors; ++import org.bukkit.plugin.java.JavaPlugin; ++import org.slf4j.Logger; ++ ++public class SparkProviderSource implements ProviderSource { ++ ++ public static final SparkProviderSource INSTANCE = new SparkProviderSource(); ++ private static final FileProviderSource FILE_PROVIDER_SOURCE = new FileProviderSource("File '%s' specified by Purpur"::formatted); ++ private static final Logger LOGGER = LogUtils.getClassLogger(); ++ ++ @Override ++ public Path prepareContext(Path context) { ++ // first, check if user doesn't want spark at all ++ if (Boolean.getBoolean("Purpur.IReallyDontWantSpark")) { ++ return null; // boo! ++ } ++ ++ // second, check if user has their own spark ++ if (hasSpark()) { ++ LOGGER.info("Purpur: Using user-provided spark plugin instead of our own."); ++ return null; // let's hope it's at least the modern version :3 ++ } ++ ++ // you can't have errors in your code if you wrap the entire codebase in a try/catch block ++ try { ++ ++ // make sure the directory exists where we want to keep spark ++ File file = context.toFile(); ++ file.getParentFile().mkdirs(); ++ ++ boolean shouldDownload; ++ ++ // check if our spark exists ++ if (!file.exists()) { ++ // it does not, so let's download it ++ shouldDownload = true; ++ } else { ++ // we have a spark file, let's see if it's up-to-date by comparing shas ++ String fileSha1 = String.format("%040x", new BigInteger(1, MessageDigest.getInstance("SHA-1").digest(Files.readAllBytes(file.toPath())))); ++ String sparkSha1; ++ ++ // luck has a nifty endpoint containing the sha of the newest version ++ URLConnection urlConnection = new URL("https://sparkapi.lucko.me/download/bukkit/sha1").openConnection(); ++ ++ // set a reasonable timeout to prevent servers without internet from hanging for 60+ seconds on startup ++ urlConnection.setReadTimeout(5000); ++ urlConnection.setConnectTimeout(5000); ++ ++ // read it ++ try (BufferedReader reader = new BufferedReader(new InputStreamReader(urlConnection.getInputStream()))) { ++ sparkSha1 = reader.lines().collect(Collectors.joining("")); ++ } ++ ++ // compare; we only download a new spark if the shas don't match ++ shouldDownload = !fileSha1.equals(sparkSha1); ++ } ++ ++ // ok, finally we can download spark if we need it ++ if (shouldDownload) { ++ URLConnection urlConnection = new URL("https://sparkapi.lucko.me/download/bukkit").openConnection(); ++ urlConnection.setReadTimeout(5000); ++ urlConnection.setConnectTimeout(5000); ++ Files.copy(urlConnection.getInputStream(), file.toPath(), StandardCopyOption.REPLACE_EXISTING); ++ } ++ ++ // register the spark, newly downloaded or existing ++ return FILE_PROVIDER_SOURCE.prepareContext(context); ++ ++ } catch (Throwable e) { ++ LOGGER.error("Purpur: Failed to download and install spark plugin", e); ++ } ++ return null; ++ } ++ ++ @Override ++ public void registerProviders(final EntrypointHandler entrypointHandler, final Path context) { ++ if (context == null) { ++ return; ++ } ++ ++ try { ++ FILE_PROVIDER_SOURCE.registerProviders(entrypointHandler, context); ++ } catch (IllegalArgumentException ignored) { ++ // Ignore illegal argument exceptions from jar checking ++ } catch (Exception e) { ++ LOGGER.error("Error loading our spark plugin: " + e.getMessage(), e); ++ } ++ } ++ ++ private static boolean hasSpark() { ++ for (PluginProvider provider : LaunchEntryPointHandler.INSTANCE.get(Entrypoint.PLUGIN).getRegisteredProviders()) { ++ if (provider.getMeta().getName().equalsIgnoreCase("spark")) { ++ return true; ++ } ++ } ++ return false; ++ } ++} +diff --git a/src/main/java/net/minecraft/CrashReport.java b/src/main/java/net/minecraft/CrashReport.java +index 4f3cc14d48690bb183d09bb7a5ba1e23e8a0c08a..c366d84518979e842a6f10f969a5951539ecac93 100644 +--- a/src/main/java/net/minecraft/CrashReport.java ++++ b/src/main/java/net/minecraft/CrashReport.java +@@ -125,6 +125,10 @@ public class CrashReport { + StringBuilder stringbuilder = new StringBuilder(); + + stringbuilder.append("---- Minecraft Crash Report ----\n"); ++ // Purpur start ++ stringbuilder.append("// "); ++ stringbuilder.append("// DO NOT REPORT THIS TO PAPER! REPORT TO PURPUR INSTEAD!"); ++ // Purpur end + stringbuilder.append("// "); + stringbuilder.append(CrashReport.getErrorComment()); + stringbuilder.append("\n\n"); +diff --git a/src/main/java/net/minecraft/commands/CommandSourceStack.java b/src/main/java/net/minecraft/commands/CommandSourceStack.java +index e6c7f62ed379a78645933670299e4fcda8540ed1..7475aaac2673729091eabc741c8ebb561aeec8f1 100644 +--- a/src/main/java/net/minecraft/commands/CommandSourceStack.java ++++ b/src/main/java/net/minecraft/commands/CommandSourceStack.java +@@ -230,6 +230,19 @@ public class CommandSourceStack implements ExecutionCommandSource").replacement(bukkitPermission).build()))); ++ } ++ return false; ++ } ++ // Purpur end ++ + public Vec3 getPosition() { + return this.worldPosition; + } +@@ -331,6 +344,30 @@ public class CommandSourceStack implements ExecutionCommandSource io.papermc.paper.adventure.PaperAdventure.asVanilla(message), broadcastToOps); ++ } ++ // Purpur end ++ + public void sendSuccess(Supplier feedbackSupplier, boolean broadcastToOps) { + boolean flag1 = this.source.acceptsSuccess() && !this.silent; + boolean flag2 = broadcastToOps && this.source.shouldInformAdmins() && !this.silent; +diff --git a/src/main/java/net/minecraft/commands/Commands.java b/src/main/java/net/minecraft/commands/Commands.java +index aa2fca6917fb67fe0e9ba067d11487c3a274f675..f9d0e8ee9414b3897f268ba78a1469ddf868f51a 100644 +--- a/src/main/java/net/minecraft/commands/Commands.java ++++ b/src/main/java/net/minecraft/commands/Commands.java +@@ -166,7 +166,7 @@ public class Commands { + DamageCommand.register(this.dispatcher, commandRegistryAccess); + DataCommands.register(this.dispatcher); + DataPackCommand.register(this.dispatcher); +- DebugCommand.register(this.dispatcher); ++ //DebugCommand.register(this.dispatcher); // Purpur + DefaultGameModeCommands.register(this.dispatcher); + DifficultyCommand.register(this.dispatcher); + EffectCommands.register(this.dispatcher, commandRegistryAccess); +@@ -222,8 +222,8 @@ public class Commands { + JfrCommand.register(this.dispatcher); + } + +- if (SharedConstants.IS_RUNNING_IN_IDE) { +- TestCommand.register(this.dispatcher); ++ if (org.purpurmc.purpur.PurpurConfig.registerMinecraftDebugCommands || SharedConstants.IS_RUNNING_IN_IDE) { // Purpur ++ if (!org.purpurmc.purpur.PurpurConfig.registerMinecraftDebugCommands) TestCommand.register(this.dispatcher); // Purpur + ResetChunksCommand.register(this.dispatcher); + RaidCommand.register(this.dispatcher, commandRegistryAccess); + DebugPathCommand.register(this.dispatcher); +@@ -252,6 +252,14 @@ public class Commands { + StopCommand.register(this.dispatcher); + TransferCommand.register(this.dispatcher); + WhitelistCommand.register(this.dispatcher); ++ org.purpurmc.purpur.command.CreditsCommand.register(this.dispatcher); // Purpur ++ org.purpurmc.purpur.command.DemoCommand.register(this.dispatcher); // Purpur ++ org.purpurmc.purpur.command.PingCommand.register(this.dispatcher); // Purpur ++ org.purpurmc.purpur.command.UptimeCommand.register(this.dispatcher); // Purpur ++ org.purpurmc.purpur.command.TPSBarCommand.register(this.dispatcher); // Purpur ++ org.purpurmc.purpur.command.CompassCommand.register(this.dispatcher); // Purpur ++ org.purpurmc.purpur.command.RamBarCommand.register(this.dispatcher); // Purpur ++ org.purpurmc.purpur.command.RamCommand.register(this.dispatcher); // Purpur + } + + if (environment.includeIntegrated) { +@@ -327,9 +335,9 @@ public class Commands { + public void performCommand(ParseResults parseresults, String s, String label) { // CraftBukkit + CommandSourceStack commandlistenerwrapper = (CommandSourceStack) parseresults.getContext().getSource(); + +- commandlistenerwrapper.getServer().getProfiler().push(() -> { ++ /*commandlistenerwrapper.getServer().getProfiler().push(() -> { // Purpur + return "/" + s; +- }); ++ });*/ // Purpur + ContextChain contextchain = this.finishParsing(parseresults, s, commandlistenerwrapper, label); // CraftBukkit // Paper - Add UnknownCommandEvent + + try { +@@ -358,7 +366,7 @@ public class Commands { + Commands.LOGGER.error("'/{}' threw an exception", s, exception); + } + } finally { +- commandlistenerwrapper.getServer().getProfiler().pop(); ++ //commandlistenerwrapper.getServer().getProfiler().pop(); // Purpur + } + + } +@@ -501,6 +509,7 @@ public class Commands { + private void runSync(ServerPlayer player, Collection bukkit, RootCommandNode rootcommandnode) { + // Paper end - Perf: Async command map building + new com.destroystokyo.paper.event.brigadier.AsyncPlayerSendCommandsEvent(player.getBukkitEntity(), (RootCommandNode) rootcommandnode, false).callEvent(); // Paper - Brigadier API ++ if (PlayerCommandSendEvent.getHandlerList().getRegisteredListeners().length > 0) { // Purpur - skip all this crap if there's nothing listening + PlayerCommandSendEvent event = new PlayerCommandSendEvent(player.getBukkitEntity(), new LinkedHashSet<>(bukkit)); + event.getPlayer().getServer().getPluginManager().callEvent(event); + +@@ -511,6 +520,7 @@ public class Commands { + } + } + // CraftBukkit end ++ } // Purpur - skip event + player.connection.send(new ClientboundCommandsPacket(rootcommandnode)); + } + +diff --git a/src/main/java/net/minecraft/commands/arguments/selector/EntitySelector.java b/src/main/java/net/minecraft/commands/arguments/selector/EntitySelector.java +index 676a1499747b071515479130875157263d3a8352..fc1bba350030c076405711716e9830f8ae7f3953 100644 +--- a/src/main/java/net/minecraft/commands/arguments/selector/EntitySelector.java ++++ b/src/main/java/net/minecraft/commands/arguments/selector/EntitySelector.java +@@ -200,10 +200,10 @@ public class EntitySelector { + + if (this.playerName != null) { + entityplayer = source.getServer().getPlayerList().getPlayerByName(this.playerName); +- return (List) (entityplayer == null ? Collections.emptyList() : Lists.newArrayList(new ServerPlayer[]{entityplayer})); ++ return entityplayer == null || !canSee(source, entityplayer) ? Collections.emptyList() : Lists.newArrayList(entityplayer); // Purpur + } else if (this.entityUUID != null) { + entityplayer = source.getServer().getPlayerList().getPlayer(this.entityUUID); +- return (List) (entityplayer == null ? Collections.emptyList() : Lists.newArrayList(new ServerPlayer[]{entityplayer})); ++ return entityplayer == null || !canSee(source, entityplayer) ? Collections.emptyList() : Lists.newArrayList(entityplayer); // Purpur + } else { + Vec3 vec3d = (Vec3) this.position.apply(source.getPosition()); + Predicate predicate = this.getPredicate(vec3d); +@@ -215,7 +215,7 @@ public class EntitySelector { + ServerPlayer entityplayer1 = (ServerPlayer) entity; + + if (predicate.test(entityplayer1)) { +- return Lists.newArrayList(new ServerPlayer[]{entityplayer1}); ++ return !canSee(source, entityplayer1) ? Collections.emptyList() : Lists.newArrayList(entityplayer1); // Purpur + } + } + +@@ -226,6 +226,7 @@ public class EntitySelector { + + if (this.isWorldLimited()) { + object = source.getLevel().getPlayers(predicate, i); ++ ((List) object).removeIf(entityplayer3 -> !canSee(source, (ServerPlayer) entityplayer3)); // Purpur + } else { + object = Lists.newArrayList(); + Iterator iterator = source.getServer().getPlayerList().getPlayers().iterator(); +@@ -233,7 +234,7 @@ public class EntitySelector { + while (iterator.hasNext()) { + ServerPlayer entityplayer2 = (ServerPlayer) iterator.next(); + +- if (predicate.test(entityplayer2)) { ++ if (predicate.test(entityplayer2) && canSee(source, entityplayer2)) { // Purpur + ((List) object).add(entityplayer2); + if (((List) object).size() >= i) { + return (List) object; +@@ -278,4 +279,10 @@ public class EntitySelector { + public static Component joinNames(List entities) { + return ComponentUtils.formatList(entities, Entity::getDisplayName); + } ++ ++ // Purpur start ++ private boolean canSee(CommandSourceStack sender, ServerPlayer target) { ++ return !org.purpurmc.purpur.PurpurConfig.hideHiddenPlayersFromEntitySelector || !(sender.getEntity() instanceof ServerPlayer player) || player.getBukkitEntity().canSee(target.getBukkitEntity()); ++ } ++ // Purpur end + } +diff --git a/src/main/java/net/minecraft/commands/execution/tasks/BuildContexts.java b/src/main/java/net/minecraft/commands/execution/tasks/BuildContexts.java +index b0d26b0eadb2a43924629424a6c13198aace8f69..e7cc8105fff9cb952eabfd006e0a4e4638091019 100644 +--- a/src/main/java/net/minecraft/commands/execution/tasks/BuildContexts.java ++++ b/src/main/java/net/minecraft/commands/execution/tasks/BuildContexts.java +@@ -42,7 +42,7 @@ public class BuildContexts> { + ChainModifiers chainModifiers = flags; + List list = sources; + if (contextChain.getStage() != Stage.EXECUTE) { +- context.profiler().push(() -> "prepare " + this.commandInput); ++ //context.profiler().push(() -> "prepare " + this.commandInput); // Purpur + + try { + for (int i = context.forkLimit(); contextChain.getStage() != Stage.EXECUTE; contextChain = contextChain.nextStage()) { +@@ -52,7 +52,7 @@ public class BuildContexts> { + } + + RedirectModifier redirectModifier = commandContext.getRedirectModifier(); +- if (redirectModifier instanceof CustomModifierExecutor customModifierExecutor) { ++ if (redirectModifier instanceof CustomModifierExecutor customModifierExecutor) { // Purpur - decompile error + customModifierExecutor.apply(baseSource, list, contextChain, chainModifiers, ExecutionControl.create(context, frame)); + return; + } +@@ -86,17 +86,17 @@ public class BuildContexts> { + } + } + } finally { +- context.profiler().pop(); ++ //context.profiler().pop(); // Purpur + } + } + + if (list.isEmpty()) { + if (chainModifiers.isReturn()) { +- context.queueNext(new CommandQueueEntry<>(frame, FallthroughTask.instance())); ++ context.queueNext(new CommandQueueEntry<>(frame, (EntryAction) FallthroughTask.instance())); // Purpur - decompile error + } + } else { + CommandContext commandContext2 = contextChain.getTopContext(); +- if (commandContext2.getCommand() instanceof CustomCommandExecutor customCommandExecutor) { ++ if (commandContext2.getCommand() instanceof CustomCommandExecutor customCommandExecutor) { // Purpur - decompile error + ExecutionControl executionControl = ExecutionControl.create(context, frame); + + for (T executionCommandSource2 : list) { +diff --git a/src/main/java/net/minecraft/commands/execution/tasks/ExecuteCommand.java b/src/main/java/net/minecraft/commands/execution/tasks/ExecuteCommand.java +index e9775b4506909bee65a74964f0d5391a0513de1d..684f7f202305c09b1037c5d38a52a5ea7f00751b 100644 +--- a/src/main/java/net/minecraft/commands/execution/tasks/ExecuteCommand.java ++++ b/src/main/java/net/minecraft/commands/execution/tasks/ExecuteCommand.java +@@ -23,7 +23,7 @@ public class ExecuteCommand> implements Unbo + + @Override + public void execute(T executionCommandSource, ExecutionContext executionContext, Frame frame) { +- executionContext.profiler().push(() -> "execute " + this.commandInput); ++ //executionContext.profiler().push(() -> "execute " + this.commandInput); // Purpur + + try { + executionContext.incrementCost(); +@@ -37,7 +37,7 @@ public class ExecuteCommand> implements Unbo + } catch (CommandSyntaxException var9) { + executionCommandSource.handleError(var9, this.modifiers.isForked(), executionContext.tracer()); + } finally { +- executionContext.profiler().pop(); ++ //executionContext.profiler().pop(); // Purpur + } + } + } +diff --git a/src/main/java/net/minecraft/commands/synchronization/ArgumentTypeInfos.java b/src/main/java/net/minecraft/commands/synchronization/ArgumentTypeInfos.java +index 7b118a92a6eb779f800ae8f5d8f6e3c861fc4f6a..057a038e8dcacd7496a0b2373de2c20255a5c297 100644 +--- a/src/main/java/net/minecraft/commands/synchronization/ArgumentTypeInfos.java ++++ b/src/main/java/net/minecraft/commands/synchronization/ArgumentTypeInfos.java +@@ -119,10 +119,10 @@ public class ArgumentTypeInfos { + register(registry, "dimension", DimensionArgument.class, SingletonArgumentInfo.contextFree(DimensionArgument::dimension)); + register(registry, "gamemode", GameModeArgument.class, SingletonArgumentInfo.contextFree(GameModeArgument::gameMode)); + register(registry, "time", TimeArgument.class, new TimeArgument.Info()); +- register(registry, "resource_or_tag", fixClassType(ResourceOrTagArgument.class), new ResourceOrTagArgument.Info()); +- register(registry, "resource_or_tag_key", fixClassType(ResourceOrTagKeyArgument.class), new ResourceOrTagKeyArgument.Info()); +- register(registry, "resource", fixClassType(ResourceArgument.class), new ResourceArgument.Info()); +- register(registry, "resource_key", fixClassType(ResourceKeyArgument.class), new ResourceKeyArgument.Info()); ++ register(registry, "resource_or_tag", fixClassType(ResourceOrTagArgument.class), new ResourceOrTagArgument.Info<>()); // Purpur - decompile error ++ register(registry, "resource_or_tag_key", fixClassType(ResourceOrTagKeyArgument.class), new ResourceOrTagKeyArgument.Info<>()); // Purpur - decompile error ++ register(registry, "resource", fixClassType(ResourceArgument.class), new ResourceArgument.Info<>()); // Purpur - decompile error ++ register(registry, "resource_key", fixClassType(ResourceKeyArgument.class), new ResourceKeyArgument.Info<>()); // Purpur - decompile error + register(registry, "template_mirror", TemplateMirrorArgument.class, SingletonArgumentInfo.contextFree(TemplateMirrorArgument::templateMirror)); + register(registry, "template_rotation", TemplateRotationArgument.class, SingletonArgumentInfo.contextFree(TemplateRotationArgument::templateRotation)); + register(registry, "heightmap", HeightmapTypeArgument.class, SingletonArgumentInfo.contextFree(HeightmapTypeArgument::heightmap)); +diff --git a/src/main/java/net/minecraft/core/BlockPos.java b/src/main/java/net/minecraft/core/BlockPos.java +index 665e88b2dedf9d5bb50914d5f3d377f2d19f40b0..f70a80b496bd1498778e82fc221c3b1b39308b75 100644 +--- a/src/main/java/net/minecraft/core/BlockPos.java ++++ b/src/main/java/net/minecraft/core/BlockPos.java +@@ -61,6 +61,12 @@ public class BlockPos extends Vec3i { + private static final int X_OFFSET = 38; + // Paper end - Optimize Bit Operations by inlining + ++ // Purpur start ++ public BlockPos(net.minecraft.world.entity.Entity entity) { ++ super(entity.getBlockX(), entity.getBlockY(), entity.getBlockZ()); ++ } ++ // Purpur end ++ + public BlockPos(int x, int y, int z) { + super(x, y, z); + } +diff --git a/src/main/java/net/minecraft/core/dispenser/DispenseItemBehavior.java b/src/main/java/net/minecraft/core/dispenser/DispenseItemBehavior.java +index 5dab1e10303177e5a4d97a91ee46ede66f30ae35..68236139e3571791b891dbbef6e3ee20031e16d9 100644 +--- a/src/main/java/net/minecraft/core/dispenser/DispenseItemBehavior.java ++++ b/src/main/java/net/minecraft/core/dispenser/DispenseItemBehavior.java +@@ -1048,5 +1048,22 @@ public interface DispenseItemBehavior { + } + } + }); ++ // Purpur start ++ DispenserBlock.registerBehavior(Items.ANVIL, (new OptionalDispenseItemBehavior() { ++ @Override ++ public ItemStack execute(BlockSource dispenser, ItemStack stack) { ++ net.minecraft.world.level.Level level = dispenser.level(); ++ if (!level.purpurConfig.dispenserPlaceAnvils) return super.execute(dispenser, stack); ++ Direction facing = dispenser.blockEntity().getBlockState().getValue(DispenserBlock.FACING); ++ BlockPos pos = dispenser.pos().relative(facing); ++ BlockState state = level.getBlockState(pos); ++ if (state.isAir()) { ++ level.setBlockAndUpdate(pos, Blocks.ANVIL.defaultBlockState().setValue(net.minecraft.world.level.block.AnvilBlock.FACING, facing.getAxis() == Direction.Axis.Y ? Direction.NORTH : facing.getClockWise())); ++ stack.shrink(1); ++ } ++ return stack; ++ } ++ })); ++ // Purpur end + } + } +diff --git a/src/main/java/net/minecraft/core/dispenser/ShearsDispenseItemBehavior.java b/src/main/java/net/minecraft/core/dispenser/ShearsDispenseItemBehavior.java +index a024c697a65bbab27408da1d6a75e531d9719b47..e4fab82b369f2c2ea0d8c8acd814d06140d551fc 100644 +--- a/src/main/java/net/minecraft/core/dispenser/ShearsDispenseItemBehavior.java ++++ b/src/main/java/net/minecraft/core/dispenser/ShearsDispenseItemBehavior.java +@@ -105,7 +105,7 @@ public class ShearsDispenseItemBehavior extends OptionalDispenseItemBehavior { + if (ishearable.readyForShearing()) { + // CraftBukkit start + // Paper start - Add drops to shear events +- org.bukkit.event.block.BlockShearEntityEvent event = CraftEventFactory.callBlockShearEntityEvent(entityliving, bukkitBlock, craftItem, ishearable.generateDefaultDrops()); ++ org.bukkit.event.block.BlockShearEntityEvent event = CraftEventFactory.callBlockShearEntityEvent(entityliving, bukkitBlock, craftItem, ishearable.generateDefaultDrops(net.minecraft.world.item.enchantment.EnchantmentHelper.getItemEnchantmentLevel(net.minecraft.world.item.enchantment.Enchantments.LOOTING, CraftItemStack.asNMSCopy(craftItem)))); // Purpur + if (event.isCancelled()) { + // Paper end - Add drops to shear events + continue; +diff --git a/src/main/java/net/minecraft/network/Connection.java b/src/main/java/net/minecraft/network/Connection.java +index 58d28b6c1cc7da7d786f78308db971f7502ad844..9f274048be29ed54dd91983447beadf076cf7438 100644 +--- a/src/main/java/net/minecraft/network/Connection.java ++++ b/src/main/java/net/minecraft/network/Connection.java +@@ -607,11 +607,20 @@ public class Connection extends SimpleChannelInboundHandler> { + private static final int MAX_PER_TICK = io.papermc.paper.configuration.GlobalConfiguration.get().misc.maxJoinsPerTick; // Paper - Buffer joins to world + private static int joinAttemptsThisTick; // Paper - Buffer joins to world + private static int currTick; // Paper - Buffer joins to world ++ private static int tickSecond; // Purpur + public void tick() { + this.flushQueue(); + // Paper start - Buffer joins to world + if (Connection.currTick != net.minecraft.server.MinecraftServer.currentTick) { + Connection.currTick = net.minecraft.server.MinecraftServer.currentTick; ++ // Purpur start ++ if (org.purpurmc.purpur.PurpurConfig.maxJoinsPerSecond) { ++ if (++Connection.tickSecond > 20) { ++ Connection.tickSecond = 0; ++ Connection.joinAttemptsThisTick = 0; ++ } ++ } else ++ // Purpur end + Connection.joinAttemptsThisTick = 0; + } + // Paper end - Buffer joins to world +diff --git a/src/main/java/net/minecraft/network/protocol/PacketUtils.java b/src/main/java/net/minecraft/network/protocol/PacketUtils.java +index 57e76b53e5e314c3e6b8856010f7a84188121582..8c134a642ccaf3530022f2e675a858d726e1dda4 100644 +--- a/src/main/java/net/minecraft/network/protocol/PacketUtils.java ++++ b/src/main/java/net/minecraft/network/protocol/PacketUtils.java +@@ -51,7 +51,7 @@ public class PacketUtils { + if (listener instanceof ServerCommonPacketListenerImpl serverCommonPacketListener && serverCommonPacketListener.processedDisconnect) return; // CraftBukkit - Don't handle sync packets for kicked players + if (listener.shouldHandleMessage(packet)) { + co.aikar.timings.Timing timing = co.aikar.timings.MinecraftTimings.getPacketTiming(packet); // Paper - timings +- try (co.aikar.timings.Timing ignored = timing.startTiming()) { // Paper - timings ++ try { // Paper - timings // Purpur + packet.handle(listener); + } catch (Exception exception) { + if (exception instanceof ReportedException) { +diff --git a/src/main/java/net/minecraft/network/protocol/game/ClientboundSetTimePacket.java b/src/main/java/net/minecraft/network/protocol/game/ClientboundSetTimePacket.java +index 76ef195a5074006b009acd9cc1744667c6aecbb9..659577549e132754281df76a7a1bfd884443c56a 100644 +--- a/src/main/java/net/minecraft/network/protocol/game/ClientboundSetTimePacket.java ++++ b/src/main/java/net/minecraft/network/protocol/game/ClientboundSetTimePacket.java +@@ -10,7 +10,7 @@ public class ClientboundSetTimePacket implements Packet processQueue = new java.util.concurrent.ConcurrentLinkedQueue(); + public int autosavePeriod; + public Commands vanillaCommandDispatcher; +@@ -310,11 +311,13 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop { ++ //this.metricsRecorder = InactiveMetricsRecorder.INSTANCE; // Purpur ++ //this.profiler = this.metricsRecorder.getProfiler(); // Purpur ++ /*this.onMetricsRecordingStopped = (methodprofilerresults) -> { // Purpur + this.stopRecordingMetrics(); +- }; +- this.onMetricsRecordingFinished = (path) -> { +- }; ++ };*/ // Purpur ++ //this.onMetricsRecordingFinished = (path) -> { // Purpur ++ //}; // Purpur + this.random = RandomSource.create(); + this.port = -1; + this.levels = Maps.newLinkedHashMap(); +@@ -960,7 +963,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop { + return false; + } : this::haveTime); +- this.profiler.popPush("nextTickWait"); ++ //this.profiler.popPush("nextTickWait"); // Purpur + this.mayHaveDelayedTasks = true; + this.delayedTasksMaxNextTickTimeNanos = Math.max(Util.getNanos() + i, this.nextTickTimeNanos); ++ // Purpur start ++ if (!org.purpurmc.purpur.PurpurConfig.tpsCatchup) { ++ this.nextTickTimeNanos = currentTime + i; ++ this.delayedTasksMaxNextTickTimeNanos = nextTickTimeNanos; ++ } ++ // Purpur end + this.startMeasuringTaskExecutionTime(); + this.waitUntilNextTick(); + this.finishMeasuringTaskExecutionTime(); +@@ -1245,9 +1270,9 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop { + return !this.canOversleep(); + }); +- isOversleep = false;MinecraftTimings.serverOversleep.stopTiming(); ++ //isOversleep = false;MinecraftTimings.serverOversleep.stopTiming(); // Purpur + // Paper end + new com.destroystokyo.paper.event.server.ServerTickStartEvent(this.tickCount+1).callEvent(); // Paper - Server Tick Events + +@@ -1584,7 +1609,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop 0 && this.tickCount % autosavePeriod == 0; + try { + this.isSaving = true; +@@ -1599,20 +1624,20 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop { + entityplayer.connection.suspendFlushing(); + }); +- MinecraftTimings.bukkitSchedulerTimer.startTiming(); // Spigot // Paper ++ //MinecraftTimings.bukkitSchedulerTimer.startTiming(); // Spigot // Paper // Purpur + this.server.getScheduler().mainThreadHeartbeat(this.tickCount); // CraftBukkit +- MinecraftTimings.bukkitSchedulerTimer.stopTiming(); // Spigot // Paper ++ //MinecraftTimings.bukkitSchedulerTimer.stopTiming(); // Spigot // Paper // Purpur + // Paper start - Folia scheduler API + ((io.papermc.paper.threadedregions.scheduler.FoliaGlobalRegionScheduler) Bukkit.getGlobalRegionScheduler()).tick(); + getAllLevels().forEach(level -> { +@@ -1717,22 +1742,22 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop 0; // Paper - Add EntityMoveEvent + net.minecraft.world.level.block.entity.HopperBlockEntity.skipHopperEvents = worldserver.paperConfig().hopper.disableMoveEvent || org.bukkit.event.inventory.InventoryMoveItemEvent.getHandlerList().getRegisteredListeners().length == 0; // Paper - Perf: Optimize Hoppers + worldserver.updateLagCompensationTick(); // Paper - lag compensation ++ worldserver.hasRidableMoveEvent = org.purpurmc.purpur.event.entity.RidableMoveEvent.getHandlerList().getRegisteredListeners().length > 0; // Purpur + +- this.profiler.push(() -> { ++ /*this.profiler.push(() -> { // Purpur + String s = String.valueOf(worldserver); + + return s + " " + String.valueOf(worldserver.dimension().location()); +- }); ++ });*/ // Purpur + /* Drop global time updates + if (this.tickCount % 20 == 0) { +- this.profiler.push("timeSync"); ++ //this.profiler.push("timeSync"); // Purpur + this.synchronizeTime(worldserver); +- this.profiler.pop(); ++ //this.profiler.pop(); // Purpur + } + // CraftBukkit end */ + +- this.profiler.push("tick"); ++ //this.profiler.push("tick"); // Purpur + + try { +- worldserver.timings.doTick.startTiming(); // Spigot ++ //worldserver.timings.doTick.startTiming(); // Spigot // Purpur + worldserver.tick(shouldKeepTicking); + // Paper start + for (final io.papermc.paper.chunk.SingleThreadChunkRegionManager regionManager : worldserver.getChunkSource().chunkMap.regionManagers) { + regionManager.recalculateRegions(); + } + // Paper end +- worldserver.timings.doTick.stopTiming(); // Spigot ++ //worldserver.timings.doTick.stopTiming(); // Spigot // Purpur + } catch (Throwable throwable) { + CrashReport crashreport = CrashReport.forThrowable(throwable, "Exception ticking world"); + +@@ -1794,33 +1820,33 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop // Pufferfish > // Paper + } + + public SystemReport fillSystemReport(SystemReport details) { +@@ -2550,6 +2576,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop { + this.executeBlocking(() -> { + this.saveDebugReport(path.resolve("server")); +@@ -2806,40 +2833,40 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop resultConsumer, Consumer dumpConsumer) { +- this.onMetricsRecordingStopped = (methodprofilerresults) -> { ++ /*this.onMetricsRecordingStopped = (methodprofilerresults) -> { // Purpur + this.stopRecordingMetrics(); + resultConsumer.accept(methodprofilerresults); + }; + this.onMetricsRecordingFinished = dumpConsumer; +- this.willStartRecordingMetrics = true; ++ this.willStartRecordingMetrics = true;*/ // Purpur + } + + public void stopRecordingMetrics() { +- this.metricsRecorder = InactiveMetricsRecorder.INSTANCE; ++ //this.metricsRecorder = InactiveMetricsRecorder.INSTANCE; // Purpur + } + + public void finishRecordingMetrics() { +- this.metricsRecorder.end(); ++ //this.metricsRecorder.end(); // Purpur + } + + public void cancelRecordingMetrics() { +- this.metricsRecorder.cancel(); +- this.profiler = this.metricsRecorder.getProfiler(); ++ //this.metricsRecorder.cancel(); // Purpur ++ //this.profiler = this.metricsRecorder.getProfiler(); // Purpur + } + + public Path getWorldPath(LevelResource worldSavePath) { +@@ -2892,15 +2919,15 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop { + // Paper start - Add Adventure message to PlayerAdvancementDoneEvent + if (event.message() != null && this.player.level().getGameRules().getBoolean(GameRules.RULE_ANNOUNCE_ADVANCEMENTS)) { ++ if (org.purpurmc.purpur.PurpurConfig.advancementOnlyBroadcastToAffectedPlayer) this.player.sendMessage(message); else // Purpur + this.playerList.broadcastSystemMessage(io.papermc.paper.adventure.PaperAdventure.asVanilla(event.message()), false); + // Paper end + } +diff --git a/src/main/java/net/minecraft/server/ServerFunctionManager.java b/src/main/java/net/minecraft/server/ServerFunctionManager.java +index a0ec6c3d122ad28d65d37f1b9f82541997b37d37..775b24a9e55528944b629fd85e1f6ebef9a9892f 100644 +--- a/src/main/java/net/minecraft/server/ServerFunctionManager.java ++++ b/src/main/java/net/minecraft/server/ServerFunctionManager.java +@@ -53,10 +53,10 @@ public class ServerFunctionManager { + } + + private void executeTagFunctions(Collection> functions, ResourceLocation label) { +- ProfilerFiller gameprofilerfiller = this.server.getProfiler(); ++ //ProfilerFiller gameprofilerfiller = this.server.getProfiler(); // Purpur + + Objects.requireNonNull(label); +- gameprofilerfiller.push(label::toString); ++ //gameprofilerfiller.push(label::toString); // Purpur + Iterator iterator = functions.iterator(); + + while (iterator.hasNext()) { +@@ -65,15 +65,15 @@ public class ServerFunctionManager { + this.execute(commandfunction, this.getGameLoopSender()); + } + +- this.server.getProfiler().pop(); ++ //this.server.getProfiler().pop(); // Purpur + } + + public void execute(CommandFunction function, CommandSourceStack source) { +- ProfilerFiller gameprofilerfiller = this.server.getProfiler(); ++ //ProfilerFiller gameprofilerfiller = this.server.getProfiler(); // Purpur + +- gameprofilerfiller.push(() -> { ++ /*gameprofilerfiller.push(() -> { // Purpur + return "function " + String.valueOf(function.id()); +- }); ++ });*/ // Purpur + + try { + InstantiatedFunction instantiatedfunction = function.instantiate((CompoundTag) null, this.getDispatcher()); +@@ -86,7 +86,7 @@ public class ServerFunctionManager { + } catch (Exception exception) { + ServerFunctionManager.LOGGER.warn("Failed to execute function {}", function.id(), exception); + } finally { +- gameprofilerfiller.pop(); ++ //gameprofilerfiller.pop(); // Purpur + } + + } +diff --git a/src/main/java/net/minecraft/server/commands/EnchantCommand.java b/src/main/java/net/minecraft/server/commands/EnchantCommand.java +index 84f1ba6275f04624f46ccd772924b5e075e7b205..bfb455fb74f0a9645212f90acb54f68d1c7d9772 100644 +--- a/src/main/java/net/minecraft/server/commands/EnchantCommand.java ++++ b/src/main/java/net/minecraft/server/commands/EnchantCommand.java +@@ -70,7 +70,7 @@ public class EnchantCommand { + + private static int enchant(CommandSourceStack source, Collection targets, Holder enchantment, int level) throws CommandSyntaxException { + Enchantment enchantment2 = enchantment.value(); +- if (level > enchantment2.getMaxLevel()) { ++ if (!org.purpurmc.purpur.PurpurConfig.allowUnsafeEnchantCommand && level > enchantment2.getMaxLevel()) { // Purpur + throw ERROR_LEVEL_TOO_HIGH.create(level, enchantment2.getMaxLevel()); + } else { + int i = 0; +@@ -81,7 +81,7 @@ public class EnchantCommand { + ItemStack itemStack = livingEntity.getMainHandItem(); + if (!itemStack.isEmpty()) { + if (enchantment2.canEnchant(itemStack) +- && EnchantmentHelper.isEnchantmentCompatible(EnchantmentHelper.getEnchantmentsForCrafting(itemStack).keySet(), enchantment2)) { ++ && EnchantmentHelper.isEnchantmentCompatible(EnchantmentHelper.getEnchantmentsForCrafting(itemStack).keySet(), enchantment2) || (org.purpurmc.purpur.PurpurConfig.allowUnsafeEnchantCommand && !itemStack.hasEnchantment(enchantment2))) { // Purpur + itemStack.enchant(enchantment2, level); + i++; + } else if (targets.size() == 1) { +diff --git a/src/main/java/net/minecraft/server/commands/GameModeCommand.java b/src/main/java/net/minecraft/server/commands/GameModeCommand.java +index d1da3600dc07107309b20ebe6e7c0c4da0e8de76..244b4719c689f153fa36381a60acc280bb0bd9b3 100644 +--- a/src/main/java/net/minecraft/server/commands/GameModeCommand.java ++++ b/src/main/java/net/minecraft/server/commands/GameModeCommand.java +@@ -57,6 +57,18 @@ public class GameModeCommand { + } + + private static int setMode(CommandContext context, Collection targets, GameType gameMode) { ++ // Purpur start ++ if (org.purpurmc.purpur.PurpurConfig.commandGamemodeRequiresPermission) { ++ String gamemode = gameMode.getName(); ++ CommandSourceStack sender = context.getSource(); ++ if (!sender.testPermission(2, "minecraft.command.gamemode." + gamemode)) { ++ return 0; ++ } ++ if (sender.getEntity() instanceof ServerPlayer player && (targets.size() > 1 || !targets.contains(player)) && !sender.testPermission(2, "minecraft.command.gamemode." + gamemode + ".other")) { ++ return 0; ++ } ++ } ++ // Purpur end + int i = 0; + + for (ServerPlayer serverPlayer : targets) { +diff --git a/src/main/java/net/minecraft/server/commands/GiveCommand.java b/src/main/java/net/minecraft/server/commands/GiveCommand.java +index 47355158e5e762540a10dc67b23092a0fc53bce3..9f1c8a62bda242781a0966fa2fc01534261423c7 100644 +--- a/src/main/java/net/minecraft/server/commands/GiveCommand.java ++++ b/src/main/java/net/minecraft/server/commands/GiveCommand.java +@@ -92,6 +92,7 @@ public class GiveCommand { + boolean flag = entityplayer.getInventory().add(itemstack1); + ItemEntity entityitem; + ++ if (org.purpurmc.purpur.PurpurConfig.disableGiveCommandDrops) continue; // Purpur - add config option for toggling give command dropping + if (flag && itemstack1.isEmpty()) { + entityitem = entityplayer.drop(itemstack, false, false, false); // CraftBukkit - SPIGOT-2942: Add boolean to call event + if (entityitem != null) { +diff --git a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java +index eb4fc900164d1fb3a78653ae8bc42ea30323f5b7..775c5de4f5094260096cef6723dd50dfe2cb0c81 100644 +--- a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java ++++ b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java +@@ -110,6 +110,7 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface + return; + } + // Paper start - Use TerminalConsoleAppender ++ if (DedicatedServer.this.gui == null || System.console() != null) // Purpur - has no GUI or has console (did not double-click) + new com.destroystokyo.paper.console.PaperConsole(DedicatedServer.this).start(); + /* + jline.console.ConsoleReader bufferedreader = DedicatedServer.this.reader; +@@ -232,6 +233,15 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface + thread.start(); // Paper - Enhance console tab completions for brigadier commands; start console thread after MinecraftServer.console & PaperConfig are initialized + io.papermc.paper.command.PaperCommands.registerCommands(this); // Paper - setup /paper command + com.destroystokyo.paper.Metrics.PaperMetrics.startMetrics(); // Paper - start metrics ++ // Purpur start ++ try { ++ org.purpurmc.purpur.PurpurConfig.init((java.io.File) options.valueOf("purpur-settings")); ++ } catch (Exception e) { ++ DedicatedServer.LOGGER.error("Unable to load server configuration", e); ++ return false; ++ } ++ org.purpurmc.purpur.PurpurConfig.registerCommands(); ++ // Purpur end + com.destroystokyo.paper.VersionHistoryManager.INSTANCE.getClass(); // Paper - load version history now + io.papermc.paper.brigadier.PaperBrigadierProviderImpl.INSTANCE.getClass(); // Paper - init PaperBrigadierProvider + +@@ -281,6 +291,30 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface + if (true) throw new IllegalStateException("Failed to bind to port", ioexception); // Paper - Propagate failed to bind to port error + return false; + } ++ // Purpur start ++ if (org.purpurmc.purpur.PurpurConfig.useUPnP) { ++ LOGGER.info("[UPnP] Attempting to start UPnP port forwarding service..."); ++ if (dev.omega24.upnp4j.UPnP4J.isUPnPAvailable()) { ++ if (dev.omega24.upnp4j.UPnP4J.isOpen(this.getPort(), dev.omega24.upnp4j.util.Protocol.TCP)) { ++ this.upnp = false; ++ LOGGER.info("[UPnP] Port {} is already open", this.getPort()); ++ } else if (dev.omega24.upnp4j.UPnP4J.open(this.getPort(), dev.omega24.upnp4j.util.Protocol.TCP)) { ++ this.upnp = true; ++ LOGGER.info("[UPnP] Successfully opened port {}", this.getPort()); ++ } else { ++ this.upnp = false; ++ LOGGER.info("[UPnP] Failed to open port {}", this.getPort()); ++ } ++ ++ if (upnp) { ++ LOGGER.info("[UPnP] {}:{}", dev.omega24.upnp4j.UPnP4J.getExternalIP(), this.getPort()); ++ } ++ } else { ++ this.upnp = false; ++ LOGGER.error("[UPnP] Service is unavailable"); ++ } ++ } ++ // Purpur end + + // CraftBukkit start + // this.setPlayerList(new DedicatedPlayerList(this, this.registries(), this.playerDataStorage)); // Spigot - moved up +@@ -354,6 +388,8 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface + DedicatedServer.LOGGER.info("JMX monitoring enabled"); + } + ++ org.purpurmc.purpur.task.BossBarTask.startAll(); // Purpur ++ if (org.purpurmc.purpur.PurpurConfig.beeCountPayload) org.purpurmc.purpur.task.BeehiveTask.instance().register(); // Purpur + return true; + } + } +@@ -506,7 +542,7 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface + } + + public void handleConsoleInputs() { +- MinecraftTimings.serverCommandTimer.startTiming(); // Spigot ++ //MinecraftTimings.serverCommandTimer.startTiming(); // Spigot // Purpur + // Paper start - Perf: use proper queue + ConsoleInput servercommand; + while ((servercommand = this.serverCommandQueue.poll()) != null) { +@@ -523,7 +559,7 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface + // CraftBukkit end + } + +- MinecraftTimings.serverCommandTimer.stopTiming(); // Spigot ++ //MinecraftTimings.serverCommandTimer.stopTiming(); // Spigot // Purpur + } + + @Override +diff --git a/src/main/java/net/minecraft/server/dedicated/DedicatedServerProperties.java b/src/main/java/net/minecraft/server/dedicated/DedicatedServerProperties.java +index 9d10cdacb3aed2c00dc60aeb6f2cbeb48905e21f..842f382de43df5d5c321422372ec30ccdd7859d7 100644 +--- a/src/main/java/net/minecraft/server/dedicated/DedicatedServerProperties.java ++++ b/src/main/java/net/minecraft/server/dedicated/DedicatedServerProperties.java +@@ -56,6 +56,7 @@ public class DedicatedServerProperties extends Settings finalizers = Lists.newArrayList(); + final AtomicBoolean isClosing = new AtomicBoolean(); ++ // Purpur start ++ private final CommandHistory history = new CommandHistory(); ++ private String currentCommand = ""; ++ private int historyIndex = 0; ++ // Purpur end + + public static MinecraftServerGui showFrameFor(final DedicatedServer server) { + try { +@@ -51,7 +56,7 @@ public class MinecraftServerGui extends JComponent { + ; + } + +- final JFrame jframe = new JFrame("Minecraft server"); ++ final JFrame jframe = new JFrame("Purpur Minecraft server"); // Purpur + final MinecraftServerGui servergui = new MinecraftServerGui(server); + + jframe.setDefaultCloseOperation(2); +@@ -59,7 +64,7 @@ public class MinecraftServerGui extends JComponent { + jframe.pack(); + jframe.setLocationRelativeTo((Component) null); + jframe.setVisible(true); +- jframe.setName("Minecraft server"); // Paper - Improve ServerGUI ++ jframe.setName("Purpur Minecraft server"); // Paper - Improve ServerGUI // Purpur + + // Paper start - Improve ServerGUI + try { +@@ -71,7 +76,7 @@ public class MinecraftServerGui extends JComponent { + jframe.addWindowListener(new WindowAdapter() { + public void windowClosing(WindowEvent windowevent) { + if (!servergui.isClosing.getAndSet(true)) { +- jframe.setTitle("Minecraft server - shutting down!"); ++ jframe.setTitle("Purpur Minecraft server - shutting down!"); // Purpur + server.halt(true); + servergui.runFinalizers(); + } +@@ -159,7 +164,7 @@ public class MinecraftServerGui extends JComponent { + + private JComponent buildChatPanel() { + JPanel jpanel = new JPanel(new BorderLayout()); +- JTextArea jtextarea = new JTextArea(); ++ org.purpurmc.purpur.gui.JColorTextPane jtextarea = new org.purpurmc.purpur.gui.JColorTextPane(); // Purpur + JScrollPane jscrollpane = new JScrollPane(jtextarea, 22, 30); + + jtextarea.setEditable(false); +@@ -171,10 +176,43 @@ public class MinecraftServerGui extends JComponent { + + if (!s.isEmpty()) { + this.server.handleConsoleInput(s, this.server.createCommandSourceStack()); ++ // Purpur start ++ history.add(s); ++ historyIndex = -1; ++ // Purpur end + } + + jtextfield.setText(""); + }); ++ // Purpur start ++ jtextfield.getInputMap().put(javax.swing.KeyStroke.getKeyStroke("UP"), "up"); ++ jtextfield.getInputMap().put(javax.swing.KeyStroke.getKeyStroke("DOWN"), "down"); ++ jtextfield.getActionMap().put("up", new javax.swing.AbstractAction() { ++ @Override ++ public void actionPerformed(java.awt.event.ActionEvent actionEvent) { ++ if (historyIndex < 0) { ++ currentCommand = jtextfield.getText(); ++ } ++ if (historyIndex < history.size() - 1) { ++ jtextfield.setText(history.get(++historyIndex)); ++ } ++ } ++ }); ++ jtextfield.getActionMap().put("down", new javax.swing.AbstractAction() { ++ @Override ++ public void actionPerformed(java.awt.event.ActionEvent actionEvent) { ++ if (historyIndex >= 0) { ++ if (historyIndex == 0) { ++ --historyIndex; ++ jtextfield.setText(currentCommand); ++ } else { ++ --historyIndex; ++ jtextfield.setText(history.get(historyIndex)); ++ } ++ } ++ } ++ }); ++ // Purpur end + jtextarea.addFocusListener(new FocusAdapter() { // CraftBukkit - decompile error + public void focusGained(FocusEvent focusevent) {} + }); +@@ -210,7 +248,7 @@ public class MinecraftServerGui extends JComponent { + } + + private static final java.util.regex.Pattern ANSI = java.util.regex.Pattern.compile("\\e\\[[\\d;]*[^\\d;]"); // CraftBukkit // Paper +- public void print(JTextArea textArea, JScrollPane scrollPane, String message) { ++ public void print(org.purpurmc.purpur.gui.JColorTextPane textArea, JScrollPane scrollPane, String message) { // Purpur + if (!SwingUtilities.isEventDispatchThread()) { + SwingUtilities.invokeLater(() -> { + this.print(textArea, scrollPane, message); +@@ -224,11 +262,14 @@ public class MinecraftServerGui extends JComponent { + flag = (double) jscrollbar.getValue() + jscrollbar.getSize().getHeight() + (double) (MinecraftServerGui.MONOSPACED.getSize() * 4) > (double) jscrollbar.getMaximum(); + } + ++ /* // Purpur + try { + document.insertString(document.getLength(), MinecraftServerGui.ANSI.matcher(message).replaceAll(""), (AttributeSet) null); // CraftBukkit + } catch (BadLocationException badlocationexception) { + ; + } ++ */ // Purpur ++ textArea.append(message); // Purpur + + if (flag) { + jscrollbar.setValue(Integer.MAX_VALUE); +@@ -236,4 +277,16 @@ public class MinecraftServerGui extends JComponent { + + } + } ++ ++ // Purpur start ++ public static class CommandHistory extends java.util.LinkedList { ++ @Override ++ public boolean add(String command) { ++ if (size() > 1000) { ++ remove(); ++ } ++ return super.offerFirst(command); ++ } ++ } ++ // Purpur end + } +diff --git a/src/main/java/net/minecraft/server/gui/StatsComponent.java b/src/main/java/net/minecraft/server/gui/StatsComponent.java +index 096c89bd01cec2abd151bf6fffc4847d1bcd548f..cd0a8a6a1be75cab8bbb8ee3ac17bb732b9e7108 100644 +--- a/src/main/java/net/minecraft/server/gui/StatsComponent.java ++++ b/src/main/java/net/minecraft/server/gui/StatsComponent.java +@@ -45,7 +45,7 @@ public class StatsComponent extends JComponent { + this.msgs[1] = "Avg tick: " + + DECIMAL_FORMAT.format((double)this.server.getAverageTickTimeNanos() / (double)TimeUtil.NANOSECONDS_PER_MILLISECOND) + + " ms"; +- this.msgs[2] = "TPS from last 1m, 5m, 15m: " + String.join(", ", tpsAvg); ++ this.msgs[2] = "TPS from last 5s, 1m, 5m, 15m: " + String.join(", ", tpsAvg); // Purpur + // Paper end - Improve ServerGUI + this.values[this.vp++ & 0xFF] = (int)(l * 100L / Runtime.getRuntime().maxMemory()); + this.repaint(); +diff --git a/src/main/java/net/minecraft/server/level/ChunkMap.java b/src/main/java/net/minecraft/server/level/ChunkMap.java +index dbe9df1e1973db133f7c8516256697ef7c968137..4e6fccec4f5ca14562bf5bae495ac36c14982d85 100644 +--- a/src/main/java/net/minecraft/server/level/ChunkMap.java ++++ b/src/main/java/net/minecraft/server/level/ChunkMap.java +@@ -541,20 +541,20 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + } + + protected void tick(BooleanSupplier shouldKeepTicking) { +- ProfilerFiller gameprofilerfiller = this.level.getProfiler(); ++ //ProfilerFiller gameprofilerfiller = this.level.getProfiler(); // Purpur + +- try (Timing ignored = this.level.timings.poiUnload.startTiming()) { // Paper +- gameprofilerfiller.push("poi"); ++ //try (Timing ignored = this.level.timings.poiUnload.startTiming()) { // Paper // Purpur ++ //gameprofilerfiller.push("poi"); // Purpur + this.poiManager.tick(shouldKeepTicking); +- } // Paper +- gameprofilerfiller.popPush("chunk_unload"); ++ //} // Paper // Purpur ++ //gameprofilerfiller.popPush("chunk_unload"); // Purpur + if (!this.level.noSave()) { +- try (Timing ignored = this.level.timings.chunkUnload.startTiming()) { // Paper ++ //try (Timing ignored = this.level.timings.chunkUnload.startTiming()) { // Paper // Purpur + this.processUnloads(shouldKeepTicking); +- } // Paper ++ //} // Paper // Purpur + } + +- gameprofilerfiller.pop(); ++ //gameprofilerfiller.pop(); // Purpur + } + + public boolean hasWork() { +@@ -1157,24 +1157,24 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + + // Paper start - optimised tracker + private final void processTrackQueue() { +- this.level.timings.tracker1.startTiming(); ++ //this.level.timings.tracker1.startTiming(); // Purpur + try { + for (TrackedEntity tracker : this.entityMap.values()) { + // update tracker entry + tracker.updatePlayers(tracker.entity.getPlayersInTrackRange()); + } + } finally { +- this.level.timings.tracker1.stopTiming(); ++ //this.level.timings.tracker1.stopTiming(); // Purpur + } + + +- this.level.timings.tracker2.startTiming(); ++ //this.level.timings.tracker2.startTiming(); // Purpur + try { + for (TrackedEntity tracker : this.entityMap.values()) { + tracker.serverEntity.sendChanges(); + } + } finally { +- this.level.timings.tracker2.stopTiming(); ++ //this.level.timings.tracker2.stopTiming(); // Purpur + } + } + // Paper end - optimised tracker +@@ -1189,7 +1189,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + List list = Lists.newArrayList(); + List list1 = this.level.players(); + ObjectIterator objectiterator = this.entityMap.values().iterator(); +- level.timings.tracker1.startTiming(); // Paper ++ //this.level.timings.tracker1.startTiming(); // Paper // Purpur + + ChunkMap.TrackedEntity playerchunkmap_entitytracker; + +@@ -1214,17 +1214,17 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + playerchunkmap_entitytracker.serverEntity.sendChanges(); + } + } +- level.timings.tracker1.stopTiming(); // Paper ++ //this.level.timings.tracker1.stopTiming(); // Paper // Purpur + + if (!list.isEmpty()) { + objectiterator = this.entityMap.values().iterator(); + +- level.timings.tracker2.startTiming(); // Paper ++ //this.level.timings.tracker2.startTiming(); // Paper // Purpur + while (objectiterator.hasNext()) { + playerchunkmap_entitytracker = (ChunkMap.TrackedEntity) objectiterator.next(); + playerchunkmap_entitytracker.updatePlayers(list); + } +- level.timings.tracker2.stopTiming(); // Paper ++ //this.level.timings.tracker2.stopTiming(); // Paper // Purpur + } + + } +diff --git a/src/main/java/net/minecraft/server/level/ServerChunkCache.java b/src/main/java/net/minecraft/server/level/ServerChunkCache.java +index b99f50604bafecbc68835974c9ed0caa91911a40..476a04d87a61b021816d2970e86042bde32d95a2 100644 +--- a/src/main/java/net/minecraft/server/level/ServerChunkCache.java ++++ b/src/main/java/net/minecraft/server/level/ServerChunkCache.java +@@ -259,14 +259,14 @@ public class ServerChunkCache extends ChunkSource { + return ifLoaded; + } + // Paper end - Perf: Optimise getChunkAt calls for loaded chunks +- ProfilerFiller gameprofilerfiller = this.level.getProfiler(); ++ //ProfilerFiller gameprofilerfiller = this.level.getProfiler(); // Purpur + +- gameprofilerfiller.incrementCounter("getChunk"); ++ //gameprofilerfiller.incrementCounter("getChunk"); // Purpur + long k = ChunkPos.asLong(x, z); + + // Paper - rewrite chunk system - there are no correct callbacks to remove items from cache in the new chunk system + +- gameprofilerfiller.incrementCounter("getChunkCacheMiss"); ++ //gameprofilerfiller.incrementCounter("getChunkCacheMiss"); // Purpur + CompletableFuture> completablefuture = this.getChunkFutureMainThread(x, z, leastStatus, create); + ServerChunkCache.MainThreadExecutor chunkproviderserver_b = this.mainThreadProcessor; + +@@ -274,10 +274,10 @@ public class ServerChunkCache extends ChunkSource { + if (!completablefuture.isDone()) { // Paper + io.papermc.paper.chunk.system.scheduling.ChunkTaskScheduler.pushChunkWait(this.level, x1, z1); // Paper - rewrite chunk system + com.destroystokyo.paper.io.SyncLoadFinder.logSyncLoad(this.level, x, z); // Paper - Add debug for sync chunk loads +- this.level.timings.syncChunkLoad.startTiming(); // Paper ++ //this.level.timings.syncChunkLoad.startTiming(); // Paper // Purpur + chunkproviderserver_b.managedBlock(completablefuture::isDone); + io.papermc.paper.chunk.system.scheduling.ChunkTaskScheduler.popChunkWait(); // Paper - rewrite chunk system +- this.level.timings.syncChunkLoad.stopTiming(); // Paper ++ //this.level.timings.syncChunkLoad.stopTiming(); // Paper // Purpur + } // Paper + ChunkResult chunkresult = (ChunkResult) completablefuture.join(); + ChunkAccess ichunkaccess1 = (ChunkAccess) chunkresult.orElse(null); // CraftBukkit - decompile error +@@ -425,17 +425,17 @@ public class ServerChunkCache extends ChunkSource { + + public void save(boolean flush) { + this.runDistanceManagerUpdates(); +- try (co.aikar.timings.Timing timed = level.timings.chunkSaveData.startTiming()) { // Paper - Timings ++ //try (co.aikar.timings.Timing timed = level.timings.chunkSaveData.startTiming()) { // Paper - Timings // Purpur + this.chunkMap.saveAllChunks(flush); +- } // Paper - Timings ++ //} // Paper - Timings // Purpur + } + + // Paper start - Incremental chunk and player saving; duplicate save, but call incremental + public void saveIncrementally() { + this.runDistanceManagerUpdates(); +- try (co.aikar.timings.Timing timed = level.timings.chunkSaveData.startTiming()) { // Paper - Timings ++ //try (co.aikar.timings.Timing timed = level.timings.chunkSaveData.startTiming()) { // Paper - Timings // Purpur + this.chunkMap.saveIncrementally(); +- } // Paper - Timings ++ //} // Paper - Timings // Purpur + } + // Paper end - Incremental chunk and player saving + +@@ -459,40 +459,39 @@ public class ServerChunkCache extends ChunkSource { + // CraftBukkit start - modelled on below + public void purgeUnload() { + if (true) return; // Paper - tickets will be removed later, this behavior isn't really well accounted for by the chunk system +- this.level.getProfiler().push("purge"); ++ //this.level.getProfiler().push("purge"); // Purpur + this.distanceManager.purgeStaleTickets(); + this.runDistanceManagerUpdates(); +- this.level.getProfiler().popPush("unload"); ++ //this.level.getProfiler().popPush("unload"); // Purpur + this.chunkMap.tick(() -> true); +- this.level.getProfiler().pop(); ++ //this.level.getProfiler().pop(); // Purpur + this.clearCache(); + } + // CraftBukkit end + + @Override + public void tick(BooleanSupplier shouldKeepTicking, boolean tickChunks) { +- this.level.getProfiler().push("purge"); +- this.level.timings.doChunkMap.startTiming(); // Spigot ++ //this.level.getProfiler().push("purge"); // Purpur ++ //this.level.timings.doChunkMap.startTiming(); // Spigot // Purpur + if (this.level.tickRateManager().runsNormally() || !tickChunks) { + this.distanceManager.purgeStaleTickets(); + } +- + this.runDistanceManagerUpdates(); +- this.level.timings.doChunkMap.stopTiming(); // Spigot +- this.level.getProfiler().popPush("chunks"); ++ //this.level.timings.doChunkMap.stopTiming(); // Spigot // Purpur ++ //this.level.getProfiler().popPush("chunks"); // Purpur + if (tickChunks) { +- this.level.timings.chunks.startTiming(); // Paper - timings ++ //this.level.timings.chunks.startTiming(); // Paper - timings // Purpur + this.chunkMap.level.playerChunkLoader.tick(); // Paper - replace player chunk loader - this is mostly required to account for view distance changes + this.tickChunks(); +- this.level.timings.chunks.stopTiming(); // Paper - timings ++ //this.level.timings.chunks.stopTiming(); // Paper - timings // Purpur + this.chunkMap.tick(); + } + +- this.level.timings.doChunkUnload.startTiming(); // Spigot +- this.level.getProfiler().popPush("unload"); ++ //this.level.timings.doChunkUnload.startTiming(); // Spigot // Purpur ++ //this.level.getProfiler().popPush("unload"); // Purpur + this.chunkMap.tick(shouldKeepTicking); +- this.level.timings.doChunkUnload.stopTiming(); // Spigot +- this.level.getProfiler().pop(); ++ //this.level.timings.doChunkUnload.stopTiming(); // Spigot // Purpur ++ //this.level.getProfiler().pop(); // Purpur + this.clearCache(); + } + +@@ -502,18 +501,18 @@ public class ServerChunkCache extends ChunkSource { + + this.lastInhabitedUpdate = i; + if (!this.level.isDebug()) { +- ProfilerFiller gameprofilerfiller = this.level.getProfiler(); ++ //ProfilerFiller gameprofilerfiller = this.level.getProfiler(); // Purpur + +- gameprofilerfiller.push("pollingChunks"); +- gameprofilerfiller.push("filteringLoadedChunks"); ++ //gameprofilerfiller.push("pollingChunks"); // Purpur ++ //gameprofilerfiller.push("filteringLoadedChunks"); // Purpur + // Paper - optimise chunk tick iteration +- if (this.level.getServer().tickRateManager().runsNormally()) this.level.timings.chunkTicks.startTiming(); // Paper ++ //if (this.level.getServer().tickRateManager().runsNormally()) this.level.timings.chunkTicks.startTiming(); // Paper // Purpur + + // Paper - optimise chunk tick iteration + + if (this.level.tickRateManager().runsNormally()) { +- gameprofilerfiller.popPush("naturalSpawnCount"); +- this.level.timings.countNaturalMobs.startTiming(); // Paper - timings ++ //gameprofilerfiller.popPush("naturalSpawnCount"); // Purpur ++ //this.level.timings.countNaturalMobs.startTiming(); // Paper - timings // Purpur + int k = this.distanceManager.getNaturalSpawnChunkCount(); + // Paper start - Optional per player mob spawns + int naturalSpawnChunkCount = k; +@@ -538,10 +537,10 @@ public class ServerChunkCache extends ChunkSource { + spawnercreature_d = NaturalSpawner.createState(naturalSpawnChunkCount, this.level.getAllEntities(), this::getFullChunk, !this.level.paperConfig().entities.spawning.perPlayerMobSpawns ? new LocalMobCapCalculator(this.chunkMap) : null, false); + } + // Paper end - Optional per player mob spawns +- this.level.timings.countNaturalMobs.stopTiming(); // Paper - timings ++ // this.level.timings.countNaturalMobs.stopTiming(); // Paper - timings // Purpur + + this.lastSpawnState = spawnercreature_d; +- gameprofilerfiller.popPush("spawnAndTick"); ++ //gameprofilerfiller.popPush("spawnAndTick"); // Purpur + boolean flag = this.level.getGameRules().getBoolean(GameRules.RULE_DOMOBSPAWNING) && !this.level.players().isEmpty(); // CraftBukkit + + // Paper start - optimise chunk tick iteration +@@ -647,19 +646,19 @@ public class ServerChunkCache extends ChunkSource { + } + } + // Paper end - optimise chunk tick iteration +- this.level.timings.chunkTicks.stopTiming(); // Paper ++ // this.level.timings.chunkTicks.stopTiming(); // Paper // Purpur + +- gameprofilerfiller.popPush("customSpawners"); ++ //gameprofilerfiller.popPush("customSpawners"); // Purpur + if (flag) { +- try (co.aikar.timings.Timing ignored = this.level.timings.miscMobSpawning.startTiming()) { // Paper - timings ++ //try (co.aikar.timings.Timing ignored = this.level.timings.miscMobSpawning.startTiming()) { // Paper - timings // Purpur + this.level.tickCustomSpawners(this.spawnEnemies, this.spawnFriendlies); +- } // Paper - timings ++ //} // Paper - timings // Purpur + } + } + +- gameprofilerfiller.popPush("broadcast"); ++ //gameprofilerfiller.popPush("broadcast"); // Purpur + // Paper - optimise chunk tick iteration +- this.level.timings.broadcastChunkUpdates.startTiming(); // Paper - timing ++ //this.level.timings.broadcastChunkUpdates.startTiming(); // Paper - timing // Purpur + // Paper start - optimise chunk tick iteration + if (!this.chunkMap.needsChangeBroadcasting.isEmpty()) { + it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet copy = this.chunkMap.needsChangeBroadcasting.clone(); +@@ -673,10 +672,10 @@ public class ServerChunkCache extends ChunkSource { + } + } + // Paper end - optimise chunk tick iteration +- this.level.timings.broadcastChunkUpdates.stopTiming(); // Paper - timing ++ //this.level.timings.broadcastChunkUpdates.stopTiming(); // Paper - timing // Purpur + // Paper - optimise chunk tick iteration +- gameprofilerfiller.pop(); +- gameprofilerfiller.pop(); ++ //gameprofilerfiller.pop(); // Purpur ++ //gameprofilerfiller.pop(); // Purpur + } + } + +@@ -848,7 +847,7 @@ public class ServerChunkCache extends ChunkSource { + + @Override + protected void doRunTask(Runnable task) { +- ServerChunkCache.this.level.getProfiler().incrementCounter("runTask"); ++ //ServerChunkCache.this.level.getProfiler().incrementCounter("runTask"); // Purpur + super.doRunTask(task); + } + +diff --git a/src/main/java/net/minecraft/server/level/ServerEntity.java b/src/main/java/net/minecraft/server/level/ServerEntity.java +index 4f103f731623a8570238a6867fda1c5f83fca4e4..39e7dcf3c92c9203c190782be401c00c010b8aeb 100644 +--- a/src/main/java/net/minecraft/server/level/ServerEntity.java ++++ b/src/main/java/net/minecraft/server/level/ServerEntity.java +@@ -77,7 +77,7 @@ public class ServerEntity { + @Nullable + private List> trackedDataValues; + // CraftBukkit start +- private final Set trackedPlayers; ++ public final Set trackedPlayers; // Purpur - private -> public + + public ServerEntity(ServerLevel worldserver, Entity entity, int i, boolean flag, Consumer> consumer, Set trackedPlayers) { + this.trackedPlayers = trackedPlayers; +diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java +index 0981d440d0dbfe4df668d1f3f1b5706a93bc4434..f72af2feb74626abbdfbfd090c15357457810240 100644 +--- a/src/main/java/net/minecraft/server/level/ServerLevel.java ++++ b/src/main/java/net/minecraft/server/level/ServerLevel.java +@@ -220,6 +220,8 @@ public class ServerLevel extends Level implements WorldGenLevel { + private final StructureManager structureManager; + private final StructureCheck structureCheck; + private final boolean tickTime; ++ private double preciseTime; // Purpur ++ private boolean forceTime; // Purpur + private final RandomSequences randomSequences; + public long lastMidTickExecuteFailure; // Paper - execute chunk tasks mid tick + +@@ -229,6 +231,7 @@ public class ServerLevel extends Level implements WorldGenLevel { + public boolean hasPhysicsEvent = true; // Paper - BlockPhysicsEvent + public boolean hasEntityMoveEvent; // Paper - Add EntityMoveEvent + private final alternate.current.wire.WireHandler wireHandler = new alternate.current.wire.WireHandler(this); // Paper - optimize redstone (Alternate Current) ++ public boolean hasRidableMoveEvent = false; // Purpur + + public LevelChunk getChunkIfLoaded(int x, int z) { + return this.chunkSource.getChunkAtIfLoadedImmediately(x, z); // Paper - Use getChunkIfLoadedImmediately +@@ -713,7 +716,24 @@ public class ServerLevel extends Level implements WorldGenLevel { + this.dragonParts = new Int2ObjectOpenHashMap(); + this.tickTime = flag1; + this.server = minecraftserver; +- this.customSpawners = list; ++ // Purpur start - enable/disable MobSpawners per world ++ this.customSpawners = Lists.newArrayList(); ++ if (purpurConfig.phantomSpawning) { ++ customSpawners.add(new net.minecraft.world.level.levelgen.PhantomSpawner()); ++ } ++ if (purpurConfig.patrolSpawning) { ++ customSpawners.add(new net.minecraft.world.level.levelgen.PatrolSpawner()); ++ } ++ if (purpurConfig.catSpawning) { ++ customSpawners.add(new net.minecraft.world.entity.npc.CatSpawner()); ++ } ++ if (purpurConfig.villageSiegeSpawning) { ++ customSpawners.add(new net.minecraft.world.entity.ai.village.VillageSiege()); ++ } ++ if (purpurConfig.villagerTraderSpawning) { ++ customSpawners.add(new net.minecraft.world.entity.npc.WanderingTraderSpawner(iworlddataserver)); ++ } ++ // Purpur end + this.serverLevelData = iworlddataserver; + ChunkGenerator chunkgenerator = worlddimension.generator(); + // CraftBukkit start +@@ -775,6 +795,7 @@ public class ServerLevel extends Level implements WorldGenLevel { + + this.chunkTaskScheduler = new io.papermc.paper.chunk.system.scheduling.ChunkTaskScheduler(this, io.papermc.paper.chunk.system.scheduling.ChunkTaskScheduler.workerThreads); // Paper - rewrite chunk system + this.entityLookup = new io.papermc.paper.chunk.system.entity.EntityLookup(this, new EntityCallbacks()); // Paper - rewrite chunk system ++ this.preciseTime = this.serverLevelData.getDayTime(); // Purpur + } + + // Paper start +@@ -809,23 +830,23 @@ public class ServerLevel extends Level implements WorldGenLevel { + } + + public void tick(BooleanSupplier shouldKeepTicking) { +- ProfilerFiller gameprofilerfiller = this.getProfiler(); ++ //ProfilerFiller gameprofilerfiller = this.getProfiler(); // Purpur + + this.handlingTick = true; + TickRateManager tickratemanager = this.tickRateManager(); + boolean flag = tickratemanager.runsNormally(); + + if (flag) { +- gameprofilerfiller.push("world border"); ++ //gameprofilerfiller.push("world border"); // Purpur + this.getWorldBorder().tick(); +- gameprofilerfiller.popPush("weather"); ++ //gameprofilerfiller.popPush("weather"); // Purpur + this.advanceWeatherCycle(); + } + + int i = this.getGameRules().getInt(GameRules.RULE_PLAYERS_SLEEPING_PERCENTAGE); + long j; + +- if (this.sleepStatus.areEnoughSleeping(i) && this.sleepStatus.areEnoughDeepSleeping(i, this.players)) { ++ if (this.purpurConfig.playersSkipNight && this.sleepStatus.areEnoughSleeping(i) && this.sleepStatus.areEnoughDeepSleeping(i, this.players)) { + // CraftBukkit start + j = this.levelData.getDayTime() + 24000L; + TimeSkipEvent event = new TimeSkipEvent(this.getWorld(), TimeSkipEvent.SkipReason.NIGHT_SKIP, (j - j % 24000L) - this.getDayTime()); +@@ -850,38 +871,38 @@ public class ServerLevel extends Level implements WorldGenLevel { + this.tickTime(); + } + +- gameprofilerfiller.popPush("tickPending"); +- this.timings.scheduledBlocks.startTiming(); // Paper ++ //gameprofilerfiller.popPush("tickPending"); // Purpur ++ //this.timings.scheduledBlocks.startTiming(); // Paper // Purpur + if (!this.isDebug() && flag) { + j = this.getGameTime(); +- gameprofilerfiller.push("blockTicks"); ++ //gameprofilerfiller.push("blockTicks"); // Purpur + this.blockTicks.tick(j, paperConfig().environment.maxBlockTicks, this::tickBlock); // Paper - configurable max block ticks +- gameprofilerfiller.popPush("fluidTicks"); ++ //gameprofilerfiller.popPush("fluidTicks"); // Purpur + this.fluidTicks.tick(j, paperConfig().environment.maxFluidTicks, this::tickFluid); // Paper - configurable max fluid ticks +- gameprofilerfiller.pop(); ++ //gameprofilerfiller.pop(); // Purpur + } +- this.timings.scheduledBlocks.stopTiming(); // Paper ++ //this.timings.scheduledBlocks.stopTiming(); // Paper // Purpur + +- gameprofilerfiller.popPush("raid"); ++ //gameprofilerfiller.popPush("raid"); // Purpur + if (flag) { +- this.timings.raids.startTiming(); // Paper - timings ++ // this.timings.raids.startTiming(); // Paper - timings // Purpur + this.raids.tick(); +- this.timings.raids.stopTiming(); // Paper - timings ++ // this.timings.raids.stopTiming(); // Paper - timings // Purpur + } + +- gameprofilerfiller.popPush("chunkSource"); +- this.timings.chunkProviderTick.startTiming(); // Paper - timings ++ //gameprofilerfiller.popPush("chunkSource"); // Purpur ++ //this.timings.chunkProviderTick.startTiming(); // Paper - timings // Purpur + this.getChunkSource().tick(shouldKeepTicking, true); +- this.timings.chunkProviderTick.stopTiming(); // Paper - timings +- gameprofilerfiller.popPush("blockEvents"); ++ //this.timings.chunkProviderTick.stopTiming(); // Paper - timings // Purpur ++ //gameprofilerfiller.popPush("blockEvents"); // Purpur + if (flag) { +- this.timings.doSounds.startTiming(); // Spigot ++ // this.timings.doSounds.startTiming(); // Spigot // Purpur + this.runBlockEvents(); +- this.timings.doSounds.stopTiming(); // Spigot ++ // this.timings.doSounds.stopTiming(); // Spigot // Purpur + } + + this.handlingTick = false; +- gameprofilerfiller.pop(); ++ //gameprofilerfiller.pop(); // Purpur + boolean flag1 = !paperConfig().unsupportedSettings.disableWorldTickingWhenEmpty || !this.players.isEmpty() || !this.getForcedChunks().isEmpty(); // CraftBukkit - this prevents entity cleanup, other issues on servers with no players // Paper - restore this + + if (flag1) { +@@ -889,24 +910,24 @@ public class ServerLevel extends Level implements WorldGenLevel { + } + + if (flag1 || this.emptyTime++ < 300) { +- gameprofilerfiller.push("entities"); +- this.timings.tickEntities.startTiming(); // Spigot ++ //gameprofilerfiller.push("entities"); // Purpur ++ //this.timings.tickEntities.startTiming(); // Spigot // Purpur + if (this.dragonFight != null && flag) { +- gameprofilerfiller.push("dragonFight"); ++ //gameprofilerfiller.push("dragonFight"); // Purpur + this.dragonFight.tick(); +- gameprofilerfiller.pop(); ++ //gameprofilerfiller.pop(); // Purpur + } + + org.spigotmc.ActivationRange.activateEntities(this); // Spigot +- this.timings.entityTick.startTiming(); // Spigot ++ //this.timings.entityTick.startTiming(); // Spigot // Purpur + this.entityTickList.forEach((entity) -> { + if (!entity.isRemoved()) { + if (false && this.shouldDiscardEntity(entity)) { // CraftBukkit - We prevent spawning in general, so this butchering is not needed + entity.discard(); + } else if (!tickratemanager.isEntityFrozen(entity)) { +- gameprofilerfiller.push("checkDespawn"); ++ //gameprofilerfiller.push("checkDespawn"); // Purpur + entity.checkDespawn(); +- gameprofilerfiller.pop(); ++ //gameprofilerfiller.pop(); // Purpur + if (true || this.chunkSource.chunkMap.getDistanceManager().inEntityTickingRange(entity.chunkPosition().toLong())) { // Paper - now always true if in the ticking list + Entity entity1 = entity.getVehicle(); + +@@ -918,22 +939,21 @@ public class ServerLevel extends Level implements WorldGenLevel { + entity.stopRiding(); + } + +- gameprofilerfiller.push("tick"); ++ //gameprofilerfiller.push("tick"); // Purpur + this.guardEntityTick(this::tickNonPassenger, entity); +- gameprofilerfiller.pop(); ++ //gameprofilerfiller.pop(); // Purpur + } + } + } + }); +- this.timings.entityTick.stopTiming(); // Spigot +- this.timings.tickEntities.stopTiming(); // Spigot +- gameprofilerfiller.pop(); ++ //this.timings.entityTick.stopTiming(); // Spigot // Purpur ++ //this.timings.tickEntities.stopTiming(); // Spigot // Purpur ++ //gameprofilerfiller.pop(); // Purpur + this.tickBlockEntities(); + } + +- gameprofilerfiller.push("entityManagement"); ++ //gameprofilerfiller.push("entityManagement"); // Purpur + //this.entityManager.tick(); // Paper - rewrite chunk system +- gameprofilerfiller.pop(); + } + + @Override +@@ -951,6 +971,13 @@ public class ServerLevel extends Level implements WorldGenLevel { + this.serverLevelData.setGameTime(i); + this.serverLevelData.getScheduledEvents().tick(this.server, i); + if (this.levelData.getGameRules().getBoolean(GameRules.RULE_DAYLIGHT)) { ++ // Purpur start ++ int incrementTicks = isDay() ? this.purpurConfig.daytimeTicks : this.purpurConfig.nighttimeTicks; ++ if (incrementTicks != 12000) { ++ this.preciseTime += 12000 / (double) incrementTicks; ++ this.setDayTime(this.preciseTime); ++ } else ++ // Purpur end + this.setDayTime(this.levelData.getDayTime() + 1L); + } + +@@ -959,7 +986,21 @@ public class ServerLevel extends Level implements WorldGenLevel { + + public void setDayTime(long timeOfDay) { + this.serverLevelData.setDayTime(timeOfDay); ++ // Purpur start ++ this.preciseTime = timeOfDay; ++ this.forceTime = false; + } ++ public void setDayTime(double i) { ++ this.serverLevelData.setDayTime((long) i); ++ this.forceTime = true; ++ // Purpur end ++ } ++ ++ // Purpur start ++ public boolean isForceTime() { ++ return this.forceTime; ++ } ++ // Purpur end + + public void tickCustomSpawners(boolean spawnMonsters, boolean spawnAnimals) { + Iterator iterator = this.customSpawners.iterator(); +@@ -992,9 +1033,9 @@ public class ServerLevel extends Level implements WorldGenLevel { + boolean flag = this.isRaining(); + int j = chunkcoordintpair.getMinBlockX(); + int k = chunkcoordintpair.getMinBlockZ(); +- ProfilerFiller gameprofilerfiller = this.getProfiler(); ++ //ProfilerFiller gameprofilerfiller = this.getProfiler(); // Purpur + +- gameprofilerfiller.push("thunder"); ++ //gameprofilerfiller.push("thunder"); // Purpur + final BlockPos.MutableBlockPos blockposition = this.chunkTickMutablePosition; // Paper - use mutable to reduce allocation rate, final to force compile fail on change + + if (!this.paperConfig().environment.disableThunder && flag && this.isThundering() && this.spigotConfig.thunderChance > 0 && this.random.nextInt(this.spigotConfig.thunderChance) == 0) { // Spigot // Paper - Option to disable thunder +@@ -1005,10 +1046,18 @@ public class ServerLevel extends Level implements WorldGenLevel { + boolean flag1 = this.getGameRules().getBoolean(GameRules.RULE_DOMOBSPAWNING) && this.random.nextDouble() < (double) difficultydamagescaler.getEffectiveDifficulty() * this.paperConfig().entities.spawning.skeletonHorseThunderSpawnChance.or(0.01D) && !this.getBlockState(blockposition.below()).is(Blocks.LIGHTNING_ROD); // Paper - Configurable spawn chances for skeleton horses + + if (flag1) { +- SkeletonHorse entityhorseskeleton = (SkeletonHorse) EntityType.SKELETON_HORSE.create(this); ++ // Purpur start ++ net.minecraft.world.entity.animal.horse.AbstractHorse entityhorseskeleton; ++ if (purpurConfig.zombieHorseSpawnChance > 0D && random.nextDouble() <= purpurConfig.zombieHorseSpawnChance) { ++ entityhorseskeleton = EntityType.ZOMBIE_HORSE.create(this); ++ } else { ++ entityhorseskeleton = EntityType.SKELETON_HORSE.create(this); ++ if (entityhorseskeleton != null) ((SkeletonHorse) entityhorseskeleton).setTrap(true); ++ } ++ // Purpur end + + if (entityhorseskeleton != null) { +- entityhorseskeleton.setTrap(true); ++ //entityhorseskeleton.setTrap(true); // Purpur - moved up + entityhorseskeleton.setAge(0); + entityhorseskeleton.setPos((double) blockposition.getX(), (double) blockposition.getY(), (double) blockposition.getZ()); + this.addFreshEntity(entityhorseskeleton, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.LIGHTNING); // CraftBukkit +@@ -1025,7 +1074,7 @@ public class ServerLevel extends Level implements WorldGenLevel { + } + } + +- gameprofilerfiller.popPush("iceandsnow"); ++ //gameprofilerfiller.popPush("iceandsnow"); // Purpur + + if (!this.paperConfig().environment.disableIceAndSnow) { // Paper - Option to disable ice and snow + for (int l = 0; l < randomTickSpeed; ++l) { +@@ -1038,8 +1087,8 @@ public class ServerLevel extends Level implements WorldGenLevel { + } + } // Paper - Option to disable ice and snow + +- gameprofilerfiller.popPush("tickBlocks"); +- timings.chunkTicksBlocks.startTiming(); // Paper ++ //gameprofilerfiller.popPush("tickBlocks"); // Purpur ++ //timings.chunkTicksBlocks.startTiming(); // Paper // Purpur + if (randomTickSpeed > 0) { + // Paper start - optimize random block ticking + LevelChunkSection[] sections = chunk.getSections(); +@@ -1073,8 +1122,8 @@ public class ServerLevel extends Level implements WorldGenLevel { + } + // Paper end - optimise random block ticking + +- timings.chunkTicksBlocks.stopTiming(); // Paper +- gameprofilerfiller.pop(); ++ //timings.chunkTicksBlocks.stopTiming(); // Paper // Purpur ++ //gameprofilerfiller.pop(); // Purpur + } + + @VisibleForTesting +@@ -1132,7 +1181,7 @@ public class ServerLevel extends Level implements WorldGenLevel { + return holder.is(PoiTypes.LIGHTNING_ROD); + }, (blockposition1) -> { + return blockposition1.getY() == this.getHeight(Heightmap.Types.WORLD_SURFACE, blockposition1.getX(), blockposition1.getZ()) - 1; +- }, pos, 128, PoiManager.Occupancy.ANY); ++ }, pos, org.purpurmc.purpur.PurpurConfig.lightningRodRange, PoiManager.Occupancy.ANY); + + return optional.map((blockposition1) -> { + return blockposition1.above(1); +@@ -1181,11 +1230,27 @@ public class ServerLevel extends Level implements WorldGenLevel { + if (this.canSleepThroughNights()) { + if (!this.getServer().isSingleplayer() || this.getServer().isPublished()) { + int i = this.getGameRules().getInt(GameRules.RULE_PLAYERS_SLEEPING_PERCENTAGE); +- MutableComponent ichatmutablecomponent; ++ Component ichatmutablecomponent; + + if (this.sleepStatus.areEnoughSleeping(i)) { ++ // Purpur start ++ if (org.purpurmc.purpur.PurpurConfig.sleepSkippingNight.isBlank()) { ++ return; ++ } ++ if (!org.purpurmc.purpur.PurpurConfig.sleepSkippingNight.equalsIgnoreCase("default")) { ++ ichatmutablecomponent = io.papermc.paper.adventure.PaperAdventure.asVanilla(net.kyori.adventure.text.minimessage.MiniMessage.miniMessage().deserialize(org.purpurmc.purpur.PurpurConfig.sleepSkippingNight)); ++ } else + ichatmutablecomponent = Component.translatable("sleep.skipping_night"); + } else { ++ if (org.purpurmc.purpur.PurpurConfig.sleepingPlayersPercent.isBlank()) { ++ return; ++ } ++ if (!org.purpurmc.purpur.PurpurConfig.sleepingPlayersPercent.equalsIgnoreCase("default")) { ++ ichatmutablecomponent = io.papermc.paper.adventure.PaperAdventure.asVanilla(net.kyori.adventure.text.minimessage.MiniMessage.miniMessage().deserialize(org.purpurmc.purpur.PurpurConfig.sleepingPlayersPercent, ++ net.kyori.adventure.text.minimessage.tag.resolver.Placeholder.parsed("count", Integer.toString(this.sleepStatus.amountSleeping())), ++ net.kyori.adventure.text.minimessage.tag.resolver.Placeholder.parsed("total", Integer.toString(this.sleepStatus.sleepersNeeded(i))))); ++ } else ++ // Purpur end + ichatmutablecomponent = Component.translatable("sleep.players_sleeping", this.sleepStatus.amountSleeping(), this.sleepStatus.sleepersNeeded(i)); + } + +@@ -1325,6 +1390,7 @@ public class ServerLevel extends Level implements WorldGenLevel { + @VisibleForTesting + public void resetWeatherCycle() { + // CraftBukkit start ++ if (this.purpurConfig.rainStopsAfterSleep) // Purpur + this.serverLevelData.setRaining(false, org.bukkit.event.weather.WeatherChangeEvent.Cause.SLEEP); // Paper - Add cause to Weather/ThunderChangeEvents + // If we stop due to everyone sleeping we should reset the weather duration to some other random value. + // Not that everyone ever manages to get the whole server to sleep at the same time.... +@@ -1332,6 +1398,7 @@ public class ServerLevel extends Level implements WorldGenLevel { + this.serverLevelData.setRainTime(0); + } + // CraftBukkit end ++ if (this.purpurConfig.thunderStopsAfterSleep) // Purpur + this.serverLevelData.setThundering(false, org.bukkit.event.weather.ThunderChangeEvent.Cause.SLEEP); // Paper - Add cause to Weather/ThunderChangeEvents + // CraftBukkit start + // If we stop due to everyone sleeping we should reset the weather duration to some other random value. +@@ -1399,24 +1466,24 @@ public class ServerLevel extends Level implements WorldGenLevel { + // Spigot end + // Paper start- timings + final boolean isActive = org.spigotmc.ActivationRange.checkIfActive(entity); +- timer = isActive ? entity.getType().tickTimer.startTiming() : entity.getType().inactiveTickTimer.startTiming(); // Paper +- try { ++ //timer = isActive ? entity.getType().tickTimer.startTiming() : entity.getType().inactiveTickTimer.startTiming(); // Paper // Purpur ++ //try { // Purpur + // Paper end - timings + entity.setOldPosAndRot(); +- ProfilerFiller gameprofilerfiller = this.getProfiler(); ++ //ProfilerFiller gameprofilerfiller = this.getProfiler(); // Purpur + + ++entity.tickCount; +- this.getProfiler().push(() -> { ++ /*this.getProfiler().push(() -> { // Purpur + return BuiltInRegistries.ENTITY_TYPE.getKey(entity.getType()).toString(); +- }); +- gameprofilerfiller.incrementCounter("tickNonPassenger"); ++ });*/ // Purpur ++ //gameprofilerfiller.incrementCounter("tickNonPassenger"); // Purpur + if (isActive) { // Paper - EAR 2 + TimingHistory.activatedEntityTicks++; + entity.tick(); + entity.postTick(); // CraftBukkit + } else { entity.inactiveTick(); } // Paper - EAR 2 +- this.getProfiler().pop(); +- } finally { timer.stopTiming(); } // Paper - timings ++ //this.getProfiler().pop(); // Purpur ++ //} finally { timer.stopTiming(); } // Paper - timings // Purpur + Iterator iterator = entity.getPassengers().iterator(); + + while (iterator.hasNext()) { +@@ -1439,17 +1506,17 @@ public class ServerLevel extends Level implements WorldGenLevel { + if (passenger instanceof Player || this.entityTickList.contains(passenger)) { + // Paper - EAR 2 + final boolean isActive = org.spigotmc.ActivationRange.checkIfActive(passenger); +- co.aikar.timings.Timing timer = isActive ? passenger.getType().passengerTickTimer.startTiming() : passenger.getType().passengerInactiveTickTimer.startTiming(); // Paper +- try { ++ //co.aikar.timings.Timing timer = isActive ? passenger.getType().passengerTickTimer.startTiming() : passenger.getType().passengerInactiveTickTimer.startTiming(); // Paper // Purpur ++ //try { // Purpur + // Paper end + passenger.setOldPosAndRot(); + ++passenger.tickCount; +- ProfilerFiller gameprofilerfiller = this.getProfiler(); ++ //ProfilerFiller gameprofilerfiller = this.getProfiler(); // Purpur + +- gameprofilerfiller.push(() -> { ++ /*gameprofilerfiller.push(() -> { // Purpur + return BuiltInRegistries.ENTITY_TYPE.getKey(passenger.getType()).toString(); +- }); +- gameprofilerfiller.incrementCounter("tickPassenger"); ++ });*/ // Purpur ++ //gameprofilerfiller.incrementCounter("tickPassenger"); // Purpur + // Paper start - EAR 2 + if (isActive) { + passenger.rideTick(); +@@ -1461,7 +1528,7 @@ public class ServerLevel extends Level implements WorldGenLevel { + vehicle.positionRider(passenger); + } + // Paper end - EAR 2 +- gameprofilerfiller.pop(); ++ //gameprofilerfiller.pop(); // Purpur + Iterator iterator = passenger.getPassengers().iterator(); + + while (iterator.hasNext()) { +@@ -1470,7 +1537,7 @@ public class ServerLevel extends Level implements WorldGenLevel { + this.tickPassenger(passenger, entity2); + } + +- } finally { timer.stopTiming(); }// Paper - EAR2 timings ++ //} finally { timer.stopTiming(); }// Paper - EAR2 timings // Purpur + } + } else { + passenger.stopRiding(); +@@ -1490,14 +1557,14 @@ public class ServerLevel extends Level implements WorldGenLevel { + org.bukkit.Bukkit.getPluginManager().callEvent(new org.bukkit.event.world.WorldSaveEvent(getWorld())); + } + +- try (co.aikar.timings.Timing ignored = this.timings.worldSave.startTiming()) { ++ //try (co.aikar.timings.Timing ignored = this.timings.worldSave.startTiming()) { // Purpur + if (doFull) { + this.saveLevelData(true); // Paper - Write SavedData IO async + } + +- this.timings.worldSaveChunks.startTiming(); // Paper ++ //this.timings.worldSaveChunks.startTiming(); // Paper // Purpur + if (!this.noSave()) chunkproviderserver.saveIncrementally(); +- this.timings.worldSaveChunks.stopTiming(); // Paper ++ //this.timings.worldSaveChunks.stopTiming(); // Paper // Purpur + + // Copied from save() + // CraftBukkit start - moved from MinecraftServer.saveChunks +@@ -1509,7 +1576,7 @@ public class ServerLevel extends Level implements WorldGenLevel { + this.convertable.saveDataTag(this.server.registryAccess(), this.serverLevelData, this.server.getPlayerList().getSingleplayerData()); + } + // CraftBukkit end +- } ++ //} // Purpur + } + // Paper end - Incremental chunk and player saving + +@@ -1523,7 +1590,7 @@ public class ServerLevel extends Level implements WorldGenLevel { + + if (!savingDisabled) { + org.bukkit.Bukkit.getPluginManager().callEvent(new org.bukkit.event.world.WorldSaveEvent(this.getWorld())); // CraftBukkit +- try (co.aikar.timings.Timing ignored = timings.worldSave.startTiming()) { // Paper ++ //try (co.aikar.timings.Timing ignored = timings.worldSave.startTiming()) { // Paper // Purpur // Purpur + if (progressListener != null) { + progressListener.progressStartNoAbort(Component.translatable("menu.savingLevel")); + } +@@ -1533,11 +1600,11 @@ public class ServerLevel extends Level implements WorldGenLevel { + progressListener.progressStage(Component.translatable("menu.savingChunks")); + } + +- timings.worldSaveChunks.startTiming(); // Paper ++ //timings.worldSaveChunks.startTiming(); // Paper // Purpur + if (!close) chunkproviderserver.save(flush); // Paper - rewrite chunk system + if (close) chunkproviderserver.close(true); // Paper - rewrite chunk system +- timings.worldSaveChunks.stopTiming(); // Paper +- }// Paper ++ //timings.worldSaveChunks.stopTiming(); // Paper // Purpur ++ //}// Paper // Purpur + // Paper - rewrite chunk system - entity saving moved into ChunkHolder + + } else if (close) { chunkproviderserver.close(false); } // Paper - rewrite chunk system +@@ -2787,7 +2854,7 @@ public class ServerLevel extends Level implements WorldGenLevel { + // Spigot Start + if (entity.getBukkitEntity() instanceof org.bukkit.inventory.InventoryHolder && (!(entity instanceof ServerPlayer) || entity.getRemovalReason() != Entity.RemovalReason.KILLED)) { // SPIGOT-6876: closeInventory clears death message + // Paper start - Fix merchant inventory not closing on entity removal +- if (entity.getBukkitEntity() instanceof org.bukkit.inventory.Merchant merchant && merchant.getTrader() != null) { ++ if (!entity.level().purpurConfig.playerVoidTrading && entity.getBukkitEntity() instanceof org.bukkit.inventory.Merchant merchant && merchant.getTrader() != null) { // Purpur + merchant.getTrader().closeInventory(org.bukkit.event.inventory.InventoryCloseEvent.Reason.UNLOADED); + } + // Paper end - Fix merchant inventory not closing on entity removal +diff --git a/src/main/java/net/minecraft/server/level/ServerPlayer.java b/src/main/java/net/minecraft/server/level/ServerPlayer.java +index 8437316888c6056060a2780652147590b6fe7443..42a623254bd2886d09eb0cfeb01dd12d0dda19cf 100644 +--- a/src/main/java/net/minecraft/server/level/ServerPlayer.java ++++ b/src/main/java/net/minecraft/server/level/ServerPlayer.java +@@ -298,6 +298,10 @@ public class ServerPlayer extends Player { + public com.destroystokyo.paper.event.entity.PlayerNaturallySpawnCreaturesEvent playerNaturallySpawnedEvent; // Paper - PlayerNaturallySpawnCreaturesEvent + public @Nullable String clientBrandName = null; // Paper - Brand support + public org.bukkit.event.player.PlayerQuitEvent.QuitReason quitReason = null; // Paper - Add API for quit reason; there are a lot of changes to do if we change all methods leading to the event ++ public boolean purpurClient = false; // Purpur ++ private boolean tpsBar = false; // Purpur ++ private boolean compassBar = false; // Purpur ++ private boolean ramBar = false; // Purpur + + // Paper start - replace player chunk loader + private final java.util.concurrent.atomic.AtomicReference viewDistances = new java.util.concurrent.atomic.AtomicReference<>(new io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.ViewDistances(-1, -1, -1)); +@@ -608,6 +612,9 @@ public class ServerPlayer extends Player { + }); + } + ++ if (nbt.contains("Purpur.TPSBar")) { this.tpsBar = nbt.getBoolean("Purpur.TPSBar"); } // Purpur ++ if (nbt.contains("Purpur.CompassBar")) { this.compassBar = nbt.getBoolean("Purpur.CompassBar"); } // Purpur ++ if (nbt.contains("Purpur.RamBar")) { this.ramBar = nbt.getBoolean("Purpur.RamBar"); } // Purpur + } + + @Override +@@ -684,6 +691,9 @@ public class ServerPlayer extends Player { + }); + } + ++ nbt.putBoolean("Purpur.RamBar", this.ramBar); // Purpur ++ nbt.putBoolean("Purpur.TPSBar", this.tpsBar); // Purpur ++ nbt.putBoolean("Purpur.CompassBar", this.compassBar); // Purpur + } + + // CraftBukkit start - World fallback code, either respawn location or global spawn +@@ -813,6 +823,15 @@ public class ServerPlayer extends Player { + this.trackEnteredOrExitedLavaOnVehicle(); + this.updatePlayerAttributes(); + this.advancements.flushDirty(this); ++ ++ // Purpur start ++ if (this.level().purpurConfig.useNightVisionWhenRiding && this.getVehicle() != null && this.getVehicle().getRider() == this && this.level().getGameTime() % 100 == 0) { // 5 seconds ++ MobEffectInstance nightVision = this.getEffect(MobEffects.NIGHT_VISION); ++ if (nightVision == null || nightVision.getDuration() <= 300) { // 15 seconds ++ this.addEffect(new MobEffectInstance(MobEffects.NIGHT_VISION, 400, 0)); // 20 seconds ++ } ++ } ++ // Purpur end + } + + private void updatePlayerAttributes() { +@@ -1076,6 +1095,7 @@ public class ServerPlayer extends Player { + })); + PlayerTeam scoreboardteam = this.getTeam(); + ++ if (org.purpurmc.purpur.PurpurConfig.deathMessageOnlyBroadcastToAffectedPlayer) this.sendSystemMessage(ichatbasecomponent); else // Purpur + if (scoreboardteam != null && scoreboardteam.getDeathMessageVisibility() != Team.Visibility.ALWAYS) { + if (scoreboardteam.getDeathMessageVisibility() == Team.Visibility.HIDE_FOR_OTHER_TEAMS) { + this.server.getPlayerList().broadcastSystemToTeam(this, ichatbasecomponent); +@@ -1179,6 +1199,16 @@ public class ServerPlayer extends Player { + if (this.isInvulnerableTo(source)) { + return false; + } else { ++ // Purpur start ++ if (source.is(DamageTypeTags.IS_FALL)) { // Purpur ++ if (getRootVehicle() instanceof net.minecraft.world.entity.vehicle.AbstractMinecart && level().purpurConfig.minecartControllable && !level().purpurConfig.minecartControllableFallDamage) { ++ return false; ++ } ++ if (getRootVehicle() instanceof net.minecraft.world.entity.vehicle.Boat && !level().purpurConfig.boatsDoFallDamage) { ++ return false; ++ } ++ } ++ // Purpur end + boolean flag = this.server.isDedicatedServer() && this.isPvpAllowed() && source.is(DamageTypeTags.IS_FALL); + + if (!flag && this.spawnInvulnerableTime > 0 && !source.is(DamageTypeTags.BYPASSES_INVULNERABILITY)) { +@@ -1290,7 +1320,7 @@ public class ServerPlayer extends Player { + PortalInfo shapedetectorshape = this.findDimensionEntryPoint(worldserver); + + if (shapedetectorshape != null) { +- worldserver1.getProfiler().push("moving"); ++ //worldserver1.getProfiler().push("moving"); // Purpur + worldserver = shapedetectorshape.world; // CraftBukkit + if (worldserver == null) { } else // CraftBukkit - empty to fall through to null to event + if (resourcekey == LevelStem.OVERWORLD && worldserver.getTypeKey() == LevelStem.NETHER) { // CraftBukkit +@@ -1313,8 +1343,8 @@ public class ServerPlayer extends Player { + worldserver = ((CraftWorld) exit.getWorld()).getHandle(); + // CraftBukkit end + +- worldserver1.getProfiler().pop(); +- worldserver1.getProfiler().push("placing"); ++ //worldserver1.getProfiler().pop(); // Purpur ++ //worldserver1.getProfiler().push("placing"); // Purpur + if (true) { // CraftBukkit + this.isChangingDimension = true; // CraftBukkit - Set teleport invulnerability only if player changing worlds + +@@ -1325,13 +1355,14 @@ public class ServerPlayer extends Player { + playerlist.sendPlayerPermissionLevel(this); + worldserver1.removePlayerImmediately(this, Entity.RemovalReason.CHANGED_DIMENSION); + this.unsetRemoved(); ++ this.portalPos = io.papermc.paper.util.MCUtil.toBlockPosition(exit); // Purpur + + // CraftBukkit end + this.setServerLevel(worldserver); + this.connection.teleport(exit); // CraftBukkit - use internal teleport without event + this.connection.resetPosition(); + worldserver.addDuringPortalTeleport(this); +- worldserver1.getProfiler().pop(); ++ //worldserver1.getProfiler().pop(); // Purpur + this.triggerDimensionChangeTriggers(worldserver1); + this.connection.send(new ClientboundPlayerAbilitiesPacket(this.getAbilities())); + playerlist.sendLevelInfo(this, worldserver); +@@ -1481,7 +1512,7 @@ public class ServerPlayer extends Player { + return entitymonster.isPreventingPlayerRest(this); + }); + +- if (!list.isEmpty()) { ++ if (!this.level().purpurConfig.playerSleepNearMonsters && !list.isEmpty()) { // Purpur + return Either.left(Player.BedSleepingProblem.NOT_SAFE); + } + } +@@ -1521,7 +1552,19 @@ public class ServerPlayer extends Player { + }); + + if (!this.serverLevel().canSleepThroughNights()) { +- this.displayClientMessage(Component.translatable("sleep.not_possible"), true); ++ // Purpur start ++ Component clientMessage; ++ if (org.purpurmc.purpur.PurpurConfig.sleepNotPossible.isBlank()) { ++ clientMessage = null; ++ } else if (!org.purpurmc.purpur.PurpurConfig.sleepNotPossible.equalsIgnoreCase("default")) { ++ clientMessage = io.papermc.paper.adventure.PaperAdventure.asVanilla(net.kyori.adventure.text.minimessage.MiniMessage.miniMessage().deserialize(org.purpurmc.purpur.PurpurConfig.sleepNotPossible)); ++ } else { ++ clientMessage = Component.translatable("sleep.not_possible"); ++ } ++ if (clientMessage != null) { ++ this.displayClientMessage(clientMessage, true); ++ } ++ // Purpur end + } + + ((ServerLevel) this.level()).updateSleepingPlayerList(); +@@ -1977,6 +2020,26 @@ public class ServerPlayer extends Player { + this.lastSentExp = -1; // CraftBukkit - Added to reset + } + ++ // Purpur start ++ public void sendActionBarMessage(@Nullable String message) { ++ if (message != null && !message.isEmpty()) { ++ sendActionBarMessage(net.kyori.adventure.text.minimessage.MiniMessage.miniMessage().deserialize(message)); ++ } ++ } ++ ++ public void sendActionBarMessage(@Nullable net.kyori.adventure.text.Component message) { ++ if (message != null) { ++ sendActionBarMessage(io.papermc.paper.adventure.PaperAdventure.asVanilla(message)); ++ } ++ } ++ ++ public void sendActionBarMessage(@Nullable Component message) { ++ if (message != null) { ++ displayClientMessage(message, true); ++ } ++ } ++ // Purpur end ++ + @Override + public void displayClientMessage(Component message, boolean overlay) { + this.sendSystemMessage(message, overlay); +@@ -2296,8 +2359,68 @@ public class ServerPlayer extends Player { + + public void resetLastActionTime() { + this.lastActionTime = Util.getMillis(); ++ this.setAfk(false); // Purpur + } + ++ // Purpur Start ++ private boolean isAfk = false; ++ ++ @Override ++ public void setAfk(boolean afk) { ++ if (this.isAfk == afk) { ++ return; ++ } ++ ++ String msg = afk ? org.purpurmc.purpur.PurpurConfig.afkBroadcastAway : org.purpurmc.purpur.PurpurConfig.afkBroadcastBack; ++ ++ org.purpurmc.purpur.event.PlayerAFKEvent event = new org.purpurmc.purpur.event.PlayerAFKEvent(this.getBukkitEntity(), afk, this.level().purpurConfig.idleTimeoutKick, msg, !Bukkit.isPrimaryThread()); ++ if (!event.callEvent() || event.shouldKick()) { ++ return; ++ } ++ ++ this.isAfk = afk; ++ ++ if (!afk) { ++ resetLastActionTime(); ++ } ++ ++ msg = event.getBroadcastMsg(); ++ if (msg != null && !msg.isEmpty()) { ++ String playerName = this.getGameProfile().getName(); ++ if (org.purpurmc.purpur.PurpurConfig.afkBroadcastUseDisplayName) { ++ net.kyori.adventure.text.Component playerDisplayNameComponent = net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection().deserialize(this.getBukkitEntity().getDisplayName()); ++ playerName = net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer.plainText().serialize(playerDisplayNameComponent); ++ } ++ server.getPlayerList().broadcastMiniMessage(String.format(msg, playerName), false); ++ } ++ ++ if (this.level().purpurConfig.idleTimeoutUpdateTabList) { ++ String scoreboardName = getScoreboardName(); ++ String playerListName = net.kyori.adventure.text.minimessage.MiniMessage.miniMessage().serialize(getBukkitEntity().playerListName()); ++ String[] split = playerListName.split(scoreboardName); ++ String prefix = (split.length > 0 ? split[0] : "").replace(org.purpurmc.purpur.PurpurConfig.afkTabListPrefix, ""); ++ String suffix = (split.length > 1 ? split[1] : "").replace(org.purpurmc.purpur.PurpurConfig.afkTabListSuffix, ""); ++ if (afk) { ++ getBukkitEntity().setPlayerListName(org.purpurmc.purpur.PurpurConfig.afkTabListPrefix + prefix + scoreboardName + suffix + org.purpurmc.purpur.PurpurConfig.afkTabListSuffix, true); ++ } else { ++ getBukkitEntity().setPlayerListName(prefix + scoreboardName + suffix); ++ } ++ } ++ ++ ((ServerLevel) this.level()).updateSleepingPlayerList(); ++ } ++ ++ @Override ++ public boolean isAfk() { ++ return this.isAfk; ++ } ++ ++ @Override ++ public boolean canBeCollidedWith() { ++ return !this.isAfk() && super.canBeCollidedWith(); ++ } ++ // Purpur End ++ + public ServerStatsCounter getStats() { + return this.stats; + } +@@ -2865,4 +2988,50 @@ public class ServerPlayer extends Player { + return (CraftPlayer) super.getBukkitEntity(); + } + // CraftBukkit end ++ ++ // Purpur start ++ public void teleport(Location to) { ++ this.ejectPassengers(); ++ this.stopRiding(true); ++ ++ if (this.isSleeping()) { ++ this.stopSleepInBed(true, false); ++ } ++ ++ if (this.containerMenu != this.inventoryMenu) { ++ this.closeContainer(org.bukkit.event.inventory.InventoryCloseEvent.Reason.TELEPORT); ++ } ++ ++ ServerLevel toLevel = ((CraftWorld) to.getWorld()).getHandle(); ++ if (this.level() == toLevel) { ++ this.connection.internalTeleport(to.getX(), to.getY(), to.getZ(), to.getYaw(), to.getPitch(), java.util.EnumSet.noneOf(net.minecraft.world.entity.RelativeMovement.class)); ++ } else { ++ this.server.getPlayerList().respawn(this, toLevel, true, to, !toLevel.paperConfig().environment.disableTeleportationSuffocationCheck, org.bukkit.event.player.PlayerRespawnEvent.RespawnReason.DEATH); ++ } ++ } ++ ++ public boolean tpsBar() { ++ return this.tpsBar; ++ } ++ ++ public void tpsBar(boolean tpsBar) { ++ this.tpsBar = tpsBar; ++ } ++ ++ public boolean compassBar() { ++ return this.compassBar; ++ } ++ ++ public void compassBar(boolean compassBar) { ++ this.compassBar = compassBar; ++ } ++ ++ public boolean ramBar() { ++ return this.ramBar; ++ } ++ ++ public void ramBar(boolean ramBar) { ++ this.ramBar = ramBar; ++ } ++ // Purpur end + } +diff --git a/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java b/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java +index 5cedce1f432f6b809b25269242a16477682c824f..6d194797d8fe2cd6e5652d596f4bc66ffc3b6375 100644 +--- a/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java ++++ b/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java +@@ -400,6 +400,7 @@ public class ServerPlayerGameMode { + } else {capturedBlockEntity = true;} // Paper - Send block entities after destroy prediction + return false; + } ++ if (this.player.level().purpurConfig.slabHalfBreak && this.player.isShiftKeyDown() && iblockdata.getBlock() instanceof net.minecraft.world.level.block.SlabBlock && ((net.minecraft.world.level.block.SlabBlock) iblockdata.getBlock()).halfBreak(iblockdata, pos, this.player)) return true; // Purpur + } + // CraftBukkit end + +@@ -512,6 +513,7 @@ public class ServerPlayerGameMode { + public InteractionHand interactHand; + public ItemStack interactItemStack; + public InteractionResult useItemOn(ServerPlayer player, Level world, ItemStack stack, InteractionHand hand, BlockHitResult hitResult) { ++ if (shiftClickMended(stack)) return InteractionResult.SUCCESS; // Purpur + BlockPos blockposition = hitResult.getBlockPos(); + BlockState iblockdata = world.getBlockState(blockposition); + boolean cancelledBlock = false; +@@ -573,7 +575,7 @@ public class ServerPlayerGameMode { + ItemStack itemstack1 = stack.copy(); + InteractionResult enuminteractionresult; + +- if (!flag1) { ++ if (!flag1 || (player.level().purpurConfig.composterBulkProcess && iblockdata.is(Blocks.COMPOSTER))) { // Purpur + ItemInteractionResult iteminteractionresult = iblockdata.useItemOn(player.getItemInHand(hand), world, player, hand, hitResult); + + if (iteminteractionresult.consumesAction()) { +@@ -621,4 +623,18 @@ public class ServerPlayerGameMode { + public void setLevel(ServerLevel world) { + this.level = world; + } ++ ++ // Purpur start ++ public boolean shiftClickMended(ItemStack itemstack) { ++ if (this.player.level().purpurConfig.shiftRightClickRepairsMendingPoints > 0 && this.player.isShiftKeyDown() && this.player.getBukkitEntity().hasPermission("purpur.mending_shift_click")) { ++ int points = Math.min(this.player.totalExperience, this.player.level().purpurConfig.shiftRightClickRepairsMendingPoints); ++ if (points > 0 && itemstack.isDamaged() && net.minecraft.world.item.enchantment.EnchantmentHelper.getItemEnchantmentLevel(net.minecraft.world.item.enchantment.Enchantments.MENDING, itemstack) > 0) { ++ this.player.giveExperiencePoints(-points); ++ this.player.level().addFreshEntity(new net.minecraft.world.entity.ExperienceOrb(this.player.level(), this.player.getX(), this.player.getY(), this.player.getZ(), points, org.bukkit.entity.ExperienceOrb.SpawnReason.UNKNOWN, this.player, this.player)); ++ return true; ++ } ++ } ++ return false; ++ } ++ // Purpur end + } +diff --git a/src/main/java/net/minecraft/server/level/WorldGenRegion.java b/src/main/java/net/minecraft/server/level/WorldGenRegion.java +index 1351423a12c19a01f602a202832372a399e6a867..1e2025674eafcf56460c741083c91e2e42d61b19 100644 +--- a/src/main/java/net/minecraft/server/level/WorldGenRegion.java ++++ b/src/main/java/net/minecraft/server/level/WorldGenRegion.java +@@ -326,6 +326,7 @@ public class WorldGenRegion implements WorldGenLevel { + return true; + } else { + // Paper start - Buffer OOB setBlock calls ++ if (!org.purpurmc.purpur.PurpurConfig.loggerSuppressSetBlockFarChunk) // Purpur + if (!hasSetFarWarned) { + Util.logAndPauseIfInIde("Detected setBlock in a far chunk [" + i + ", " + j + "], pos: " + String.valueOf(pos) + ", status: " + String.valueOf(this.generatingStatus) + (this.currentlyGenerating == null ? "" : ", currently generating: " + (String) this.currentlyGenerating.get())); + hasSetFarWarned = true; +diff --git a/src/main/java/net/minecraft/server/network/ServerCommonPacketListenerImpl.java b/src/main/java/net/minecraft/server/network/ServerCommonPacketListenerImpl.java +index 8ac5d8ccf731100a1be690cb2ed1be82cadba8ed..8368c5ff929df9d32cdb95cc2da0e9f7f3b85d2a 100644 +--- a/src/main/java/net/minecraft/server/network/ServerCommonPacketListenerImpl.java ++++ b/src/main/java/net/minecraft/server/network/ServerCommonPacketListenerImpl.java +@@ -73,11 +73,13 @@ public abstract class ServerCommonPacketListenerImpl implements ServerCommonPack + private long keepAliveChallenge; + private long closedListenerTime; + private boolean closed = false; ++ private it.unimi.dsi.fastutil.longs.LongList keepAlives = new it.unimi.dsi.fastutil.longs.LongArrayList(); // Purpur + private int latency; + private volatile boolean suspendFlushingOnServerThread = false; + public final java.util.Map packCallbacks = new java.util.concurrent.ConcurrentHashMap<>(); // Paper - adventure resource pack callbacks + private static final long KEEPALIVE_LIMIT = Long.getLong("paper.playerconnection.keepalive", 30) * 1000; // Paper - provide property to set keepalive limit + protected static final ResourceLocation MINECRAFT_BRAND = new ResourceLocation("brand"); // Paper - Brand support ++ protected static final ResourceLocation PURPUR_CLIENT = new ResourceLocation("purpur", "client"); // Purpur + + public ServerCommonPacketListenerImpl(MinecraftServer minecraftserver, Connection networkmanager, CommonListenerCookie commonlistenercookie, ServerPlayer player) { // CraftBukkit + this.server = minecraftserver; +@@ -123,6 +125,16 @@ public abstract class ServerCommonPacketListenerImpl implements ServerCommonPack + + @Override + public void handleKeepAlive(ServerboundKeepAlivePacket packet) { ++ // Purpur start ++ if (org.purpurmc.purpur.PurpurConfig.useAlternateKeepAlive) { ++ long id = packet.getId(); ++ if (keepAlives.size() > 0 && keepAlives.contains(id)) { ++ int ping = (int) (Util.getMillis() - id); ++ this.latency = (this.latency * 3 + ping) / 4; ++ keepAlives.clear(); // we got a valid response, lets roll with it and forget the rest ++ } ++ } else ++ // Purpur end + //PacketUtils.ensureRunningOnSameThread(packet, this, this.player.serverLevel()); // CraftBukkit // Paper - handle ServerboundKeepAlivePacket async + if (this.keepAlivePending && packet.getId() == this.keepAliveChallenge) { + int i = (int) (Util.getMillis() - this.keepAliveTime); +@@ -170,6 +182,13 @@ public abstract class ServerCommonPacketListenerImpl implements ServerCommonPack + ServerGamePacketListenerImpl.LOGGER.error("Couldn\'t register custom payload", ex); + this.disconnect("Invalid payload REGISTER!", org.bukkit.event.player.PlayerKickEvent.Cause.INVALID_PAYLOAD); // Paper - kick event cause + } ++ // Purpur start ++ } else if (identifier.equals(PURPUR_CLIENT)) { ++ try { ++ player.purpurClient = true; ++ } catch (Exception ignore) { ++ } ++ // Purpur end + } else if (identifier.equals(ServerCommonPacketListenerImpl.CUSTOM_UNREGISTER)) { + try { + String channels = payload.toString(com.google.common.base.Charsets.UTF_8); +@@ -246,12 +265,27 @@ public abstract class ServerCommonPacketListenerImpl implements ServerCommonPack + } + + protected void keepConnectionAlive() { +- this.server.getProfiler().push("keepAlive"); ++ //this.server.getProfiler().push("keepAlive"); // Purpur + // Paper start - give clients a longer time to respond to pings as per pre 1.12.2 timings + // This should effectively place the keepalive handling back to "as it was" before 1.12.2 + long currentTime = Util.getMillis(); + long elapsedTime = currentTime - this.keepAliveTime; + ++ // Purpur start ++ if (org.purpurmc.purpur.PurpurConfig.useAlternateKeepAlive) { ++ if (elapsedTime >= 1000L) { // 1 second ++ if (!processedDisconnect && keepAlives.size() * 1000L >= KEEPALIVE_LIMIT) { ++ LOGGER.warn("{} was kicked due to keepalive timeout!", this.player.getScoreboardName()); ++ disconnect(ServerCommonPacketListenerImpl.TIMEOUT_DISCONNECTION_MESSAGE, org.bukkit.event.player.PlayerKickEvent.Cause.TIMEOUT); ++ } else { ++ keepAliveTime = currentTime; // hijack this field for 1 second intervals ++ keepAlives.add(currentTime); // currentTime is ID ++ send(new ClientboundKeepAlivePacket(currentTime)); ++ } ++ } ++ } else ++ // Purpur end ++ + if (!this.isSingleplayerOwner() && elapsedTime >= KEEPALIVE_LIMIT) { // Paper - check keepalive limit, don't fire if already disconnected + if (this.keepAlivePending && !this.processedDisconnect) { // Paper + this.disconnect(ServerCommonPacketListenerImpl.TIMEOUT_DISCONNECTION_MESSAGE, org.bukkit.event.player.PlayerKickEvent.Cause.TIMEOUT); // Paper - kick event cause +@@ -264,7 +298,7 @@ public abstract class ServerCommonPacketListenerImpl implements ServerCommonPack + } + // Paper end - give clients a longer time to respond to pings as per pre 1.12.2 timings + +- this.server.getProfiler().pop(); ++ //this.server.getProfiler().pop(); // Purpur + } + + private boolean checkIfClosed(long time) { +diff --git a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java +index 8e67853a7a93fa736c147e8b2df537746dc8e94f..6d9242bc79526ebe4fdfe1f5d0ded429da2a9f95 100644 +--- a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java ++++ b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java +@@ -332,6 +332,20 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl + private boolean justTeleported = false; + // CraftBukkit end + ++ // Purpur start ++ private final com.google.common.cache.LoadingCache kickPermissionCache = com.google.common.cache.CacheBuilder.newBuilder() ++ .maximumSize(1000) ++ .expireAfterWrite(1, java.util.concurrent.TimeUnit.MINUTES) ++ .build( ++ new com.google.common.cache.CacheLoader<>() { ++ @Override ++ public Boolean load(CraftPlayer player) { ++ return player.hasPermission("purpur.bypassIdleKick"); ++ } ++ } ++ ); ++ // Purpur end ++ + @Override + public void tick() { + if (this.ackBlockChangesUpTo > -1) { +@@ -399,6 +413,12 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl + } + + if (this.player.getLastActionTime() > 0L && this.server.getPlayerIdleTimeout() > 0 && Util.getMillis() - this.player.getLastActionTime() > (long) this.server.getPlayerIdleTimeout() * 1000L * 60L && !this.player.wonGame) { // Paper - Prevent AFK kick while watching end credits ++ // Purpur start ++ this.player.setAfk(true); ++ if (!this.player.level().purpurConfig.idleTimeoutKick || (!Boolean.parseBoolean(System.getenv("PURPUR_FORCE_IDLE_KICK")) && kickPermissionCache.getUnchecked(this.player.getBukkitEntity()))) { ++ return; ++ } ++ // Purpur end + this.player.resetLastActionTime(); // CraftBukkit - SPIGOT-854 + this.disconnect(Component.translatable("multiplayer.disconnect.idling"), org.bukkit.event.player.PlayerKickEvent.Cause.IDLING); // Paper - kick event cause + } +@@ -658,6 +678,8 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl + this.lastYaw = to.getYaw(); + this.lastPitch = to.getPitch(); + ++ if (!to.getWorld().getUID().equals(from.getWorld().getUID()) || to.getBlockX() != from.getBlockX() || to.getBlockY() != from.getBlockY() || to.getBlockZ() != from.getBlockZ() || to.getYaw() != from.getYaw() || to.getPitch() != from.getPitch()) this.player.resetLastActionTime(); // Purpur ++ + Location oldTo = to.clone(); + PlayerMoveEvent event = new PlayerMoveEvent(player, from, to); + this.cserver.getPluginManager().callEvent(event); +@@ -731,6 +753,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl + if (packet.getId() == this.awaitingTeleport) { + if (this.awaitingPositionFromClient == null) { + this.disconnect(Component.translatable("multiplayer.disconnect.invalid_player_movement"), org.bukkit.event.player.PlayerKickEvent.Cause.INVALID_PLAYER_MOVEMENT); // Paper - kick event cause ++ ServerGamePacketListenerImpl.LOGGER.warn("Disconnected on accept teleport packet. Was not expecting position data from client at this time"); // Purpur + return; + } + +@@ -1161,10 +1184,15 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl + int maxBookPageSize = io.papermc.paper.configuration.GlobalConfiguration.get().itemValidation.bookSize.pageMax; + double multiplier = Math.max(0.3D, Math.min(1D, io.papermc.paper.configuration.GlobalConfiguration.get().itemValidation.bookSize.totalMultiplier)); + long byteAllowed = maxBookPageSize; ++ // Purpur start ++ int slot = packet.slot(); ++ ItemStack itemstack = Inventory.isHotbarSlot(slot) || slot == Inventory.SLOT_OFFHAND ? this.player.getInventory().getItem(slot) : ItemStack.EMPTY; ++ // Purpur end + for (String testString : pageList) { + int byteLength = testString.getBytes(java.nio.charset.StandardCharsets.UTF_8).length; + if (byteLength > 256 * 4) { + ServerGamePacketListenerImpl.LOGGER.warn(this.player.getScoreboardName() + " tried to send a book with with a page too large!"); ++ org.purpurmc.purpur.event.player.PlayerBookTooLargeEvent event = new org.purpurmc.purpur.event.player.PlayerBookTooLargeEvent(player.getBukkitEntity(), itemstack.asBukkitCopy()); if (event.shouldKickPlayer()) // Purpur + server.scheduleOnMain(() -> this.disconnect("Book too large!", org.bukkit.event.player.PlayerKickEvent.Cause.ILLEGAL_ACTION)); // Paper - kick event cause + return; + } +@@ -1188,6 +1216,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl + + if (byteTotal > byteAllowed) { + ServerGamePacketListenerImpl.LOGGER.warn(this.player.getScoreboardName() + " tried to send too large of a book. Book Size: " + byteTotal + " - Allowed: "+ byteAllowed + " - Pages: " + pageList.size()); ++ org.purpurmc.purpur.event.player.PlayerBookTooLargeEvent event = new org.purpurmc.purpur.event.player.PlayerBookTooLargeEvent(player.getBukkitEntity(), itemstack.asBukkitCopy()); if (event.shouldKickPlayer()) // Purpur + server.scheduleOnMain(() -> this.disconnect("Book too large!", org.bukkit.event.player.PlayerKickEvent.Cause.ILLEGAL_ACTION)); // Paper - kick event cause + return; + } +@@ -1306,8 +1335,16 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl + @Override + public void handleMovePlayer(ServerboundMovePlayerPacket packet) { + PacketUtils.ensureRunningOnSameThread(packet, this, this.player.serverLevel()); +- if (ServerGamePacketListenerImpl.containsInvalidValues(packet.getX(0.0D), packet.getY(0.0D), packet.getZ(0.0D), packet.getYRot(0.0F), packet.getXRot(0.0F))) { ++ // Purpur start ++ boolean invalidX = Double.isNaN(packet.getX(0.0D)); ++ boolean invalidY = Double.isNaN(packet.getY(0.0D)); ++ boolean invalidZ = Double.isNaN(packet.getZ(0.0D)); ++ boolean invalidYaw = !Floats.isFinite(packet.getYRot(0.0F)); ++ boolean invalidPitch = !Floats.isFinite(packet.getXRot(0.0F)); ++ if (invalidX || invalidY || invalidZ || invalidYaw || invalidPitch) { + this.disconnect(Component.translatable("multiplayer.disconnect.invalid_player_movement"), org.bukkit.event.player.PlayerKickEvent.Cause.INVALID_PLAYER_MOVEMENT); // Paper - kick event cause ++ ServerGamePacketListenerImpl.LOGGER.warn(String.format("Disconnected on move player packet. Invalid data: x=%b, y=%b, z=%b, yaw=%b, pitch=%b", invalidX, invalidY, invalidZ, invalidYaw, invalidPitch)); ++ // Purpur end + } else { + ServerLevel worldserver = this.player.serverLevel(); + +@@ -1494,7 +1531,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl + movedWrongly = true; + if (event.getLogWarning()) + // Paper end +- ServerGamePacketListenerImpl.LOGGER.warn("{} moved wrongly!", this.player.getName().getString()); ++ ServerGamePacketListenerImpl.LOGGER.warn("{} moved wrongly!, ({})", this.player.getName().getString(), d11); // Purpur + } // Paper + } + +@@ -1562,6 +1599,8 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl + this.lastYaw = to.getYaw(); + this.lastPitch = to.getPitch(); + ++ if (!to.getWorld().getUID().equals(from.getWorld().getUID()) || to.getBlockX() != from.getBlockX() || to.getBlockY() != from.getBlockY() || to.getBlockZ() != from.getBlockZ() || to.getYaw() != from.getYaw() || to.getPitch() != from.getPitch()) this.player.resetLastActionTime(); // Purpur ++ + Location oldTo = to.clone(); + PlayerMoveEvent event = new PlayerMoveEvent(player, from, to); + this.cserver.getPluginManager().callEvent(event); +@@ -1603,6 +1642,13 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl + this.player.resetCurrentImpulseContext(); + } + ++ // Purpur Start ++ if (this.player.level().purpurConfig.dontRunWithScissors && this.player.isSprinting() && !(this.player.level().purpurConfig.ignoreScissorsInWater && this.player.isInWater()) && !(this.player.level().purpurConfig.ignoreScissorsInLava && this.player.isInLava()) && (isScissor(this.player.getItemInHand(InteractionHand.MAIN_HAND)) || isScissor(this.player.getItemInHand(InteractionHand.OFF_HAND))) && (int) (Math.random() * 10) == 0) { ++ this.player.hurt(this.player.damageSources().scissors(), (float) this.player.level().purpurConfig.scissorsRunningDamage); ++ if (!org.purpurmc.purpur.PurpurConfig.dontRunWithScissors.isBlank()) this.player.sendActionBarMessage(org.purpurmc.purpur.PurpurConfig.dontRunWithScissors); ++ } ++ // Purpur End ++ + this.player.checkMovementStatistics(this.player.getX() - d3, this.player.getY() - d4, this.player.getZ() - d5); + this.lastGoodX = this.player.getX(); + this.lastGoodY = this.player.getY(); +@@ -1642,6 +1688,15 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl + return false; + } + // Paper end - optimise out extra getCubes ++ ++ // Purpur start ++ public boolean isScissor(ItemStack stack) { ++ if (!stack.is(Items.SHEARS)) return false; ++ net.minecraft.world.item.component.CustomModelData customModelData = stack.get(net.minecraft.core.component.DataComponents.CUSTOM_MODEL_DATA); ++ return customModelData == null || customModelData.value() == 0; ++ } ++ // Purpur end ++ + private boolean isPlayerCollidingWithAnythingNew(LevelReader world, AABB box, double newX, double newY, double newZ) { + AABB axisalignedbb1 = this.player.getBoundingBox().move(newX - this.player.getX(), newY - this.player.getY(), newZ - this.player.getZ()); + Iterable iterable = world.getCollisions(this.player, axisalignedbb1.deflate(9.999999747378752E-6D)); +@@ -1652,7 +1707,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl + + do { + if (!iterator.hasNext()) { +- return false; ++ return !org.purpurmc.purpur.PurpurConfig.kickForOutOfOrderChat; // Purpur + } + + voxelshape1 = (VoxelShape) iterator.next(); +@@ -1990,6 +2045,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl + + boolean cancelled; + if (movingobjectposition == null || movingobjectposition.getType() != HitResult.Type.BLOCK) { ++ if (this.player.gameMode.shiftClickMended(itemstack)) return; // Purpur + org.bukkit.event.player.PlayerInteractEvent event = CraftEventFactory.callPlayerInteractEvent(this.player, Action.RIGHT_CLICK_AIR, itemstack, enumhand); + cancelled = event.useItemInHand() == Event.Result.DENY; + } else { +@@ -2464,7 +2520,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl + + public void handleCommand(String s) { // Paper - private -> public + org.spigotmc.AsyncCatcher.catchOp("Command Dispatched Async: " + s); // Paper - Add async catcher +- co.aikar.timings.MinecraftTimings.playerCommandTimer.startTiming(); // Paper ++ //co.aikar.timings.MinecraftTimings.playerCommandTimer.startTiming(); // Paper // Purpur + if ( org.spigotmc.SpigotConfig.logCommands ) // Spigot + this.LOGGER.info(this.player.getScoreboardName() + " issued server command: " + s); + +@@ -2474,7 +2530,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl + this.cserver.getPluginManager().callEvent(event); + + if (event.isCancelled()) { +- co.aikar.timings.MinecraftTimings.playerCommandTimer.stopTiming(); // Paper ++ //co.aikar.timings.MinecraftTimings.playerCommandTimer.stopTiming(); // Paper // Purpur + return; + } + +@@ -2487,7 +2543,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl + java.util.logging.Logger.getLogger(ServerGamePacketListenerImpl.class.getName()).log(java.util.logging.Level.SEVERE, null, ex); + return; + } finally { +- co.aikar.timings.MinecraftTimings.playerCommandTimer.stopTiming(); // Paper ++ //co.aikar.timings.MinecraftTimings.playerCommandTimer.stopTiming(); // Paper // Purpur + } + } + // CraftBukkit end +@@ -2774,6 +2830,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl + AABB axisalignedbb = entity.getBoundingBox(); + + if (this.player.canInteractWithEntity(axisalignedbb, 1.0D)) { ++ if (entity instanceof Mob mob) mob.ticksSinceLastInteraction = 0; // Purpur + packet.dispatch(new ServerboundInteractPacket.Handler() { + private void performInteraction(InteractionHand enumhand, ServerGamePacketListenerImpl.EntityInteraction playerconnection_a, PlayerInteractEntityEvent event) { // CraftBukkit + ItemStack itemstack = ServerGamePacketListenerImpl.this.player.getItemInHand(enumhand); +@@ -2787,6 +2844,8 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl + + ServerGamePacketListenerImpl.this.cserver.getPluginManager().callEvent(event); + ++ player.processClick(enumhand); // Purpur ++ + // Entity in bucket - SPIGOT-4048 and SPIGOT-6859a + if ((entity instanceof Bucketable && entity instanceof LivingEntity && origItem != null && origItem.asItem() == Items.WATER_BUCKET) && (event.isCancelled() || ServerGamePacketListenerImpl.this.player.getInventory().getSelected() == null || ServerGamePacketListenerImpl.this.player.getInventory().getSelected().getItem() != origItem)) { + entity.resendPossiblyDesyncedEntityData(ServerGamePacketListenerImpl.this.player); // Paper - The entire mob gets deleted, so resend it. +diff --git a/src/main/java/net/minecraft/server/network/ServerLoginPacketListenerImpl.java b/src/main/java/net/minecraft/server/network/ServerLoginPacketListenerImpl.java +index b656741eb68adeb04bf995f1045902cb6bd5f2e7..3b4fadb37eafb2f7b0ce4d6b276d2fdaa8287521 100644 +--- a/src/main/java/net/minecraft/server/network/ServerLoginPacketListenerImpl.java ++++ b/src/main/java/net/minecraft/server/network/ServerLoginPacketListenerImpl.java +@@ -315,7 +315,7 @@ public class ServerLoginPacketListenerImpl implements ServerLoginPacketListener, + ServerLoginPacketListenerImpl.LOGGER.warn("Failed to verify username but will let them in anyway!"); + ServerLoginPacketListenerImpl.this.startClientVerification(ServerLoginPacketListenerImpl.this.createOfflineProfile(s1)); // Spigot + } else { +- ServerLoginPacketListenerImpl.this.disconnect(Component.translatable("multiplayer.disconnect.unverified_username")); ++ ServerLoginPacketListenerImpl.this.disconnect(org.purpurmc.purpur.PurpurConfig.unverifiedUsername.equals("default") ? Component.translatable("multiplayer.disconnect.unverified_username") : io.papermc.paper.adventure.PaperAdventure.asVanilla(net.kyori.adventure.text.minimessage.MiniMessage.miniMessage().deserialize(org.purpurmc.purpur.PurpurConfig.unverifiedUsername))); // Purpur + ServerLoginPacketListenerImpl.LOGGER.error("Username '{}' tried to join with an invalid session", s1); + } + } catch (AuthenticationUnavailableException authenticationunavailableexception) { +diff --git a/src/main/java/net/minecraft/server/network/ServerStatusPacketListenerImpl.java b/src/main/java/net/minecraft/server/network/ServerStatusPacketListenerImpl.java +index 6f1c9fa89e718cbc01a8d72de34154f49c5f46db..2455f8e9679914660ec4fcd081138dabfe9c225b 100644 +--- a/src/main/java/net/minecraft/server/network/ServerStatusPacketListenerImpl.java ++++ b/src/main/java/net/minecraft/server/network/ServerStatusPacketListenerImpl.java +@@ -153,6 +153,7 @@ public class ServerStatusPacketListenerImpl implements ServerStatusPacketListene + this.connection.send(new ClientboundStatusResponsePacket(ping)); + // CraftBukkit end + */ ++ if (MinecraftServer.getServer().getStatus().version().isEmpty()) return; // Purpur - do not respond to pings before we know the protocol version + com.destroystokyo.paper.network.StandardPaperServerListPingEventImpl.processRequest(MinecraftServer.getServer(), this.connection); + // Paper end + } +diff --git a/src/main/java/net/minecraft/server/packs/resources/ResourceManagerReloadListener.java b/src/main/java/net/minecraft/server/packs/resources/ResourceManagerReloadListener.java +index f14113eef226e906c0d21641e74a27471254909d..0c25f3ed0a8a538edc7cadd3476100c9b3631f7a 100644 +--- a/src/main/java/net/minecraft/server/packs/resources/ResourceManagerReloadListener.java ++++ b/src/main/java/net/minecraft/server/packs/resources/ResourceManagerReloadListener.java +@@ -16,11 +16,11 @@ public interface ResourceManagerReloadListener extends PreparableReloadListener + Executor applyExecutor + ) { + return synchronizer.wait(Unit.INSTANCE).thenRunAsync(() -> { +- applyProfiler.startTick(); +- applyProfiler.push("listener"); ++ //applyProfiler.startTick(); // Purpur ++ //applyProfiler.push("listener"); // Purpur + this.onResourceManagerReload(manager); +- applyProfiler.pop(); +- applyProfiler.endTick(); ++ //applyProfiler.pop(); // Purpur ++ //applyProfiler.endTick(); // Purpur + }, applyExecutor); + } + +diff --git a/src/main/java/net/minecraft/server/players/PlayerList.java b/src/main/java/net/minecraft/server/players/PlayerList.java +index a2142930b4d4b05987c90496fb9d733d99040aa0..b863f6fe65c796a1d3102cc3eddb5d6c5becd3ac 100644 +--- a/src/main/java/net/minecraft/server/players/PlayerList.java ++++ b/src/main/java/net/minecraft/server/players/PlayerList.java +@@ -486,6 +486,7 @@ public abstract class PlayerList { + scoreboard.addPlayerToTeam(player.getScoreboardName(), collideRuleTeam); + } + // Paper end - Configurable player collision ++ org.purpurmc.purpur.task.BossBarTask.addToAll(player); // Purpur + PlayerList.LOGGER.info("{}[{}] logged in with entity id {} at ([{}]{}, {}, {})", player.getName().getString(), s1, player.getId(), worldserver1.serverLevelData.getLevelName(), player.getX(), player.getY(), player.getZ()); + } + +@@ -597,6 +598,7 @@ public abstract class PlayerList { + } + public net.kyori.adventure.text.Component remove(ServerPlayer entityplayer, net.kyori.adventure.text.Component leaveMessage) { + // Paper end - Fix kick event leave message not being sent ++ org.purpurmc.purpur.task.BossBarTask.removeFromAll(entityplayer.getBukkitEntity()); // Purpur + ServerLevel worldserver = entityplayer.serverLevel(); + + entityplayer.awardStat(Stats.LEAVE_GAME); +@@ -752,7 +754,7 @@ public abstract class PlayerList { + event.disallow(PlayerLoginEvent.Result.KICK_BANNED, io.papermc.paper.adventure.PaperAdventure.asAdventure(ichatmutablecomponent)); // Paper - Adventure + } else { + // return this.players.size() >= this.maxPlayers && !this.canBypassPlayerLimit(gameprofile) ? IChatBaseComponent.translatable("multiplayer.disconnect.server_full") : null; +- if (this.players.size() >= this.maxPlayers && !this.canBypassPlayerLimit(gameprofile)) { ++ if (this.players.size() >= this.maxPlayers && !(player.hasPermission("purpur.joinfullserver") || this.canBypassPlayerLimit(gameprofile))) { // Purpur + event.disallow(PlayerLoginEvent.Result.KICK_FULL, net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection().deserialize(org.spigotmc.SpigotConfig.serverFullMessage)); // Spigot // Paper - Adventure + } + } +@@ -1063,6 +1065,20 @@ public abstract class PlayerList { + } + // CraftBukkit end + ++ // Purpur Start ++ public void broadcastMiniMessage(@Nullable String message, boolean overlay) { ++ if (message != null && !message.isEmpty()) { ++ this.broadcastMessage(net.kyori.adventure.text.minimessage.MiniMessage.miniMessage().deserialize(message), overlay); ++ } ++ } ++ ++ public void broadcastMessage(@Nullable net.kyori.adventure.text.Component message, boolean overlay) { ++ if (message != null) { ++ this.broadcastSystemMessage(io.papermc.paper.adventure.PaperAdventure.asVanilla(message), overlay); ++ } ++ } ++ // Purpur end ++ + public void broadcastAll(Packet packet, ResourceKey dimension) { + Iterator iterator = this.players.iterator(); + +@@ -1166,6 +1182,7 @@ public abstract class PlayerList { + } else { + b0 = (byte) (24 + permissionLevel); + } ++ if (b0 < 28 && player.getBukkitEntity().hasPermission("purpur.debug.f3n")) b0 = 28; // Purpur + + player.connection.send(new ClientboundEntityEventPacket(player, b0)); + } +@@ -1174,6 +1191,27 @@ public abstract class PlayerList { + player.getBukkitEntity().recalculatePermissions(); // CraftBukkit + this.server.getCommands().sendCommands(player); + } // Paper - Add sendOpLevel API ++ ++ // Purpur start ++ if (org.purpurmc.purpur.PurpurConfig.enderChestSixRows && org.purpurmc.purpur.PurpurConfig.enderChestPermissionRows) { ++ org.bukkit.craftbukkit.entity.CraftHumanEntity bukkit = player.getBukkitEntity(); ++ if (bukkit.hasPermission("purpur.enderchest.rows.six")) { ++ player.sixRowEnderchestSlotCount = 54; ++ } else if (bukkit.hasPermission("purpur.enderchest.rows.five")) { ++ player.sixRowEnderchestSlotCount = 45; ++ } else if (bukkit.hasPermission("purpur.enderchest.rows.four")) { ++ player.sixRowEnderchestSlotCount = 36; ++ } else if (bukkit.hasPermission("purpur.enderchest.rows.three")) { ++ player.sixRowEnderchestSlotCount = 27; ++ } else if (bukkit.hasPermission("purpur.enderchest.rows.two")) { ++ player.sixRowEnderchestSlotCount = 18; ++ } else if (bukkit.hasPermission("purpur.enderchest.rows.one")) { ++ player.sixRowEnderchestSlotCount = 9; ++ } ++ } else { ++ player.sixRowEnderchestSlotCount = -1; ++ } ++ //Purpur end + } + + public boolean isWhiteListed(GameProfile profile) { +@@ -1235,7 +1273,7 @@ public abstract class PlayerList { + + public void saveAll(int interval) { + io.papermc.paper.util.MCUtil.ensureMain("Save Players" , () -> { // Paper - Ensure main +- MinecraftTimings.savePlayers.startTiming(); // Paper ++ //MinecraftTimings.savePlayers.startTiming(); // Paper // Purpur + int numSaved = 0; + long now = MinecraftServer.currentTick; + for (int i = 0; i < this.players.size(); ++i) { +@@ -1246,7 +1284,7 @@ public abstract class PlayerList { + } + // Paper end - Incremental chunk and player saving + } +- MinecraftTimings.savePlayers.stopTiming(); // Paper ++ //MinecraftTimings.savePlayers.stopTiming(); // Paper // Purpur + return null; }); // Paper - ensure main + } + +diff --git a/src/main/java/net/minecraft/server/players/SleepStatus.java b/src/main/java/net/minecraft/server/players/SleepStatus.java +index 823efad652d8ff9e96b99375b102fef6f017716e..caa8a69bde0c212c36dd990a67836ac2f95548c0 100644 +--- a/src/main/java/net/minecraft/server/players/SleepStatus.java ++++ b/src/main/java/net/minecraft/server/players/SleepStatus.java +@@ -19,7 +19,7 @@ public class SleepStatus { + + public boolean areEnoughDeepSleeping(int percentage, List players) { + // CraftBukkit start +- int j = (int) players.stream().filter((eh) -> { return eh.isSleepingLongEnough() || eh.fauxSleeping; }).count(); ++ int j = (int) players.stream().filter((eh) -> { return eh.isSleepingLongEnough() || eh.fauxSleeping || (eh.level().purpurConfig.idleTimeoutCountAsSleeping && eh.isAfk()); }).count(); // Purpur + boolean anyDeepSleep = players.stream().anyMatch(Player::isSleepingLongEnough); + + return anyDeepSleep && j >= this.sleepersNeeded(percentage); +@@ -52,7 +52,7 @@ public class SleepStatus { + + if (!entityplayer.isSpectator()) { + ++this.activePlayers; +- if (entityplayer.isSleeping() || entityplayer.fauxSleeping) { // CraftBukkit ++ if ((entityplayer.isSleeping() || entityplayer.fauxSleeping) || (entityplayer.level().purpurConfig.idleTimeoutCountAsSleeping && entityplayer.isAfk())) { // CraftBukkit // Purpur + ++this.sleepingPlayers; + } + // CraftBukkit start +diff --git a/src/main/java/net/minecraft/stats/ServerRecipeBook.java b/src/main/java/net/minecraft/stats/ServerRecipeBook.java +index 4103ddf16164e3992fef0765d368282572537e29..a0cb49233b1dbf53ce9d1bcc52b8967829d0530e 100644 +--- a/src/main/java/net/minecraft/stats/ServerRecipeBook.java ++++ b/src/main/java/net/minecraft/stats/ServerRecipeBook.java +@@ -125,6 +125,7 @@ public class ServerRecipeBook extends RecipeBook { + Optional> optional = recipeManager.byKey(minecraftkey); + + if (optional.isEmpty()) { ++ if (!org.purpurmc.purpur.PurpurConfig.loggerSuppressUnrecognizedRecipeErrors) // Purpur + ServerRecipeBook.LOGGER.error("Tried to load unrecognized recipe: {} removed now.", minecraftkey); + } else { + handler.accept((RecipeHolder) optional.get()); +diff --git a/src/main/java/net/minecraft/util/StringUtil.java b/src/main/java/net/minecraft/util/StringUtil.java +index 0bd191acb9596d3aa21c337230d26f09d26f6888..20211f40aeeade9217ece087688974bdf55afc56 100644 +--- a/src/main/java/net/minecraft/util/StringUtil.java ++++ b/src/main/java/net/minecraft/util/StringUtil.java +@@ -69,6 +69,7 @@ public class StringUtil { + + // Paper start - Username validation + public static boolean isReasonablePlayerName(final String name) { ++ if (true) return org.purpurmc.purpur.PurpurConfig.usernameValidCharactersPattern.matcher(name).matches(); // Purpur + if (name.isEmpty() || name.length() > 16) { + return false; + } +diff --git a/src/main/java/net/minecraft/util/profiling/ActiveProfiler.java b/src/main/java/net/minecraft/util/profiling/ActiveProfiler.java +index bce2dac613d29083dd5fbb68739304cc5a6d4d27..600a7036b503f60cc9c95f189f73c2dbf020e2e1 100644 +--- a/src/main/java/net/minecraft/util/profiling/ActiveProfiler.java ++++ b/src/main/java/net/minecraft/util/profiling/ActiveProfiler.java +@@ -55,7 +55,7 @@ public class ActiveProfiler implements ProfileCollector { + this.started = true; + this.path = ""; + this.paths.clear(); +- this.push("root"); ++ //this.push("root"); // Purpur + } + } + +@@ -64,7 +64,7 @@ public class ActiveProfiler implements ProfileCollector { + if (!this.started) { + LOGGER.error("Profiler tick already ended - missing startTick()?"); + } else { +- this.pop(); ++ //this.pop(); // Purpur + this.started = false; + if (!this.path.isEmpty()) { + LOGGER.error( +@@ -93,7 +93,7 @@ public class ActiveProfiler implements ProfileCollector { + + @Override + public void push(Supplier locationGetter) { +- this.push(locationGetter.get()); ++ //this.push(locationGetter.get()); // Purpur + } + + @Override +@@ -132,14 +132,14 @@ public class ActiveProfiler implements ProfileCollector { + + @Override + public void popPush(String location) { +- this.pop(); +- this.push(location); ++ //this.pop(); // Purpur ++ //this.push(location); // Purpur + } + + @Override + public void popPush(Supplier locationGetter) { +- this.pop(); +- this.push(locationGetter); ++ //this.pop(); // Purpur ++ //this.push(locationGetter); // Purpur + } + + private ActiveProfiler.PathEntry getCurrentEntry() { +diff --git a/src/main/java/net/minecraft/util/profiling/ProfilerFiller.java b/src/main/java/net/minecraft/util/profiling/ProfilerFiller.java +index a715ecf4a8ac91d3e5e5c6269d89e54b2c1cd279..223c3665126c576eddb1a8f7c9f5bc60c6ff9818 100644 +--- a/src/main/java/net/minecraft/util/profiling/ProfilerFiller.java ++++ b/src/main/java/net/minecraft/util/profiling/ProfilerFiller.java +@@ -6,32 +6,44 @@ import net.minecraft.util.profiling.metrics.MetricCategory; + public interface ProfilerFiller { + String ROOT = "root"; + ++ @io.papermc.paper.annotation.DoNotUse // Purpur + void startTick(); + ++ @io.papermc.paper.annotation.DoNotUse // Purpur + void endTick(); + ++ @io.papermc.paper.annotation.DoNotUse // Purpur + void push(String location); + ++ @io.papermc.paper.annotation.DoNotUse // Purpur + void push(Supplier locationGetter); + ++ @io.papermc.paper.annotation.DoNotUse // Purpur + void pop(); + ++ @io.papermc.paper.annotation.DoNotUse // Purpur + void popPush(String location); + ++ @io.papermc.paper.annotation.DoNotUse // Purpur + void popPush(Supplier locationGetter); + ++ @io.papermc.paper.annotation.DoNotUse // Purpur + void markForCharting(MetricCategory type); + ++ @io.papermc.paper.annotation.DoNotUse // Purpur + default void incrementCounter(String marker) { +- this.incrementCounter(marker, 1); ++ //this.incrementCounter(marker, 1); // Purpur + } + ++ @io.papermc.paper.annotation.DoNotUse // Purpur + void incrementCounter(String marker, int num); + ++ @io.papermc.paper.annotation.DoNotUse // Purpur + default void incrementCounter(Supplier markerGetter) { +- this.incrementCounter(markerGetter, 1); ++ //this.incrementCounter(markerGetter, 1); // Purpur + } + ++ @io.papermc.paper.annotation.DoNotUse // Purpur + void incrementCounter(Supplier markerGetter, int num); + + static ProfilerFiller tee(ProfilerFiller a, ProfilerFiller b) { +@@ -41,62 +53,62 @@ public interface ProfilerFiller { + return b == InactiveProfiler.INSTANCE ? a : new ProfilerFiller() { + @Override + public void startTick() { +- a.startTick(); +- b.startTick(); ++ //a.startTick(); // Purpur ++ //b.startTick(); // Purpur + } + + @Override + public void endTick() { +- a.endTick(); +- b.endTick(); ++ //a.endTick(); // Purpur ++ //b.endTick(); // Purpur + } + + @Override + public void push(String location) { +- a.push(location); +- b.push(location); ++ //a.push(location); // Purpur ++ //b.push(location); // Purpur + } + + @Override + public void push(Supplier locationGetter) { +- a.push(locationGetter); +- b.push(locationGetter); ++ //a.push(locationGetter); // Purpur ++ //b.push(locationGetter); // Purpur + } + + @Override + public void markForCharting(MetricCategory type) { +- a.markForCharting(type); +- b.markForCharting(type); ++ //a.markForCharting(type); // Purpur ++ //b.markForCharting(type); // Purpur + } + + @Override + public void pop() { +- a.pop(); +- b.pop(); ++ //a.pop(); // Purpur ++ //b.pop(); // Purpur + } + + @Override + public void popPush(String location) { +- a.popPush(location); +- b.popPush(location); ++ //a.popPush(location); // Purpur ++ //b.popPush(location); // Purpur + } + + @Override + public void popPush(Supplier locationGetter) { +- a.popPush(locationGetter); +- b.popPush(locationGetter); ++ //a.popPush(locationGetter); // Purpur ++ //b.popPush(locationGetter); // Purpur + } + + @Override + public void incrementCounter(String marker, int num) { +- a.incrementCounter(marker, num); +- b.incrementCounter(marker, num); ++ //a.incrementCounter(marker, num); // Purpur ++ //b.incrementCounter(marker, num); // Purpur + } + + @Override + public void incrementCounter(Supplier markerGetter, int num) { +- a.incrementCounter(markerGetter, num); +- b.incrementCounter(markerGetter, num); ++ //a.incrementCounter(markerGetter, num); // Purpur ++ //b.incrementCounter(markerGetter, num); // Purpur + } + }; + } +diff --git a/src/main/java/net/minecraft/world/damagesource/CombatRules.java b/src/main/java/net/minecraft/world/damagesource/CombatRules.java +index ddc880ac0c8378bc1132be5deba746c1484c941c..7a8e4b9a9f2e1e5a9c38ad330c75df1f880d3e8b 100644 +--- a/src/main/java/net/minecraft/world/damagesource/CombatRules.java ++++ b/src/main/java/net/minecraft/world/damagesource/CombatRules.java +@@ -12,7 +12,7 @@ public class CombatRules { + + public static float getDamageAfterAbsorb(float damage, DamageSource source, float armor, float armorToughnesss) { + float f = 2.0F + armorToughnesss / 4.0F; +- float g = Mth.clamp(armor - damage / f, armor * 0.2F, 20.0F); ++ float g = Mth.clamp(armor - damage / f, armor * 0.2F, org.purpurmc.purpur.PurpurConfig.limitArmor ? 20F : Float.MAX_VALUE); // Purpur + float h = g / 25.0F; + float i = EnchantmentHelper.calculateArmorBreach(source.getEntity(), h); + float j = 1.0F - i; +@@ -20,7 +20,7 @@ public class CombatRules { + } + + public static float getDamageAfterMagicAbsorb(float damageDealt, float protection) { +- float f = Mth.clamp(protection, 0.0F, 20.0F); ++ float f = Mth.clamp(protection, 0.0F, org.purpurmc.purpur.PurpurConfig.limitArmor ? 20F : Float.MAX_VALUE); // Purpur + return damageDealt * (1.0F - f / 25.0F); + } + } +diff --git a/src/main/java/net/minecraft/world/damagesource/CombatTracker.java b/src/main/java/net/minecraft/world/damagesource/CombatTracker.java +index 99a7e9eb75231c15bd8bb24fbb4e296bc9fdedff..4fb025a63628eb60509d90b680922a0220104bcb 100644 +--- a/src/main/java/net/minecraft/world/damagesource/CombatTracker.java ++++ b/src/main/java/net/minecraft/world/damagesource/CombatTracker.java +@@ -54,7 +54,7 @@ public class CombatTracker { + + private Component getMessageForAssistedFall(Entity attacker, Component attackerDisplayName, String itemDeathTranslationKey, String deathTranslationKey) { + ItemStack itemStack = attacker instanceof LivingEntity livingEntity ? livingEntity.getMainHandItem() : ItemStack.EMPTY; +- return !itemStack.isEmpty() && itemStack.has(DataComponents.CUSTOM_NAME) ++ return !itemStack.isEmpty() && (org.purpurmc.purpur.PurpurConfig.playerDeathsAlwaysShowItem || itemStack.has(DataComponents.CUSTOM_NAME)) // Purpur + ? Component.translatable(itemDeathTranslationKey, this.mob.getDisplayName(), attackerDisplayName, itemStack.getDisplayName()) + : Component.translatable(deathTranslationKey, this.mob.getDisplayName(), attackerDisplayName); + } +@@ -98,6 +98,13 @@ public class CombatTracker { + Component component = ComponentUtils.wrapInSquareBrackets(Component.translatable(string + ".link")).withStyle(INTENTIONAL_GAME_DESIGN_STYLE); + return Component.translatable(string + ".message", this.mob.getDisplayName(), component); + } else { ++ // Purpur start ++ if (damageSource.isScissors()) { ++ return damageSource.getLocalizedDeathMessage(org.purpurmc.purpur.PurpurConfig.deathMsgRunWithScissors, this.mob); ++ } else if (damageSource.isStonecutter()) { ++ return damageSource.getLocalizedDeathMessage(org.purpurmc.purpur.PurpurConfig.deathMsgStonecutter, this.mob); ++ } ++ // Purpur end + return damageSource.getLocalizedDeathMessage(this.mob); + } + } +diff --git a/src/main/java/net/minecraft/world/damagesource/DamageSource.java b/src/main/java/net/minecraft/world/damagesource/DamageSource.java +index 359a2f0492a9b938a4f015c546e100e0092ae1d4..25e614be19b2b29b36af136b823f27f85e1650fa 100644 +--- a/src/main/java/net/minecraft/world/damagesource/DamageSource.java ++++ b/src/main/java/net/minecraft/world/damagesource/DamageSource.java +@@ -29,6 +29,8 @@ public class DamageSource { + private boolean withSweep = false; + private boolean melting = false; + private boolean poison = false; ++ private boolean scissors = false; // Purpur ++ private boolean stonecutter = false; // Purpur + @Nullable + private Entity customEventDamager = null; // This field is a helper for when causing entity damage is not set by vanilla // Paper - fix DamageSource API + +@@ -59,6 +61,26 @@ public class DamageSource { + return this.poison; + } + ++ // Purpur start ++ public DamageSource scissors() { ++ this.scissors = true; ++ return this; ++ } ++ ++ public boolean isScissors() { ++ return this.scissors; ++ } ++ ++ public DamageSource stonecutter() { ++ this.stonecutter = true; ++ return this; ++ } ++ ++ public boolean isStonecutter() { ++ return this.stonecutter; ++ } ++ // Purpur end ++ + // Paper start - fix DamageSource API + public @Nullable Entity getCustomEventDamager() { + return (this.customEventDamager != null) ? this.customEventDamager : this.directEntity; +@@ -101,6 +123,8 @@ public class DamageSource { + damageSource.withSweep = this.isSweep(); + damageSource.poison = this.isPoison(); + damageSource.melting = this.isMelting(); ++ damageSource.scissors = this.isScissors(); // Purpur ++ damageSource.stonecutter = this.isStonecutter(); // Purpur + return damageSource; + } + // CraftBukkit end +@@ -173,10 +197,19 @@ public class DamageSource { + + ItemStack itemstack1 = itemstack; + +- return !itemstack1.isEmpty() && itemstack1.has(DataComponents.CUSTOM_NAME) ? Component.translatable(s + ".item", killed.getDisplayName(), ichatbasecomponent, itemstack1.getDisplayName()) : Component.translatable(s, killed.getDisplayName(), ichatbasecomponent); ++ return !itemstack1.isEmpty() && (org.purpurmc.purpur.PurpurConfig.playerDeathsAlwaysShowItem || itemstack1.has(DataComponents.CUSTOM_NAME)) ? Component.translatable(s + ".item", killed.getDisplayName(), ichatbasecomponent, itemstack1.getDisplayName()) : Component.translatable(s, killed.getDisplayName(), ichatbasecomponent); + } + } + ++ // Purpur start ++ public Component getLocalizedDeathMessage(String str, LivingEntity entity) { ++ net.kyori.adventure.text.Component name = io.papermc.paper.adventure.PaperAdventure.asAdventure(entity.getDisplayName()); ++ net.kyori.adventure.text.minimessage.tag.resolver.TagResolver template = net.kyori.adventure.text.minimessage.tag.resolver.Placeholder.component("player", name); ++ net.kyori.adventure.text.Component component = net.kyori.adventure.text.minimessage.MiniMessage.miniMessage().deserialize(str, template); ++ return io.papermc.paper.adventure.PaperAdventure.asVanilla(component); ++ } ++ // Purpur end ++ + public String getMsgId() { + return this.type().msgId(); + } +diff --git a/src/main/java/net/minecraft/world/damagesource/DamageSources.java b/src/main/java/net/minecraft/world/damagesource/DamageSources.java +index 349d1683458ec5d641c9823aa7a68dec8820a664..f0568c3d731afaf610ac2f45db53148d38338cdf 100644 +--- a/src/main/java/net/minecraft/world/damagesource/DamageSources.java ++++ b/src/main/java/net/minecraft/world/damagesource/DamageSources.java +@@ -44,11 +44,15 @@ public class DamageSources { + // CraftBukkit start + private final DamageSource melting; + private final DamageSource poison; ++ private final DamageSource scissors; // Purpur ++ private final DamageSource stonecutter; // Purpur + + public DamageSources(RegistryAccess registryManager) { + this.damageTypes = registryManager.registryOrThrow(Registries.DAMAGE_TYPE); + this.melting = this.source(DamageTypes.ON_FIRE).melting(); + this.poison = this.source(DamageTypes.MAGIC).poison(); ++ this.scissors = this.source(DamageTypes.MAGIC).scissors(); // Purpur ++ this.stonecutter = this.source(DamageTypes.MAGIC).stonecutter(); // Purpur + // CraftBukkit end + this.inFire = this.source(DamageTypes.IN_FIRE); + this.lightningBolt = this.source(DamageTypes.LIGHTNING_BOLT); +@@ -97,6 +101,15 @@ public class DamageSources { + } + // CraftBukkit end + ++ // Purpur start ++ public DamageSource scissors() { ++ return this.scissors; ++ } ++ public DamageSource stonecutter() { ++ return this.stonecutter; ++ } ++ // Purpur end ++ + public DamageSource inFire() { + return this.inFire; + } +diff --git a/src/main/java/net/minecraft/world/effect/HungerMobEffect.java b/src/main/java/net/minecraft/world/effect/HungerMobEffect.java +index a476b56ed98d0a1afc6a396ce29424df78f24ada..5119ff3414fbd9a1ae0a8db0fd15bd3c57c8e148 100644 +--- a/src/main/java/net/minecraft/world/effect/HungerMobEffect.java ++++ b/src/main/java/net/minecraft/world/effect/HungerMobEffect.java +@@ -12,7 +12,7 @@ class HungerMobEffect extends MobEffect { + @Override + public boolean applyEffectTick(LivingEntity entity, int amplifier) { + if (entity instanceof Player entityhuman) { +- entityhuman.causeFoodExhaustion(0.005F * (float) (amplifier + 1), org.bukkit.event.entity.EntityExhaustionEvent.ExhaustionReason.HUNGER_EFFECT); // CraftBukkit - EntityExhaustionEvent ++ entityhuman.causeFoodExhaustion(entity.level().purpurConfig.humanHungerExhaustionAmount * (float) (amplifier + 1), org.bukkit.event.entity.EntityExhaustionEvent.ExhaustionReason.HUNGER_EFFECT); // CraftBukkit - EntityExhaustionEvent // Purpur + } + + return true; +diff --git a/src/main/java/net/minecraft/world/effect/PoisonMobEffect.java b/src/main/java/net/minecraft/world/effect/PoisonMobEffect.java +index 3e7a703632251e0a5234259e3702b58b332e5ef0..f2cc43fbccb5d2ba012b350268065c2cfe014faf 100644 +--- a/src/main/java/net/minecraft/world/effect/PoisonMobEffect.java ++++ b/src/main/java/net/minecraft/world/effect/PoisonMobEffect.java +@@ -10,8 +10,8 @@ class PoisonMobEffect extends MobEffect { + + @Override + public boolean applyEffectTick(LivingEntity entity, int amplifier) { +- if (entity.getHealth() > 1.0F) { +- entity.hurt(entity.damageSources().poison(), 1.0F); // CraftBukkit - DamageSource.MAGIC -> CraftEventFactory.POISON ++ if (entity.getHealth() > entity.level().purpurConfig.entityMinimalHealthPoison) { // Purpur ++ entity.hurt(entity.damageSources().poison(), entity.level().purpurConfig.entityPoisonDegenerationAmount); // CraftBukkit - DamageSource.MAGIC -> CraftEventFactory.POISON // Purpur + } + + return true; +diff --git a/src/main/java/net/minecraft/world/effect/RegenerationMobEffect.java b/src/main/java/net/minecraft/world/effect/RegenerationMobEffect.java +index 4dba3e813e054951cbfbe0b323c1f5d973469cc0..426f61d55b9692cf085368df4e4df6f6997aa420 100644 +--- a/src/main/java/net/minecraft/world/effect/RegenerationMobEffect.java ++++ b/src/main/java/net/minecraft/world/effect/RegenerationMobEffect.java +@@ -11,7 +11,7 @@ class RegenerationMobEffect extends MobEffect { + @Override + public boolean applyEffectTick(LivingEntity entity, int amplifier) { + if (entity.getHealth() < entity.getMaxHealth()) { +- entity.heal(1.0F, org.bukkit.event.entity.EntityRegainHealthEvent.RegainReason.MAGIC_REGEN); // CraftBukkit ++ entity.heal(entity.level().purpurConfig.entityHealthRegenAmount, org.bukkit.event.entity.EntityRegainHealthEvent.RegainReason.MAGIC_REGEN); // CraftBukkit // Purpur + } + + return true; +diff --git a/src/main/java/net/minecraft/world/effect/SaturationMobEffect.java b/src/main/java/net/minecraft/world/effect/SaturationMobEffect.java +index 7b415dca88f50dc472fe4be96e5ef0996f117913..2bb872f29350d15db46b32c686aef78fc1b6fa29 100644 +--- a/src/main/java/net/minecraft/world/effect/SaturationMobEffect.java ++++ b/src/main/java/net/minecraft/world/effect/SaturationMobEffect.java +@@ -20,7 +20,7 @@ class SaturationMobEffect extends InstantenousMobEffect { + int oldFoodLevel = entityhuman.getFoodData().foodLevel; + org.bukkit.event.entity.FoodLevelChangeEvent event = CraftEventFactory.callFoodLevelChangeEvent(entityhuman, amplifier + 1 + oldFoodLevel); + if (!event.isCancelled()) { +- entityhuman.getFoodData().eat(event.getFoodLevel() - oldFoodLevel, 1.0F); ++ entityhuman.getFoodData().eat(event.getFoodLevel() - oldFoodLevel, entity.level().purpurConfig.humanSaturationRegenAmount); // Purpur + } + + ((CraftPlayer) entityhuman.getBukkitEntity()).sendHealthUpdate(); +diff --git a/src/main/java/net/minecraft/world/effect/WitherMobEffect.java b/src/main/java/net/minecraft/world/effect/WitherMobEffect.java +index f43bf280999ff3860cc702def50cc62b131eb1bd..66d9e99a351f5fc6cf58be3bee4397d92c932d64 100644 +--- a/src/main/java/net/minecraft/world/effect/WitherMobEffect.java ++++ b/src/main/java/net/minecraft/world/effect/WitherMobEffect.java +@@ -9,7 +9,7 @@ class WitherMobEffect extends MobEffect { + + @Override + public boolean applyEffectTick(LivingEntity entity, int amplifier) { +- entity.hurt(entity.damageSources().wither(), 1.0F); ++ entity.hurt(entity.damageSources().wither(), entity.level().purpurConfig.entityWitherDegenerationAmount); // Purpur + return true; + } + +diff --git a/src/main/java/net/minecraft/world/entity/Entity.java b/src/main/java/net/minecraft/world/entity/Entity.java +index 2bc85351e6e52f90da5fdb29d8d042a06132d742..3aae4fa4176c0bf170f4532ae187e3122c142a6a 100644 +--- a/src/main/java/net/minecraft/world/entity/Entity.java ++++ b/src/main/java/net/minecraft/world/entity/Entity.java +@@ -163,7 +163,7 @@ import org.bukkit.plugin.PluginManager; + // CraftBukkit end + + public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess, CommandSource, ScoreHolder { +- ++ public static javax.script.ScriptEngine scriptEngine = new javax.script.ScriptEngineManager().getEngineByName("rhino"); // Purpur + // CraftBukkit start + private static final int CURRENT_LEVEL = 2; + public boolean preserveMotion = true; // Paper - Fix Entity Teleportation and cancel velocity if teleported; keep initial motion on first setPositionRotation +@@ -340,6 +340,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + public double xOld; + public double yOld; + public double zOld; ++ public float maxUpStep; // Purpur + public boolean noPhysics; + public final RandomSource random; + public int tickCount; +@@ -381,7 +382,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + private final Set tags; + private final double[] pistonDeltas; + private long pistonDeltasGameTime; +- private EntityDimensions dimensions; ++ protected EntityDimensions dimensions; // Purpur - private -> protected + private float eyeHeight; + public boolean isInPowderSnow; + public boolean wasInPowderSnow; +@@ -426,6 +427,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + private UUID originWorld; + public boolean freezeLocked = false; // Paper - Freeze Tick Lock API + public boolean fixedPose = false; // Paper - Expand Pose API ++ public @Nullable Boolean immuneToFire = null; // Purpur - Fire immune API + + public void setOrigin(@javax.annotation.Nonnull Location location) { + this.origin = location.toVector(); +@@ -557,6 +559,25 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + return false; + } + ++ public boolean canSaveToDisk() { ++ return true; ++ } ++ ++ // Purpur start - copied from Mob ++ public boolean isSunBurnTick() { ++ if (this.level().isDay() && !this.level().isClientSide) { ++ float f = this.getLightLevelDependentMagicValue(); ++ BlockPos blockposition = BlockPos.containing(this.getX(), this.getEyeY(), this.getZ()); ++ boolean flag = this.isInWaterRainOrBubble() || this.isInPowderSnow || this.wasInPowderSnow; ++ ++ if (f > 0.5F && this.random.nextFloat() * 30.0F < (f - 0.4F) * 2.0F && !flag && this.level().canSeeSky(blockposition)) { ++ return true; ++ } ++ } ++ ++ return false; ++ } ++ + public final boolean hardCollides() { + return this.hardCollides; + } +@@ -577,7 +598,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + this.bb = Entity.INITIAL_AABB; + this.stuckSpeedMultiplier = Vec3.ZERO; + this.nextStep = 1.0F; +- this.random = SHARED_RANDOM; // Paper - Share random for entities to make them more random ++ this.random = world == null || world.purpurConfig.entitySharedRandom ? SHARED_RANDOM : RandomSource.create(); // Paper - Share random for entities to make them more random // Purpur + this.remainingFireTicks = -this.getFireImmuneTicks(); + this.fluidHeight = new Object2DoubleArrayMap(2); + this.fluidOnEyes = new HashSet(); +@@ -877,7 +898,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + // CraftBukkit end + + public void baseTick() { +- this.level().getProfiler().push("entityBaseTick"); ++ //this.level().getProfiler().push("entityBaseTick"); // Purpur + if (firstTick && this instanceof net.minecraft.world.entity.NeutralMob neutralMob) neutralMob.tickInitialPersistentAnger(level); // Paper - Prevent entity loading causing async lookups + this.inBlockState = null; + if (this.isPassenger() && this.getVehicle().isRemoved()) { +@@ -938,7 +959,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + } + + this.firstTick = false; +- this.level().getProfiler().pop(); ++ //this.level().getProfiler().pop(); // Purpur + } + + public void setSharedFlagOnFire(boolean onFire) { +@@ -947,10 +968,11 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + + public void checkBelowWorld() { + // Paper start - Configurable nether ceiling damage +- if (this.getY() < (double) (this.level.getMinBuildHeight() - 64) || (this.level.getWorld().getEnvironment() == org.bukkit.World.Environment.NETHER ++ if (this.getY() < (double) (this.level.getMinBuildHeight() + level().purpurConfig.voidDamageHeight) || (this.level.getWorld().getEnvironment() == org.bukkit.World.Environment.NETHER // Purpur + && this.level.paperConfig().environment.netherCeilingVoidDamageHeight.test(v -> this.getY() >= v) + && (!(this instanceof Player player) || !player.getAbilities().invulnerable))) { + // Paper end - Configurable nether ceiling damage ++ if (this.level().purpurConfig.teleportOnNetherCeilingDamage && this.level.getWorld().getEnvironment() == org.bukkit.World.Environment.NETHER && this instanceof ServerPlayer player) player.teleport(io.papermc.paper.util.MCUtil.toLocation(this.level, this.level.getSharedSpawnPos())); else // Purpur + this.onBelowWorld(); + } + +@@ -1155,7 +1177,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + } + } + +- this.level().getProfiler().push("move"); ++ //this.level().getProfiler().push("move"); // Purpur + if (this.stuckSpeedMultiplier.lengthSqr() > 1.0E-7D) { + movement = movement.multiply(this.stuckSpeedMultiplier); + this.stuckSpeedMultiplier = Vec3.ZERO; +@@ -1164,7 +1186,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + // Paper start - ignore movement changes while inactive. + if (isTemporarilyActive && !(this instanceof ItemEntity) && movement == getDeltaMovement() && movementType == MoverType.SELF) { + setDeltaMovement(Vec3.ZERO); +- this.level.getProfiler().pop(); ++ //this.level.getProfiler().pop(); // Purpur + return; + } + // Paper end +@@ -1185,8 +1207,8 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + this.setPos(this.getX() + vec3d1.x, this.getY() + vec3d1.y, this.getZ() + vec3d1.z); + } + +- this.level().getProfiler().pop(); +- this.level().getProfiler().push("rest"); ++ //this.level().getProfiler().pop(); // Purpur ++ //this.level().getProfiler().push("rest"); // Purpur + boolean flag = !Mth.equal(movement.x, vec3d1.x); + boolean flag1 = !Mth.equal(movement.z, vec3d1.z); + +@@ -1205,7 +1227,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + + this.checkFallDamage(vec3d1.y, this.onGround(), iblockdata, blockposition); + if (this.isRemoved()) { +- this.level().getProfiler().pop(); ++ //this.level().getProfiler().pop(); // Purpur + } else { + if (this.horizontalCollision) { + Vec3 vec3d2 = this.getDeltaMovement(); +@@ -1343,7 +1365,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + this.setRemainingFireTicks(-this.getFireImmuneTicks()); + } + +- this.level().getProfiler().pop(); ++ //this.level().getProfiler().pop(); // Purpur + } + } + // Paper start - detailed watchdog information +@@ -1858,7 +1880,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + } + + public boolean fireImmune() { +- return this.getType().fireImmune(); ++ return this.immuneToFire != null ? immuneToFire : this.getType().fireImmune(); // Purpur - add fire immune API + } + + public boolean causeFallDamage(float fallDistance, float damageMultiplier, DamageSource damageSource) { +@@ -1931,7 +1953,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + return this.isInWater() || flag; + } + +- void updateInWaterStateAndDoWaterCurrentPushing() { ++ public void updateInWaterStateAndDoWaterCurrentPushing() { // Purpur - package-private -> public + Entity entity = this.getVehicle(); + + if (entity instanceof Boat entityboat) { +@@ -2555,6 +2577,11 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + nbttagcompound.putBoolean("Paper.FreezeLock", true); + } + // Paper end ++ // Purpur start ++ if (immuneToFire != null) { ++ nbttagcompound.putBoolean("Purpur.FireImmune", immuneToFire); ++ } ++ // Purpur end + return nbttagcompound; + } catch (Throwable throwable) { + CrashReport crashreport = CrashReport.forThrowable(throwable, "Saving entity NBT"); +@@ -2702,6 +2729,11 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + freezeLocked = nbt.getBoolean("Paper.FreezeLock"); + } + // Paper end ++ // Purpur start ++ if (nbt.contains("Purpur.FireImmune")) { ++ immuneToFire = nbt.getBoolean("Purpur.FireImmune"); ++ } ++ // Purpur end + + } catch (Throwable throwable) { + CrashReport crashreport = CrashReport.forThrowable(throwable, "Loading entity NBT"); +@@ -3080,6 +3112,13 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + this.passengers = ImmutableList.copyOf(list); + } + ++ // Purpur start ++ if (isRidable() && this.passengers.get(0) == passenger && passenger instanceof Player player) { ++ onMount(player); ++ this.rider = player; ++ } ++ // Purpur end ++ + this.gameEvent(GameEvent.ENTITY_MOUNT, passenger); + } + } +@@ -3119,6 +3158,14 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + return false; + } + // CraftBukkit end ++ ++ // Purpur start ++ if (this.rider != null && this.passengers.get(0) == this.rider) { ++ onDismount(this.rider); ++ this.rider = null; ++ } ++ // Purpur end ++ + if (this.passengers.size() == 1 && this.passengers.get(0) == entity) { + this.passengers = ImmutableList.of(); + } else { +@@ -3197,12 +3244,15 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + return Vec3.directionFromRotation(this.getRotationVector()); + } + ++ public BlockPos portalPos = BlockPos.ZERO; // Purpur + public void handleInsidePortal(BlockPos pos) { + if (this.isOnPortalCooldown()) { ++ if (!(level().purpurConfig.playerFixStuckPortal && this instanceof Player && !pos.equals(portalPos))) // Purpur + this.setPortalCooldown(); +- } else { ++ } else if (level().purpurConfig.entitiesCanUsePortals || this instanceof ServerPlayer) { // Purpur + if (!this.level().isClientSide && !pos.equals(this.portalEntrancePos)) { + this.portalEntrancePos = pos.immutable(); ++ portalPos = BlockPos.ZERO; // Purpur + } + + this.isInsidePortal = true; +@@ -3220,7 +3270,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + ServerLevel worldserver1 = minecraftserver.getLevel(resourcekey); + + if (true && !this.isPassenger() && this.portalTime++ >= i) { // CraftBukkit +- this.level().getProfiler().push("portal"); ++ //this.level().getProfiler().push("portal"); // Purpur + this.portalTime = i; + // Paper start - Add EntityPortalReadyEvent + io.papermc.paper.event.entity.EntityPortalReadyEvent event = new io.papermc.paper.event.entity.EntityPortalReadyEvent(this.getBukkitEntity(), worldserver1 == null ? null : worldserver1.getWorld(), org.bukkit.PortalType.NETHER); +@@ -3238,7 +3288,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + } + } // Paper - Add EntityPortalReadyEvent + // CraftBukkit end +- this.level().getProfiler().pop(); ++ //this.level().getProfiler().pop(); // Purpur + } + + this.isInsidePortal = false; +@@ -3429,7 +3479,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + } + + public int getMaxAirSupply() { +- return this.maxAirTicks; // CraftBukkit - SPIGOT-6907: re-implement LivingEntity#setMaximumAir() ++ return this.level == null? this.maxAirTicks : this.level().purpurConfig.drowningAirTicks; // CraftBukkit - SPIGOT-6907: re-implement LivingEntity#setMaximumAir() // Purpur + } + + public int getAirSupply() { +@@ -3698,14 +3748,14 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + } + // Paper end - Fix item duplication and teleport issues + if (this.level() instanceof ServerLevel && !this.isRemoved()) { +- this.level().getProfiler().push("changeDimension"); ++ //this.level().getProfiler().push("changeDimension"); // Purpur + // CraftBukkit start + // this.unRide(); + if (worldserver == null) { + return null; + } + // CraftBukkit end +- this.level().getProfiler().push("reposition"); ++ //this.level().getProfiler().push("reposition"); // Purpur + PortalInfo shapedetectorshape = (location == null) ? this.findDimensionEntryPoint(worldserver) : new PortalInfo(new Vec3(location.x(), location.y(), location.z()), Vec3.ZERO, this.yRot, this.xRot, worldserver, null); // CraftBukkit + + if (shapedetectorshape == null) { +@@ -3744,7 +3794,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + this.unRide(); + // CraftBukkit end + +- this.level().getProfiler().popPush("reloading"); ++ //this.level().getProfiler().popPush("reloading"); // Purpur + // Paper start - Fix item duplication and teleport issues + if (this instanceof Mob) { + ((Mob) this).dropLeash(true, true); // Paper drop lead +@@ -3771,10 +3821,10 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + } + + this.removeAfterChangingDimensions(); +- this.level().getProfiler().pop(); ++ //this.level().getProfiler().pop(); // Purpur + ((ServerLevel) this.level()).resetEmptyTime(); + worldserver.resetEmptyTime(); +- this.level().getProfiler().pop(); ++ //this.level().getProfiler().pop(); // Purpur + return entity; + } + } else { +@@ -3894,7 +3944,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + } + + public boolean canChangeDimensions() { +- return !this.isPassenger() && !this.isVehicle() && isAlive() && valid; // Paper - Fix item duplication and teleport issues ++ return !this.isPassenger() && !this.isVehicle() && isAlive() && valid && (level().purpurConfig.entitiesCanUsePortals || this instanceof ServerPlayer); // Paper - Fix item duplication and teleport issues // Purpur + } + + public float getBlockExplosionResistance(Explosion explosion, BlockGetter world, BlockPos pos, BlockState blockState, FluidState fluidState, float max) { +@@ -4190,6 +4240,20 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + return SlotAccess.NULL; + } + ++ // Purpur Start ++ public void sendMiniMessage(@Nullable String message) { ++ if (message != null && !message.isEmpty()) { ++ this.sendMessage(net.kyori.adventure.text.minimessage.MiniMessage.miniMessage().deserialize(message)); ++ } ++ } ++ ++ public void sendMessage(@Nullable net.kyori.adventure.text.Component message) { ++ if (message != null) { ++ this.sendSystemMessage(io.papermc.paper.adventure.PaperAdventure.asVanilla(message)); ++ } ++ } ++ // Purpur end ++ + @Override + public void sendSystemMessage(Component message) {} + +@@ -4477,6 +4541,12 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + this.yRotO = this.getYRot(); + } + ++ // Purpur start ++ public AABB getAxisForFluidCheck() { ++ return this.getBoundingBox().deflate(0.001D); ++ } ++ // Purpur end ++ + public boolean updateFluidHeightAndDoFluidPushing(TagKey tag, double speed) { + if (this.touchingUnloadedChunk()) { + return false; +@@ -4823,7 +4893,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + } + + public float maxUpStep() { +- return 0.0F; ++ return maxUpStep; + } + + public void onExplosionHit(@Nullable Entity entity) {} +@@ -4995,4 +5065,44 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + return ((net.minecraft.server.level.ServerChunkCache) level.getChunkSource()).isPositionTicking(this); + } + // Paper end - Expose entity id counter ++ // Purpur start ++ @Nullable ++ private Player rider = null; ++ ++ @Nullable ++ public Player getRider() { ++ return rider; ++ } ++ ++ public boolean isRidable() { ++ return false; ++ } ++ ++ public boolean isControllable() { ++ return true; ++ } ++ ++ public void onMount(Player rider) { ++ if (this instanceof Mob) { ++ ((Mob) this).setTarget(null, null, false); ++ ((Mob) this).getNavigation().stop(); ++ } ++ rider.setJumping(false); // fixes jump on mount ++ } ++ ++ public void onDismount(Player player) { ++ } ++ ++ public boolean onSpacebar() { ++ return false; ++ } ++ ++ public boolean onClick(InteractionHand hand) { ++ return false; ++ } ++ ++ public boolean processClick(InteractionHand hand) { ++ return false; ++ } ++ // Purpur end + } +diff --git a/src/main/java/net/minecraft/world/entity/EntitySelector.java b/src/main/java/net/minecraft/world/entity/EntitySelector.java +index d8cc5614502db7025349e085381b6b32ad32296a..f1b9e83206cc67e6ef29ebe088351b0aaa5eb349 100644 +--- a/src/main/java/net/minecraft/world/entity/EntitySelector.java ++++ b/src/main/java/net/minecraft/world/entity/EntitySelector.java +@@ -40,6 +40,7 @@ public final class EntitySelector { + return net.minecraft.util.Mth.clamp(serverPlayer.getStats().getValue(net.minecraft.stats.Stats.CUSTOM.get(net.minecraft.stats.Stats.TIME_SINCE_REST)), 1, Integer.MAX_VALUE) >= playerInsomniaTicks; + }; + // Paper end - Ability to control player's insomnia and phantoms ++ public static Predicate notAfk = (player) -> !player.isAfk(); // Purpur + + private EntitySelector() {} + // Paper start - Affects Spawning API +diff --git a/src/main/java/net/minecraft/world/entity/EntityType.java b/src/main/java/net/minecraft/world/entity/EntityType.java +index a46bf73c608641bf1f00fd55242de71a0f2ee06e..8120f39a9689dae1233b243b74825e9ff110eac3 100644 +--- a/src/main/java/net/minecraft/world/entity/EntityType.java ++++ b/src/main/java/net/minecraft/world/entity/EntityType.java +@@ -322,7 +322,8 @@ public class EntityType implements FeatureElement, EntityTypeT + private Component description; + @Nullable + private ResourceKey lootTable; +- private final EntityDimensions dimensions; ++ private EntityDimensions dimensions; // Purpur - remove final ++ public void setDimensions(EntityDimensions dimensions) { this.dimensions = dimensions; } // Purpur + private final float spawnDimensionsScale; + private final FeatureFlagSet requiredFeatures; + +@@ -330,6 +331,16 @@ public class EntityType implements FeatureElement, EntityTypeT + return (EntityType) Registry.register(BuiltInRegistries.ENTITY_TYPE, id, (EntityType) type.build(id)); // CraftBukkit - decompile error + } + ++ // Purpur start ++ public static EntityType getFromBukkitType(org.bukkit.entity.EntityType bukkitType) { ++ return getFromKey(new ResourceLocation(bukkitType.getKey().toString())); ++ } ++ ++ public static EntityType getFromKey(ResourceLocation location) { ++ return BuiltInRegistries.ENTITY_TYPE.get(location); ++ } ++ // Purpur end ++ + public static ResourceLocation getKey(EntityType type) { + return BuiltInRegistries.ENTITY_TYPE.getKey(type); + } +@@ -537,6 +548,16 @@ public class EntityType implements FeatureElement, EntityTypeT + return this.category; + } + ++ // Purpur start ++ public String getName() { ++ return BuiltInRegistries.ENTITY_TYPE.getKey(this).getPath(); ++ } ++ ++ public String getTranslatedName() { ++ return getDescription().getString(); ++ } ++ // Purpur end ++ + public String getDescriptionId() { + if (this.descriptionId == null) { + this.descriptionId = Util.makeDescriptionId("entity", BuiltInRegistries.ENTITY_TYPE.getKey(this)); +@@ -604,6 +625,12 @@ public class EntityType implements FeatureElement, EntityTypeT + entity.load(nbt); + }, () -> { + EntityType.LOGGER.warn("Skipping Entity with id {}", nbt.getString("id")); ++ // Purpur start - log skipped entity's position ++ try { ++ ListTag pos = nbt.getList("Pos", 6); ++ EntityType.LOGGER.warn("Location: {} {},{},{}", world.getWorld().getName(), pos.getDouble(0), pos.getDouble(1), pos.getDouble(2)); ++ } catch (Throwable ignore) {} ++ // Purpur end + }); + } + +diff --git a/src/main/java/net/minecraft/world/entity/ExperienceOrb.java b/src/main/java/net/minecraft/world/entity/ExperienceOrb.java +index a207a31d80a302dbdfe80f8727222542d3a78da2..f5debc8ddc496cd3e2d8b253511ee5cc9a723b38 100644 +--- a/src/main/java/net/minecraft/world/entity/ExperienceOrb.java ++++ b/src/main/java/net/minecraft/world/entity/ExperienceOrb.java +@@ -320,7 +320,7 @@ public class ExperienceOrb extends Entity { + public void playerTouch(Player player) { + if (!this.level().isClientSide) { + if (player.takeXpDelay == 0 && new com.destroystokyo.paper.event.player.PlayerPickupExperienceEvent(((net.minecraft.server.level.ServerPlayer) player).getBukkitEntity(), (org.bukkit.entity.ExperienceOrb) this.getBukkitEntity()).callEvent()) { // Paper - PlayerPickupExperienceEvent +- player.takeXpDelay = CraftEventFactory.callPlayerXpCooldownEvent(player, 2, PlayerExpCooldownChangeEvent.ChangeReason.PICKUP_ORB).getNewCooldown(); // CraftBukkit - entityhuman.takeXpDelay = 2; ++ player.takeXpDelay = CraftEventFactory.callPlayerXpCooldownEvent(player, this.level().purpurConfig.playerExpPickupDelay, PlayerExpCooldownChangeEvent.ChangeReason.PICKUP_ORB).getNewCooldown(); // CraftBukkit - entityhuman.takeXpDelay = 2; // Purpur + player.take(this, 1); + int i = this.repairPlayerItems(player, this.value); + +@@ -338,7 +338,7 @@ public class ExperienceOrb extends Entity { + } + + private int repairPlayerItems(Player player, int amount) { +- Entry entry = EnchantmentHelper.getRandomItemWith(Enchantments.MENDING, player, ItemStack::isDamaged); ++ Entry entry = level().purpurConfig.useBetterMending ? EnchantmentHelper.getMostDamagedEquipment(Enchantments.MENDING, player) : EnchantmentHelper.getRandomItemWith(Enchantments.MENDING, player, ItemStack::isDamaged); // Purpur + + if (entry != null) { + ItemStack itemstack = (ItemStack) entry.getValue(); +@@ -366,13 +366,15 @@ public class ExperienceOrb extends Entity { + } + } + ++ // Purpur start + public int durabilityToXp(int repairAmount) { +- return repairAmount / 2; ++ return (int) (repairAmount / (2 * level().purpurConfig.mendingMultiplier)); + } + + public int xpToDurability(int experienceAmount) { +- return experienceAmount * 2; ++ return (int) ((experienceAmount * 2) * level().purpurConfig.mendingMultiplier); + } ++ // Purpur end + + public int getValue() { + return this.value; +diff --git a/src/main/java/net/minecraft/world/entity/GlowSquid.java b/src/main/java/net/minecraft/world/entity/GlowSquid.java +index 09fdea983772612ef3fff6b2da3cf469a34e4ec0..3e2ea26c23e88c395856b65001f2895db6a52bd4 100644 +--- a/src/main/java/net/minecraft/world/entity/GlowSquid.java ++++ b/src/main/java/net/minecraft/world/entity/GlowSquid.java +@@ -23,6 +23,39 @@ public class GlowSquid extends Squid { + super(type, world); + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level().purpurConfig.glowSquidRidable; ++ } ++ ++ ++ @Override ++ public boolean isControllable() { ++ return level().purpurConfig.glowSquidControllable; ++ } ++ // Purpur end ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(net.minecraft.world.entity.ai.attributes.Attributes.MAX_HEALTH).setBaseValue(this.level().purpurConfig.glowSquidMaxHealth); ++ } ++ ++ @Override ++ public boolean canFly() { ++ return this.level().purpurConfig.glowSquidsCanFly; ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level().purpurConfig.glowSquidTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level().purpurConfig.glowSquidAlwaysDropExp; ++ } ++ + @Override + protected ParticleOptions getInkParticle() { + return ParticleTypes.GLOW_SQUID_INK; +diff --git a/src/main/java/net/minecraft/world/entity/LivingEntity.java b/src/main/java/net/minecraft/world/entity/LivingEntity.java +index 6e043457a29a890bcefd27fc5bb07c1a7e4e30f7..fa698cfefccdddf5e5e9938a2959004c70f743a7 100644 +--- a/src/main/java/net/minecraft/world/entity/LivingEntity.java ++++ b/src/main/java/net/minecraft/world/entity/LivingEntity.java +@@ -229,9 +229,9 @@ public abstract class LivingEntity extends Entity implements Attackable { + protected int deathScore; + public float lastHurt; + public boolean jumping; +- public float xxa; +- public float yya; +- public float zza; ++ public float xxa; public float getStrafeMot() { return xxa; } public void setStrafeMot(float strafe) { xxa = strafe; } // Purpur - OBFHELPER ++ public float yya; public float getVerticalMot() { return yya; } public void setVerticalMot(float vertical) { yya = vertical; } // Purpur - OBFHELPER ++ public float zza; public float getForwardMot() { return zza; } public void setForwardMot(float forward) { zza = forward; } // Purpur - OBFHELPER + protected int lerpSteps; + protected double lerpX; + protected double lerpY; +@@ -274,6 +274,7 @@ public abstract class LivingEntity extends Entity implements Attackable { + public org.bukkit.craftbukkit.entity.CraftLivingEntity getBukkitLivingEntity() { return (org.bukkit.craftbukkit.entity.CraftLivingEntity) super.getBukkitEntity(); } // Paper + public boolean silentDeath = false; // Paper - mark entity as dying silently for cancellable death event + public net.kyori.adventure.util.TriState frictionState = net.kyori.adventure.util.TriState.NOT_SET; // Paper - Friction API ++ protected boolean shouldBurnInDay = false; public boolean shouldBurnInDay() { return this.shouldBurnInDay; } public void setShouldBurnInDay(boolean shouldBurnInDay) { this.shouldBurnInDay = shouldBurnInDay; } // Purpur + + @Override + public float getBukkitYaw() { +@@ -300,7 +301,8 @@ public abstract class LivingEntity extends Entity implements Attackable { + this.useItem = ItemStack.EMPTY; + this.lastClimbablePos = Optional.empty(); + this.appliedScale = 1.0F; +- this.attributes = new AttributeMap(DefaultAttributes.getSupplier(type)); ++ this.attributes = new AttributeMap(DefaultAttributes.getSupplier(type), this); // Purpur ++ this.initAttributes(); // Purpur + this.craftAttributes = new CraftAttributeMap(this.attributes); // CraftBukkit + // CraftBukkit - setHealth(getMaxHealth()) inlined and simplified to skip the instanceof check for EntityPlayer, as getBukkitEntity() is not initialized in constructor + this.entityData.set(LivingEntity.DATA_HEALTH_ID, (float) this.getAttribute(Attributes.MAX_HEALTH).getValue()); +@@ -315,6 +317,8 @@ public abstract class LivingEntity extends Entity implements Attackable { + this.brain = this.makeBrain(new Dynamic(dynamicopsnbt, (Tag) dynamicopsnbt.createMap((Map) ImmutableMap.of(dynamicopsnbt.createString("memories"), (Tag) dynamicopsnbt.emptyMap())))); + } + ++ protected void initAttributes() {}// Purpur ++ + public Brain getBrain() { + return this.brain; + } +@@ -350,6 +354,7 @@ public abstract class LivingEntity extends Entity implements Attackable { + public static AttributeSupplier.Builder createLivingAttributes() { + return AttributeSupplier.builder().add(Attributes.MAX_HEALTH).add(Attributes.KNOCKBACK_RESISTANCE).add(Attributes.MOVEMENT_SPEED).add(Attributes.ARMOR).add(Attributes.ARMOR_TOUGHNESS).add(Attributes.MAX_ABSORPTION).add(Attributes.STEP_HEIGHT).add(Attributes.SCALE).add(Attributes.GRAVITY).add(Attributes.SAFE_FALL_DISTANCE).add(Attributes.FALL_DAMAGE_MULTIPLIER).add(Attributes.JUMP_STRENGTH); + } ++ public boolean shouldSendAttribute(Attribute attribute) { return true; } // Purpur + + @Override + protected void checkFallDamage(double heightDifference, boolean onGround, BlockState state, BlockPos landedPosition) { +@@ -418,7 +423,7 @@ public abstract class LivingEntity extends Entity implements Attackable { + } + + super.baseTick(); +- this.level().getProfiler().push("livingEntityBaseTick"); ++ //this.level().getProfiler().push("livingEntityBaseTick"); // Purpur + if (this.fireImmune() || this.level().isClientSide) { + this.clearFire(); + } +@@ -436,6 +441,7 @@ public abstract class LivingEntity extends Entity implements Attackable { + double d1 = this.level().getWorldBorder().getDamagePerBlock(); + + if (d1 > 0.0D) { ++ if (level().purpurConfig.teleportIfOutsideBorder && this instanceof ServerPlayer serverPlayer) { serverPlayer.teleport(io.papermc.paper.util.MCUtil.toLocation(level(), ((ServerLevel) level()).getSharedSpawnPos())); return; } // Purpur + this.hurt(this.damageSources().outOfBorder(), (float) Math.max(1, Mth.floor(-d0 * d1))); + } + } +@@ -447,7 +453,7 @@ public abstract class LivingEntity extends Entity implements Attackable { + + if (flag1) { + this.setAirSupply(this.decreaseAirSupply(this.getAirSupply())); +- if (this.getAirSupply() == -20) { ++ if (this.getAirSupply() == -this.level().purpurConfig.drowningDamageInterval) { // Purpur + this.setAirSupply(0); + Vec3 vec3d = this.getDeltaMovement(); + +@@ -459,7 +465,7 @@ public abstract class LivingEntity extends Entity implements Attackable { + this.level().addParticle(ParticleTypes.BUBBLE, this.getX() + d2, this.getY() + d3, this.getZ() + d4, vec3d.x, vec3d.y, vec3d.z); + } + +- this.hurt(this.damageSources().drown(), 2.0F); ++ this.hurt(this.damageSources().drown(), (float) this.level().purpurConfig.damageFromDrowning); // Purpur + } + } + +@@ -520,7 +526,7 @@ public abstract class LivingEntity extends Entity implements Attackable { + this.yHeadRotO = this.yHeadRot; + this.yRotO = this.getYRot(); + this.xRotO = this.getXRot(); +- this.level().getProfiler().pop(); ++ //this.level().getProfiler().pop(); // Purpur + } + + public boolean canSpawnSoulSpeedParticle() { +@@ -837,6 +843,7 @@ public abstract class LivingEntity extends Entity implements Attackable { + dataresult.resultOrPartial(logger::error).ifPresent((nbtbase) -> { + nbt.put("Brain", nbtbase); + }); ++ nbt.putBoolean("Purpur.ShouldBurnInDay", shouldBurnInDay); // Purpur + } + + @Override +@@ -924,6 +931,11 @@ public abstract class LivingEntity extends Entity implements Attackable { + this.brain = this.makeBrain(new Dynamic(NbtOps.INSTANCE, nbt.get("Brain"))); + } + ++ // Purpur start ++ if (nbt.contains("Purpur.ShouldBurnInDay")) { ++ shouldBurnInDay = nbt.getBoolean("Purpur.ShouldBurnInDay"); ++ } ++ // Purpur end + } + + // CraftBukkit start +@@ -1059,9 +1071,31 @@ public abstract class LivingEntity extends Entity implements Attackable { + ItemStack itemstack = this.getItemBySlot(EquipmentSlot.HEAD); + EntityType entitytypes = entity.getType(); + +- if (entitytypes == EntityType.SKELETON && itemstack.is(Items.SKELETON_SKULL) || entitytypes == EntityType.ZOMBIE && itemstack.is(Items.ZOMBIE_HEAD) || entitytypes == EntityType.PIGLIN && itemstack.is(Items.PIGLIN_HEAD) || entitytypes == EntityType.PIGLIN_BRUTE && itemstack.is(Items.PIGLIN_HEAD) || entitytypes == EntityType.CREEPER && itemstack.is(Items.CREEPER_HEAD)) { +- d0 *= 0.5D; ++ // Purpur start ++ if (entitytypes == EntityType.SKELETON && itemstack.is(Items.SKELETON_SKULL)) { ++ d0 *= entity.level().purpurConfig.skeletonHeadVisibilityPercent; ++ } ++ else if (entitytypes == EntityType.ZOMBIE && itemstack.is(Items.ZOMBIE_HEAD)) { ++ d0 *= entity.level().purpurConfig.zombieHeadVisibilityPercent; ++ } ++ else if (entitytypes == EntityType.CREEPER && itemstack.is(Items.CREEPER_HEAD)) { ++ d0 *= entity.level().purpurConfig.creeperHeadVisibilityPercent; ++ } ++ else if ((entitytypes == EntityType.PIGLIN || entitytypes == EntityType.PIGLIN_BRUTE) && itemstack.is(Items.PIGLIN_HEAD)) { ++ d0 *= entity.level().purpurConfig.piglinHeadVisibilityPercent; ++ } ++ // Purpur end ++ ++ // Purpur start ++ if (entity instanceof LivingEntity entityliving) { ++ if (entityliving.hasEffect(MobEffects.BLINDNESS)) { ++ int amplifier = entityliving.getEffect(MobEffects.BLINDNESS).getAmplifier(); ++ for (int i = 0; i < amplifier; i++) { ++ d0 *= this.level().purpurConfig.mobsBlindnessMultiplier; ++ } ++ } + } ++ // Purpur end + } + + return d0; +@@ -1120,6 +1154,7 @@ public abstract class LivingEntity extends Entity implements Attackable { + for (flag = false; iterator.hasNext(); flag = true) { + // CraftBukkit start + MobEffectInstance effect = (MobEffectInstance) iterator.next(); ++ if (cause == EntityPotionEffectEvent.Cause.MILK && !this.level().purpurConfig.milkClearsBeneficialEffects && effect.getEffect().value().isBeneficial()) continue; // Purpur + EntityPotionEffectEvent event = CraftEventFactory.callEntityPotionEffectChangeEvent(this, effect, null, cause, EntityPotionEffectEvent.Action.CLEARED); + if (event.isCancelled()) { + continue; +@@ -1527,13 +1562,13 @@ public abstract class LivingEntity extends Entity implements Attackable { + if (entity1 instanceof net.minecraft.world.entity.player.Player) { + net.minecraft.world.entity.player.Player entityhuman = (net.minecraft.world.entity.player.Player) entity1; + +- this.lastHurtByPlayerTime = 100; ++ this.lastHurtByPlayerTime = this.level().purpurConfig.mobLastHurtByPlayerTime; // Purpur + this.lastHurtByPlayer = entityhuman; + } else if (entity1 instanceof Wolf) { + Wolf entitywolf = (Wolf) entity1; + + if (entitywolf.isTame()) { +- this.lastHurtByPlayerTime = 100; ++ this.lastHurtByPlayerTime = this.level().purpurConfig.mobLastHurtByPlayerTime; // Purpur + LivingEntity entityliving2 = entitywolf.getOwner(); + + if (entityliving2 instanceof net.minecraft.world.entity.player.Player) { +@@ -1648,6 +1683,18 @@ public abstract class LivingEntity extends Entity implements Attackable { + } + } + ++ // Purpur start ++ if (level().purpurConfig.totemOfUndyingWorksInInventory && this instanceof ServerPlayer player && (itemstack == null || itemstack.getItem() != Items.TOTEM_OF_UNDYING) && player.getBukkitEntity().hasPermission("purpur.inventory_totem")) { ++ for (ItemStack item : player.getInventory().items) { ++ if (item.getItem() == Items.TOTEM_OF_UNDYING) { ++ itemstack1 = item; ++ itemstack = item.copy(); ++ break; ++ } ++ } ++ } ++ // Purpur end ++ + org.bukkit.inventory.EquipmentSlot handSlot = (hand != null) ? org.bukkit.craftbukkit.CraftEquipmentSlot.getHand(hand) : null; + EntityResurrectEvent event = new EntityResurrectEvent((org.bukkit.entity.LivingEntity) this.getBukkitEntity(), handSlot); + event.setCancelled(itemstack == null); +@@ -1814,7 +1861,7 @@ public abstract class LivingEntity extends Entity implements Attackable { + boolean flag = false; + + if (this.dead && adversary instanceof WitherBoss) { // Paper +- if (this.level().getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING)) { ++ if (this.level().purpurConfig.witherBypassMobGriefing || this.level().getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING)) { // Purpur + BlockPos blockposition = this.blockPosition(); + BlockState iblockdata = Blocks.WITHER_ROSE.defaultBlockState(); + +@@ -1860,6 +1907,7 @@ public abstract class LivingEntity extends Entity implements Attackable { + + this.dropEquipment(); // CraftBukkit - from below + if (this.shouldDropLoot() && this.level().getGameRules().getBoolean(GameRules.RULE_DOMOBLOOT)) { ++ if (!(source.is(net.minecraft.world.damagesource.DamageTypes.CRAMMING) && level().purpurConfig.disableDropsOnCrammingDeath)) { // Purpur + this.dropFromLootTable(source, flag); + // Paper start + final boolean prev = this.clearEquipmentSlots; +@@ -1868,6 +1916,7 @@ public abstract class LivingEntity extends Entity implements Attackable { + // Paper end + this.dropCustomDeathLoot(source, i, flag); + this.clearEquipmentSlots = prev; // Paper ++ } // Purpur + } + // CraftBukkit start - Call death event // Paper start - call advancement triggers with correct entity equipment + org.bukkit.event.entity.EntityDeathEvent deathEvent = CraftEventFactory.callEntityDeathEvent(this, this.drops, () -> { +@@ -2363,6 +2412,21 @@ public abstract class LivingEntity extends Entity implements Attackable { + } + } + ++ // Purpur start ++ if (damagesource.getEntity() instanceof net.minecraft.world.entity.player.Player player && damagesource.getEntity().level().purpurConfig.creativeOnePunch) { ++ if (player.isCreative()) { ++ double attackDamage; ++ net.minecraft.world.item.component.ItemAttributeModifiers itemattributemodifiers = player.getMainHandItem().getOrDefault(DataComponents.ATTRIBUTE_MODIFIERS, net.minecraft.world.item.component.ItemAttributeModifiers.EMPTY); ++ ++ attackDamage = itemattributemodifiers.compute(this.getAttributeBaseValue(Attributes.ATTACK_DAMAGE), EquipmentSlot.MAINHAND); ++ ++ if (attackDamage == 0) { ++ this.setHealth(0); ++ } ++ } ++ } ++ // Purpur end ++ + if (f > 0 || !human) { + if (human) { + // PAIL: Be sure to drag all this code from the EntityHuman subclass each update. +@@ -2586,7 +2650,7 @@ public abstract class LivingEntity extends Entity implements Attackable { + + @Override + protected void onBelowWorld() { +- this.hurt(this.damageSources().fellOutOfWorld(), 4.0F); ++ this.hurt(this.damageSources().fellOutOfWorld(), (float) level().purpurConfig.voidDamageDealt); // Purpur + } + + protected void updateSwingTime() { +@@ -2781,7 +2845,7 @@ public abstract class LivingEntity extends Entity implements Attackable { + } + + protected long lastJumpTime = 0L; // Paper - Prevent excessive velocity through repeated crits +- protected void jumpFromGround() { ++ public void jumpFromGround() { // Purpur - protected -> public + float f = this.getJumpPower(); + + if (f > 1.0E-5F) { +@@ -2941,6 +3005,7 @@ public abstract class LivingEntity extends Entity implements Attackable { + + if (f3 > 0.0F) { + this.playSound(this.getFallDamageSound((int) f3), 1.0F, 1.0F); ++ if (level().purpurConfig.elytraKineticDamage) // Purpur + this.hurt(this.damageSources().flyIntoWall(), f3); + } + } +@@ -3163,10 +3228,10 @@ public abstract class LivingEntity extends Entity implements Attackable { + } + + this.run += (f3 - this.run) * 0.3F; +- this.level().getProfiler().push("headTurn"); ++ //this.level().getProfiler().push("headTurn"); // Purpur + f2 = this.tickHeadTurn(f1, f2); +- this.level().getProfiler().pop(); +- this.level().getProfiler().push("rangeChecks"); ++ //this.level().getProfiler().pop(); // Purpur ++ //this.level().getProfiler().push("rangeChecks"); // Purpur + + // Paper start - stop large pitch and yaw changes from crashing the server + this.yRotO += Math.round((this.getYRot() - this.yRotO) / 360.0F) * 360.0F; +@@ -3178,7 +3243,7 @@ public abstract class LivingEntity extends Entity implements Attackable { + this.yHeadRotO += Math.round((this.yHeadRot - this.yHeadRotO) / 360.0F) * 360.0F; + // Paper end + +- this.level().getProfiler().pop(); ++ //this.level().getProfiler().pop(); // Purpur + this.animStep += f2; + if (this.isFallFlying()) { + ++this.fallFlyTicks; +@@ -3401,19 +3466,19 @@ public abstract class LivingEntity extends Entity implements Attackable { + } + + this.setDeltaMovement(d0, d1, d2); +- this.level().getProfiler().push("ai"); ++ //this.level().getProfiler().push("ai"); // Purpur + if (this.isImmobile()) { + this.jumping = false; + this.xxa = 0.0F; + this.zza = 0.0F; + } else if (this.isEffectiveAi()) { +- this.level().getProfiler().push("newAi"); ++ //this.level().getProfiler().push("newAi"); // Purpur + this.serverAiStep(); +- this.level().getProfiler().pop(); ++ //this.level().getProfiler().pop(); // Purpur + } + +- this.level().getProfiler().pop(); +- this.level().getProfiler().push("jump"); ++ //this.level().getProfiler().pop(); // Purpur ++ //this.level().getProfiler().push("jump"); // Purpur + if (this.jumping && this.isAffectedByFluids()) { + double d3; + +@@ -3440,8 +3505,8 @@ public abstract class LivingEntity extends Entity implements Attackable { + this.noJumpDelay = 0; + } + +- this.level().getProfiler().pop(); +- this.level().getProfiler().push("travel"); ++ //this.level().getProfiler().pop(); // Purpur ++ //this.level().getProfiler().push("travel"); // Purpur + this.xxa *= 0.98F; + this.zza *= 0.98F; + this.updateFallFlying(); +@@ -3466,8 +3531,8 @@ public abstract class LivingEntity extends Entity implements Attackable { + this.travel(vec3d1); + } + +- this.level().getProfiler().pop(); +- this.level().getProfiler().push("freezing"); ++ //this.level().getProfiler().pop(); // Purpur ++ //this.level().getProfiler().push("freezing"); // Purpur + if (!this.level().isClientSide && !this.isDeadOrDying() && !this.freezeLocked) { // Paper - Freeze Tick Lock API + int i = this.getTicksFrozen(); + +@@ -3484,18 +3549,20 @@ public abstract class LivingEntity extends Entity implements Attackable { + this.hurt(this.damageSources().freeze(), 1.0F); + } + +- this.level().getProfiler().pop(); +- this.level().getProfiler().push("push"); ++ //this.level().getProfiler().pop(); // Purpur ++ //this.level().getProfiler().push("push"); // Purpur + if (this.autoSpinAttackTicks > 0) { + --this.autoSpinAttackTicks; + this.checkAutoSpinAttack(axisalignedbb, this.getBoundingBox()); + } + + this.pushEntities(); +- this.level().getProfiler().pop(); ++ //this.level().getProfiler().pop(); // Purpur + // Paper start - Add EntityMoveEvent +- if (((ServerLevel) this.level()).hasEntityMoveEvent && !(this instanceof net.minecraft.world.entity.player.Player)) { +- if (this.xo != this.getX() || this.yo != this.getY() || this.zo != this.getZ() || this.yRotO != this.getYRot() || this.xRotO != this.getXRot()) { ++ // Purpur start ++ if (this.xo != this.getX() || this.yo != this.getY() || this.zo != this.getZ() || this.yRotO != this.getYRot() || this.xRotO != this.getXRot()) { ++ if (((ServerLevel) this.level()).hasEntityMoveEvent && !(this instanceof net.minecraft.world.entity.player.Player)) { ++ // Purpur end + Location from = new Location(this.level().getWorld(), this.xo, this.yo, this.zo, this.yRotO, this.xRotO); + Location to = new Location(this.level().getWorld(), this.getX(), this.getY(), this.getZ(), this.getYRot(), this.getXRot()); + io.papermc.paper.event.entity.EntityMoveEvent event = new io.papermc.paper.event.entity.EntityMoveEvent(this.getBukkitLivingEntity(), from, to.clone()); +@@ -3505,12 +3572,48 @@ public abstract class LivingEntity extends Entity implements Attackable { + this.absMoveTo(event.getTo().getX(), event.getTo().getY(), event.getTo().getZ(), event.getTo().getYaw(), event.getTo().getPitch()); + } + } ++ // Purpur start ++ if (getRider() != null) { ++ getRider().resetLastActionTime(); ++ if (((ServerLevel) level()).hasRidableMoveEvent && this instanceof Mob) { ++ Location from = new Location(level().getWorld(), xo, yo, zo, this.yRotO, this.xRotO); ++ Location to = new Location(level().getWorld(), getX(), getY(), getZ(), this.getYRot(), this.getXRot()); ++ org.purpurmc.purpur.event.entity.RidableMoveEvent event = new org.purpurmc.purpur.event.entity.RidableMoveEvent((org.bukkit.entity.Mob) getBukkitLivingEntity(), (Player) getRider().getBukkitEntity(), from, to.clone()); ++ if (!event.callEvent()) { ++ absMoveTo(from.getX(), from.getY(), from.getZ(), from.getYaw(), from.getPitch()); ++ } else if (!to.equals(event.getTo())) { ++ absMoveTo(to.getX(), to.getY(), to.getZ(), to.getYaw(), to.getPitch()); ++ } ++ } ++ } ++ // Purpur end + } + // Paper end - Add EntityMoveEvent + if (!this.level().isClientSide && this.isSensitiveToWater() && this.isInWaterRainOrBubble()) { + this.hurt(this.damageSources().drown(), 1.0F); + } + ++ // Purpur start - copied from Zombie ++ if (this.isAlive()) { ++ boolean flag = this.shouldBurnInDay() && this.isSunBurnTick(); ++ if (flag) { ++ ItemStack itemstack = this.getItemBySlot(EquipmentSlot.HEAD); ++ if (!itemstack.isEmpty()) { ++ if (itemstack.isDamageableItem()) { ++ itemstack.setDamageValue(itemstack.getDamageValue() + this.random.nextInt(2)); ++ if (itemstack.getDamageValue() >= itemstack.getMaxDamage()) { ++ this.broadcastBreakEvent(EquipmentSlot.HEAD); ++ this.setItemSlot(EquipmentSlot.HEAD, ItemStack.EMPTY); ++ } ++ } ++ flag = false; ++ } ++ if (flag) { ++ this.igniteForSeconds(8); ++ } ++ } ++ } ++ // Purpur end + } + + public boolean isSensitiveToWater() { +@@ -3531,7 +3634,16 @@ public abstract class LivingEntity extends Entity implements Attackable { + int j = i / 10; + + if (j % 2 == 0) { +- itemstack.hurtAndBreak(1, this, EquipmentSlot.CHEST); ++ // Purpur start ++ int damage = level().purpurConfig.elytraDamagePerSecond; ++ if (level().purpurConfig.elytraDamageMultiplyBySpeed > 0) { ++ double speed = getDeltaMovement().lengthSqr(); ++ if (speed > level().purpurConfig.elytraDamageMultiplyBySpeed) { ++ damage *= (int) speed; ++ } ++ } ++ itemstack.hurtAndBreak(damage, this, EquipmentSlot.CHEST); ++ // Purpur end + } + + this.gameEvent(GameEvent.ELYTRA_GLIDE); +diff --git a/src/main/java/net/minecraft/world/entity/Mob.java b/src/main/java/net/minecraft/world/entity/Mob.java +index e89f9c3e887601d8461eb967ae0bf582b672f631..56da8a4600688efd1987d82d4fcad1757e33f4f2 100644 +--- a/src/main/java/net/minecraft/world/entity/Mob.java ++++ b/src/main/java/net/minecraft/world/entity/Mob.java +@@ -75,6 +75,7 @@ import net.minecraft.world.item.SpawnEggItem; + import net.minecraft.world.item.SwordItem; + import net.minecraft.world.item.component.ItemAttributeModifiers; + import net.minecraft.world.item.enchantment.EnchantmentHelper; ++import net.minecraft.world.item.enchantment.Enchantments; + import net.minecraft.world.level.GameRules; + import net.minecraft.world.level.ItemLike; + import net.minecraft.world.level.Level; +@@ -151,6 +152,7 @@ public abstract class Mob extends LivingEntity implements EquipmentUser, Targeti + private BlockPos restrictCenter; + private float restrictRadius; + ++ public int ticksSinceLastInteraction; // Purpur + public boolean aware = true; // CraftBukkit + + protected Mob(EntityType type, Level world) { +@@ -165,8 +167,8 @@ public abstract class Mob extends LivingEntity implements EquipmentUser, Targeti + this.restrictRadius = -1.0F; + this.goalSelector = new GoalSelector(world.getProfilerSupplier()); + this.targetSelector = new GoalSelector(world.getProfilerSupplier()); +- this.lookControl = new LookControl(this); +- this.moveControl = new MoveControl(this); ++ this.lookControl = new org.purpurmc.purpur.controller.LookControllerWASD(this); // Purpur ++ this.moveControl = new org.purpurmc.purpur.controller.MoveControllerWASD(this); // Purpur + this.jumpControl = new JumpControl(this); + this.bodyRotationControl = this.createBodyControl(); + this.navigation = this.createNavigation(world); +@@ -338,6 +340,7 @@ public abstract class Mob extends LivingEntity implements EquipmentUser, Targeti + entityliving = null; + } + } ++ if (entityliving instanceof ServerPlayer) this.ticksSinceLastInteraction = 0; // Purpur + this.target = entityliving; + return true; + // CraftBukkit end +@@ -373,15 +376,35 @@ public abstract class Mob extends LivingEntity implements EquipmentUser, Targeti + @Override + public void baseTick() { + super.baseTick(); +- this.level().getProfiler().push("mobBaseTick"); ++ //this.level().getProfiler().push("mobBaseTick"); // Purpur + if (this.isAlive() && this.random.nextInt(1000) < this.ambientSoundTime++) { + this.resetAmbientSoundTime(); + this.playAmbientSound(); + } + +- this.level().getProfiler().pop(); ++ //this.level().getProfiler().pop(); // Purpur ++ incrementTicksSinceLastInteraction(); // Purpur + } + ++ // Purpur start ++ private void incrementTicksSinceLastInteraction() { ++ ++this.ticksSinceLastInteraction; ++ if (getRider() != null) { ++ this.ticksSinceLastInteraction = 0; ++ return; ++ } ++ if (this.level().purpurConfig.entityLifeSpan <= 0) { ++ return; // feature disabled ++ } ++ if (!this.removeWhenFarAway(0) || isPersistenceRequired() || requiresCustomPersistence() || hasCustomName()) { ++ return; // mob persistent ++ } ++ if (this.ticksSinceLastInteraction > this.level().purpurConfig.entityLifeSpan) { ++ this.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.DISCARD); ++ } ++ } ++ // Purpur end ++ + @Override + protected void playHurtSound(DamageSource damageSource) { + this.resetAmbientSoundTime(); +@@ -584,6 +607,7 @@ public abstract class Mob extends LivingEntity implements EquipmentUser, Targeti + } + + nbt.putBoolean("Bukkit.Aware", this.aware); // CraftBukkit ++ nbt.putInt("Purpur.ticksSinceLastInteraction", this.ticksSinceLastInteraction); // Purpur + } + + @Override +@@ -668,6 +692,11 @@ public abstract class Mob extends LivingEntity implements EquipmentUser, Targeti + this.aware = nbt.getBoolean("Bukkit.Aware"); + } + // CraftBukkit end ++ // Purpur start ++ if (nbt.contains("Purpur.ticksSinceLastInteraction")) { ++ this.ticksSinceLastInteraction = nbt.getInt("Purpur.ticksSinceLastInteraction"); ++ } ++ // Purpur end + } + + @Override +@@ -718,8 +747,8 @@ public abstract class Mob extends LivingEntity implements EquipmentUser, Targeti + @Override + public void aiStep() { + super.aiStep(); +- this.level().getProfiler().push("looting"); +- if (!this.level().isClientSide && this.canPickUpLoot() && this.isAlive() && !this.dead && this.level().getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING)) { ++ //this.level().getProfiler().push("looting"); // Purpur ++ if (!this.level().isClientSide && this.canPickUpLoot() && this.isAlive() && !this.dead && (this.level().purpurConfig.entitiesPickUpLootBypassMobGriefing || this.level().getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING))) { + Vec3i baseblockposition = this.getPickupReach(); + List list = this.level().getEntitiesOfClass(ItemEntity.class, this.getBoundingBox().inflate((double) baseblockposition.getX(), (double) baseblockposition.getY(), (double) baseblockposition.getZ())); + Iterator iterator = list.iterator(); +@@ -738,7 +767,7 @@ public abstract class Mob extends LivingEntity implements EquipmentUser, Targeti + } + } + +- this.level().getProfiler().pop(); ++ //this.level().getProfiler().pop(); // Purpur + } + + protected Vec3i getPickupReach() { +@@ -963,44 +992,44 @@ public abstract class Mob extends LivingEntity implements EquipmentUser, Targeti + return; + } + // Paper end - Allow nerfed mobs to jump and float +- ProfilerFiller gameprofilerfiller = this.level().getProfiler(); ++ //ProfilerFiller gameprofilerfiller = this.level().getProfiler(); // Purpur + +- gameprofilerfiller.push("sensing"); ++ //gameprofilerfiller.push("sensing"); // Purpur + this.sensing.tick(); +- gameprofilerfiller.pop(); ++ //gameprofilerfiller.pop(); // Purpur + int i = this.tickCount + this.getId(); + + if (i % 2 != 0 && this.tickCount > 1) { +- gameprofilerfiller.push("targetSelector"); ++ //gameprofilerfiller.push("targetSelector"); // Purpur + this.targetSelector.tickRunningGoals(false); +- gameprofilerfiller.pop(); +- gameprofilerfiller.push("goalSelector"); ++ //gameprofilerfiller.pop(); // Purpur ++ //gameprofilerfiller.push("goalSelector"); // Purpur + this.goalSelector.tickRunningGoals(false); +- gameprofilerfiller.pop(); ++ //gameprofilerfiller.pop(); // Purpur + } else { +- gameprofilerfiller.push("targetSelector"); ++ //gameprofilerfiller.push("targetSelector"); // Purpur + this.targetSelector.tick(); +- gameprofilerfiller.pop(); +- gameprofilerfiller.push("goalSelector"); ++ //gameprofilerfiller.pop(); // Purpur ++ //gameprofilerfiller.push("goalSelector"); // Purpur + this.goalSelector.tick(); +- gameprofilerfiller.pop(); ++ //gameprofilerfiller.pop(); // Purpur + } + +- gameprofilerfiller.push("navigation"); ++ //gameprofilerfiller.push("navigation"); // Purpur + this.navigation.tick(); +- gameprofilerfiller.pop(); +- gameprofilerfiller.push("mob tick"); ++ //gameprofilerfiller.pop(); // Purpur ++ //gameprofilerfiller.push("mob tick"); // Purpur + this.customServerAiStep(); +- gameprofilerfiller.pop(); +- gameprofilerfiller.push("controls"); +- gameprofilerfiller.push("move"); ++ //gameprofilerfiller.pop(); // Purpur ++ //gameprofilerfiller.push("controls"); // Purpur ++ //gameprofilerfiller.push("move"); // Purpur + this.moveControl.tick(); +- gameprofilerfiller.popPush("look"); ++ //gameprofilerfiller.popPush("look"); // Purpur + this.lookControl.tick(); +- gameprofilerfiller.popPush("jump"); ++ //gameprofilerfiller.popPush("jump"); // Purpur + this.jumpControl.tick(); +- gameprofilerfiller.pop(); +- gameprofilerfiller.pop(); ++ //gameprofilerfiller.pop(); // Purpur ++ //gameprofilerfiller.pop(); // Purpur + this.sendDebugPackets(); + } + +@@ -1309,6 +1338,12 @@ public abstract class Mob extends LivingEntity implements EquipmentUser, Targeti + + } + ++ // Purpur start ++ public static @Nullable EquipmentSlot getSlotForDispenser(ItemStack itemstack) { ++ return EnchantmentHelper.getItemEnchantmentLevel(Enchantments.BINDING_CURSE, itemstack) > 0 ? null : getEquipmentSlotForItem(itemstack); ++ } ++ // Purpur end ++ + @Nullable + public static Item getEquipmentForSlot(EquipmentSlot equipmentSlot, int equipmentLevel) { + switch (equipmentSlot) { +@@ -1403,7 +1438,7 @@ public abstract class Mob extends LivingEntity implements EquipmentUser, Targeti + RandomSource randomsource = world.getRandom(); + + this.getAttribute(Attributes.FOLLOW_RANGE).addPermanentModifier(new AttributeModifier("Random spawn bonus", randomsource.triangle(0.0D, 0.11485000000000001D), AttributeModifier.Operation.ADD_MULTIPLIED_BASE)); +- this.setLeftHanded(randomsource.nextFloat() < 0.05F); ++ this.setLeftHanded(randomsource.nextFloat() < world.getLevel().purpurConfig.entityLeftHandedChance); // Purpur + return entityData; + } + +@@ -1450,6 +1485,7 @@ public abstract class Mob extends LivingEntity implements EquipmentUser, Targeti + if (!this.isAlive()) { + return InteractionResult.PASS; + } else if (this.getLeashHolder() == player) { ++ if (hand == InteractionHand.OFF_HAND && (level().purpurConfig.villagerCanBeLeashed || level().purpurConfig.wanderingTraderCanBeLeashed) && this instanceof net.minecraft.world.entity.npc.AbstractVillager) return InteractionResult.CONSUME; // Purpur + // CraftBukkit start - fire PlayerUnleashEntityEvent + // Paper start - Expand EntityUnleashEvent + org.bukkit.event.player.PlayerUnleashEntityEvent event = CraftEventFactory.callPlayerUnleashEntityEvent(this, player, hand, !player.hasInfiniteMaterials()); +@@ -1525,7 +1561,7 @@ public abstract class Mob extends LivingEntity implements EquipmentUser, Targeti + protected void onOffspringSpawnedFromEgg(Player player, Mob child) {} + + protected InteractionResult mobInteract(Player player, InteractionHand hand) { +- return InteractionResult.PASS; ++ return tryRide(player, hand); // Purpur + } + + public boolean isWithinRestriction() { +@@ -1840,21 +1876,12 @@ public abstract class Mob extends LivingEntity implements EquipmentUser, Targeti + this.setLastHurtMob(target); + } + ++ if (target instanceof ServerPlayer) this.ticksSinceLastInteraction = 0; // Purpur + return flag; + } + + public boolean isSunBurnTick() { +- if (this.level().isDay() && !this.level().isClientSide) { +- float f = this.getLightLevelDependentMagicValue(); +- BlockPos blockposition = BlockPos.containing(this.getX(), this.getEyeY(), this.getZ()); +- boolean flag = this.isInWaterRainOrBubble() || this.isInPowderSnow || this.wasInPowderSnow; +- +- if (f > 0.5F && this.random.nextFloat() * 30.0F < (f - 0.4F) * 2.0F && !flag && this.level().canSeeSky(blockposition)) { +- return true; +- } +- } +- +- return false; ++ return super.isSunBurnTick(); + } + + @Override +@@ -1902,4 +1929,56 @@ public abstract class Mob extends LivingEntity implements EquipmentUser, Targeti + + return itemmonsteregg == null ? null : new ItemStack(itemmonsteregg); + } ++ ++ // Purpur start ++ public double getMaxY() { ++ return level().getHeight(); ++ } ++ ++ public InteractionResult tryRide(Player player, InteractionHand hand) { ++ return tryRide(player, hand, InteractionResult.PASS); ++ } ++ ++ public InteractionResult tryRide(Player player, InteractionHand hand, InteractionResult result) { ++ if (!isRidable()) { ++ return result; ++ } ++ if (hand != InteractionHand.MAIN_HAND) { ++ return InteractionResult.PASS; ++ } ++ if (player.isShiftKeyDown()) { ++ return InteractionResult.PASS; ++ } ++ if (!player.getItemInHand(hand).isEmpty()) { ++ return InteractionResult.PASS; ++ } ++ if (!passengers.isEmpty() || player.isPassenger()) { ++ return InteractionResult.PASS; ++ } ++ if (this instanceof TamableAnimal tamable) { ++ if (tamable.isTame() && !tamable.isOwnedBy(player)) { ++ return InteractionResult.PASS; ++ } ++ if (!tamable.isTame() && !level().purpurConfig.untamedTamablesAreRidable) { ++ return InteractionResult.PASS; ++ } ++ } ++ if (this instanceof AgeableMob ageable) { ++ if (ageable.isBaby() && !level().purpurConfig.babiesAreRidable) { ++ return InteractionResult.PASS; ++ } ++ } ++ if (!player.getBukkitEntity().hasPermission("allow.ride." + net.minecraft.core.registries.BuiltInRegistries.ENTITY_TYPE.getKey(getType()).getPath())) { ++ player.sendMiniMessage(org.purpurmc.purpur.PurpurConfig.cannotRideMob); ++ return InteractionResult.PASS; ++ } ++ player.setYRot(this.getYRot()); ++ player.setXRot(this.getXRot()); ++ if (player.startRiding(this)) { ++ return InteractionResult.SUCCESS; ++ } else { ++ return InteractionResult.PASS; ++ } ++ } ++ // Purpur end + } +diff --git a/src/main/java/net/minecraft/world/entity/Shearable.java b/src/main/java/net/minecraft/world/entity/Shearable.java +index 2ee48ac3b665db2b02bcb1a30ec972d43a3725b0..59e8f5431ce5026209e1428b5fa5b5485dcfebc7 100644 +--- a/src/main/java/net/minecraft/world/entity/Shearable.java ++++ b/src/main/java/net/minecraft/world/entity/Shearable.java +@@ -8,7 +8,7 @@ public interface Shearable { + + boolean readyForShearing(); + // Paper start - custom shear drops; ensure all implementing entities override this +- default java.util.List generateDefaultDrops() { ++ default java.util.List generateDefaultDrops(int looting) { // Purpur + return java.util.Collections.emptyList(); + } + // Paper end - custom shear drops +diff --git a/src/main/java/net/minecraft/world/entity/ai/attributes/AttributeMap.java b/src/main/java/net/minecraft/world/entity/ai/attributes/AttributeMap.java +index 9ef8f014af332da129bfcd3370da983ec035ecc6..c51b429822d56761f69c49ecd4addfab7b90bad8 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/attributes/AttributeMap.java ++++ b/src/main/java/net/minecraft/world/entity/ai/attributes/AttributeMap.java +@@ -22,13 +22,20 @@ public class AttributeMap { + private final Map, AttributeInstance> attributes = new Object2ObjectOpenHashMap<>(); + private final Set dirtyAttributes = new ObjectOpenHashSet<>(); + private final AttributeSupplier supplier; ++ private final net.minecraft.world.entity.LivingEntity entity; // Purpur + + public AttributeMap(AttributeSupplier defaultAttributes) { ++ // Purpur start ++ this(defaultAttributes, null); ++ } ++ public AttributeMap(AttributeSupplier defaultAttributes, net.minecraft.world.entity.LivingEntity entity) { ++ this.entity = entity; ++ // Purpur end + this.supplier = defaultAttributes; + } + + private void onAttributeModified(AttributeInstance instance) { +- if (instance.getAttribute().value().isClientSyncable()) { ++ if (instance.getAttribute().value().isClientSyncable() && (entity == null || entity.shouldSendAttribute(instance.getAttribute().value()))) { // Purpur + this.dirtyAttributes.add(instance); + } + } +@@ -38,7 +45,7 @@ public class AttributeMap { + } + + public Collection getSyncableAttributes() { +- return this.attributes.values().stream().filter(attribute -> attribute.getAttribute().value().isClientSyncable()).collect(Collectors.toList()); ++ return this.attributes.values().stream().filter(attribute -> attribute.getAttribute().value().isClientSyncable() && (entity == null || entity.shouldSendAttribute(attribute.getAttribute().value()))).collect(Collectors.toList()); // Purpur + } + + @Nullable +diff --git a/src/main/java/net/minecraft/world/entity/ai/attributes/DefaultAttributes.java b/src/main/java/net/minecraft/world/entity/ai/attributes/DefaultAttributes.java +index 10a1434313b11dae8210484583c6bf3b627416f7..35af18f371b3beaf81fcdca79fefe85e0a862b50 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/attributes/DefaultAttributes.java ++++ b/src/main/java/net/minecraft/world/entity/ai/attributes/DefaultAttributes.java +@@ -129,7 +129,7 @@ public class DefaultAttributes { + .put(EntityType.OCELOT, Ocelot.createAttributes().build()) + .put(EntityType.PANDA, Panda.createAttributes().build()) + .put(EntityType.PARROT, Parrot.createAttributes().build()) +- .put(EntityType.PHANTOM, Monster.createMonsterAttributes().build()) ++ .put(EntityType.PHANTOM, net.minecraft.world.entity.monster.Phantom.createAttributes().build()) // Purpur + .put(EntityType.PIG, Pig.createAttributes().build()) + .put(EntityType.PIGLIN, Piglin.createAttributes().build()) + .put(EntityType.PIGLIN_BRUTE, PiglinBrute.createAttributes().build()) +diff --git a/src/main/java/net/minecraft/world/entity/ai/attributes/RangedAttribute.java b/src/main/java/net/minecraft/world/entity/ai/attributes/RangedAttribute.java +index f0703302e7dbbda88de8c648d20d87c55ed9b1e0..a913ebabaa5f443afa987b972355a8f8d1723c78 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/attributes/RangedAttribute.java ++++ b/src/main/java/net/minecraft/world/entity/ai/attributes/RangedAttribute.java +@@ -29,6 +29,7 @@ public class RangedAttribute extends Attribute { + + @Override + public double sanitizeValue(double value) { ++ if (!org.purpurmc.purpur.PurpurConfig.clampAttributes) return Double.isNaN(value) ? this.minValue : value; // Purpur + return Double.isNaN(value) ? this.minValue : Mth.clamp(value, this.minValue, this.maxValue); + } + } +diff --git a/src/main/java/net/minecraft/world/entity/ai/behavior/AcquirePoi.java b/src/main/java/net/minecraft/world/entity/ai/behavior/AcquirePoi.java +index e1b6fe9ecda25f86431baf414f1bfd3a26a8b2bd..6499e3fe49e453db11e51eaf717ca8b3b682056b 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/behavior/AcquirePoi.java ++++ b/src/main/java/net/minecraft/world/entity/ai/behavior/AcquirePoi.java +@@ -73,7 +73,7 @@ public class AcquirePoi { + }; + // Paper start - optimise POI access + java.util.List, BlockPos>> poiposes = new java.util.ArrayList<>(); +- io.papermc.paper.util.PoiAccess.findNearestPoiPositions(poiManager, poiPredicate, predicate2, entity.blockPosition(), 48, 48*48, PoiManager.Occupancy.HAS_SPACE, false, 5, poiposes); ++ io.papermc.paper.util.PoiAccess.findNearestPoiPositions(poiManager, poiPredicate, predicate2, entity.blockPosition(), world.purpurConfig.villagerAcquirePoiSearchRadius, world.purpurConfig.villagerAcquirePoiSearchRadius*world.purpurConfig.villagerAcquirePoiSearchRadius, PoiManager.Occupancy.HAS_SPACE, false, 5, poiposes); // Purpur + Set, BlockPos>> set = new java.util.HashSet<>(poiposes); + // Paper end - optimise POI access + Path path = findPathToPois(entity, set); +diff --git a/src/main/java/net/minecraft/world/entity/ai/behavior/Behavior.java b/src/main/java/net/minecraft/world/entity/ai/behavior/Behavior.java +index 9379dd4056018b52c93ed4888dcdc94579bd9691..612a14806ec63b0dcf31814396282f4b7f4a527c 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/behavior/Behavior.java ++++ b/src/main/java/net/minecraft/world/entity/ai/behavior/Behavior.java +@@ -59,9 +59,9 @@ public abstract class Behavior implements BehaviorContro + this.status = Behavior.Status.RUNNING; + int i = this.minDuration + world.getRandom().nextInt(this.maxDuration + 1 - this.minDuration); + this.endTimestamp = time + (long)i; +- this.timing.startTiming(); // Paper - behavior timings ++ //this.timing.startTiming(); // Paper - behavior timings // Purpur + this.start(world, entity, time); +- this.timing.stopTiming(); // Paper - behavior timings ++ //this.timing.stopTiming(); // Paper - behavior timings // Purpur + return true; + } else { + return false; +@@ -73,13 +73,13 @@ public abstract class Behavior implements BehaviorContro + + @Override + public final void tickOrStop(ServerLevel world, E entity, long time) { +- this.timing.startTiming(); // Paper - behavior timings ++ //this.timing.startTiming(); // Paper - behavior timings // Purpur + if (!this.timedOut(time) && this.canStillUse(world, entity, time)) { + this.tick(world, entity, time); + } else { + this.doStop(world, entity, time); + } +- this.timing.stopTiming(); // Paper - behavior timings ++ //this.timing.stopTiming(); // Paper - behavior timings // Purpur + } + + protected void tick(ServerLevel world, E entity, long time) { +diff --git a/src/main/java/net/minecraft/world/entity/ai/behavior/HarvestFarmland.java b/src/main/java/net/minecraft/world/entity/ai/behavior/HarvestFarmland.java +index 2ade08d1466660ee1787fa97908002ef56389712..8d4e206aa05b95b7bfec5d23496085cf55a3e1de 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/behavior/HarvestFarmland.java ++++ b/src/main/java/net/minecraft/world/entity/ai/behavior/HarvestFarmland.java +@@ -41,17 +41,19 @@ public class HarvestFarmland extends Behavior { + private long nextOkStartTime; + private int timeWorkedSoFar; + private final List validFarmlandAroundVillager = Lists.newArrayList(); ++ private boolean clericWartFarmer = false; // Purpur + + public HarvestFarmland() { + super(ImmutableMap.of(MemoryModuleType.LOOK_TARGET, MemoryStatus.VALUE_ABSENT, MemoryModuleType.WALK_TARGET, MemoryStatus.VALUE_ABSENT, MemoryModuleType.SECONDARY_JOB_SITE, MemoryStatus.VALUE_PRESENT)); + } + + protected boolean checkExtraStartConditions(ServerLevel world, Villager entity) { +- if (!world.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING)) { ++ if (!world.purpurConfig.villagerBypassMobGriefing && !world.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING)) { // Purpur + return false; +- } else if (entity.getVillagerData().getProfession() != VillagerProfession.FARMER) { ++ } else if (entity.getVillagerData().getProfession() != VillagerProfession.FARMER && !(world.purpurConfig.villagerClericsFarmWarts && entity.getVillagerData().getProfession() == VillagerProfession.CLERIC)) { // Purpur + return false; + } else { ++ if (!this.clericWartFarmer && entity.getVillagerData().getProfession() == VillagerProfession.CLERIC) this.clericWartFarmer = true; // Purpur + BlockPos.MutableBlockPos blockposition_mutableblockposition = entity.blockPosition().mutable(); + + this.validFarmlandAroundVillager.clear(); +@@ -82,6 +84,7 @@ public class HarvestFarmland extends Behavior { + Block block = iblockdata.getBlock(); + Block block1 = world.getBlockState(pos.below()).getBlock(); + ++ if (this.clericWartFarmer) return block == Blocks.NETHER_WART && iblockdata.getValue(net.minecraft.world.level.block.NetherWartBlock.AGE) == 3 || iblockdata.isAir() && block1 == Blocks.SOUL_SAND; // Purpur + return block instanceof CropBlock && ((CropBlock) block).isMaxAge(iblockdata) || iblockdata.isAir() && block1 instanceof FarmBlock; + } + +@@ -107,20 +110,20 @@ public class HarvestFarmland extends Behavior { + Block block = iblockdata.getBlock(); + Block block1 = world.getBlockState(this.aboveFarmlandPos.below()).getBlock(); + +- if (block instanceof CropBlock && ((CropBlock) block).isMaxAge(iblockdata)) { ++ if (block instanceof CropBlock && ((CropBlock) block).isMaxAge(iblockdata) && !this.clericWartFarmer || this.clericWartFarmer && block == Blocks.NETHER_WART && iblockdata.getValue(net.minecraft.world.level.block.NetherWartBlock.AGE) == 3) { // Purpur + if (CraftEventFactory.callEntityChangeBlockEvent(entity, this.aboveFarmlandPos, iblockdata.getFluidState().createLegacyBlock())) { // CraftBukkit // Paper - fix wrong block state + world.destroyBlock(this.aboveFarmlandPos, true, entity); + } // CraftBukkit + } + +- if (iblockdata.isAir() && block1 instanceof FarmBlock && entity.hasFarmSeeds()) { ++ if (iblockdata.isAir() && (block1 instanceof FarmBlock && !this.clericWartFarmer || this.clericWartFarmer && block1 == Blocks.SOUL_SAND) && entity.hasFarmSeeds()) { // Purpur + SimpleContainer inventorysubcontainer = entity.getInventory(); + + for (int j = 0; j < inventorysubcontainer.getContainerSize(); ++j) { + ItemStack itemstack = inventorysubcontainer.getItem(j); + boolean flag = false; + +- if (!itemstack.isEmpty() && itemstack.is(ItemTags.VILLAGER_PLANTABLE_SEEDS)) { ++ if (!itemstack.isEmpty() && (itemstack.is(ItemTags.VILLAGER_PLANTABLE_SEEDS) || this.clericWartFarmer && itemstack.getItem() == net.minecraft.world.item.Items.NETHER_WART)) { + Item item = itemstack.getItem(); + + if (item instanceof BlockItem) { +@@ -136,7 +139,7 @@ public class HarvestFarmland extends Behavior { + } + + if (flag) { +- world.playSound((Player) null, (double) this.aboveFarmlandPos.getX(), (double) this.aboveFarmlandPos.getY(), (double) this.aboveFarmlandPos.getZ(), SoundEvents.CROP_PLANTED, SoundSource.BLOCKS, 1.0F, 1.0F); ++ world.playSound((Player) null, (double) this.aboveFarmlandPos.getX(), (double) this.aboveFarmlandPos.getY(), (double) this.aboveFarmlandPos.getZ(), this.clericWartFarmer ? SoundEvents.NETHER_WART_PLANTED : SoundEvents.CROP_PLANTED, SoundSource.BLOCKS, 1.0F, 1.0F); // Purpur + itemstack.shrink(1); + if (itemstack.isEmpty()) { + inventorysubcontainer.setItem(j, ItemStack.EMPTY); +diff --git a/src/main/java/net/minecraft/world/entity/ai/behavior/InteractWithDoor.java b/src/main/java/net/minecraft/world/entity/ai/behavior/InteractWithDoor.java +index 736f46d552d558bf0edd9a86601b5fbb6940815b..cf039181dfe0ddb3ccda44064a5d8a2f6c5c432c 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/behavior/InteractWithDoor.java ++++ b/src/main/java/net/minecraft/world/entity/ai/behavior/InteractWithDoor.java +@@ -57,7 +57,7 @@ public class InteractWithDoor { + + if (iblockdata.is(BlockTags.WOODEN_DOORS, (blockbase_blockdata) -> { + return blockbase_blockdata.getBlock() instanceof DoorBlock; +- })) { ++ }) && !DoorBlock.requiresRedstone(entityliving.level(), iblockdata, blockposition)) { // Purpur + DoorBlock blockdoor = (DoorBlock) iblockdata.getBlock(); + + if (!blockdoor.isOpen(iblockdata)) { +@@ -79,7 +79,7 @@ public class InteractWithDoor { + + if (iblockdata1.is(BlockTags.WOODEN_DOORS, (blockbase_blockdata) -> { + return blockbase_blockdata.getBlock() instanceof DoorBlock; +- })) { ++ }) && !DoorBlock.requiresRedstone(entityliving.level(), iblockdata, blockposition1)) { // Purpur + DoorBlock blockdoor1 = (DoorBlock) iblockdata1.getBlock(); + + if (!blockdoor1.isOpen(iblockdata1)) { +@@ -122,7 +122,7 @@ public class InteractWithDoor { + + if (!iblockdata.is(BlockTags.WOODEN_DOORS, (blockbase_blockdata) -> { + return blockbase_blockdata.getBlock() instanceof DoorBlock; +- })) { ++ }) || DoorBlock.requiresRedstone(entity.level(), iblockdata, blockposition)) { // Purpur + iterator.remove(); + } else { + DoorBlock blockdoor = (DoorBlock) iblockdata.getBlock(); +diff --git a/src/main/java/net/minecraft/world/entity/ai/behavior/ShowTradesToPlayer.java b/src/main/java/net/minecraft/world/entity/ai/behavior/ShowTradesToPlayer.java +index 18dad0825616c4167a0a7555689ee64910a87e09..6945992491027d43eca4f1ca697ad45ce06ded55 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/behavior/ShowTradesToPlayer.java ++++ b/src/main/java/net/minecraft/world/entity/ai/behavior/ShowTradesToPlayer.java +@@ -46,6 +46,7 @@ public class ShowTradesToPlayer extends Behavior { + + @Override + public boolean canStillUse(ServerLevel world, Villager entity, long time) { ++ if (!entity.level().purpurConfig.villagerDisplayTradeItem) return false; // Purpur + return this.checkExtraStartConditions(world, entity) + && this.lookTime > 0 + && entity.getBrain().getMemory(MemoryModuleType.INTERACTION_TARGET).isPresent(); +diff --git a/src/main/java/net/minecraft/world/entity/ai/behavior/TradeWithVillager.java b/src/main/java/net/minecraft/world/entity/ai/behavior/TradeWithVillager.java +index 8508ac7de8cda3127b73e11ff4aee62502e65ead..b1544e028d5a9b84b944e1fb5a12bb163067fb54 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/behavior/TradeWithVillager.java ++++ b/src/main/java/net/minecraft/world/entity/ai/behavior/TradeWithVillager.java +@@ -59,6 +59,12 @@ public class TradeWithVillager extends Behavior { + throwHalfStack(entity, ImmutableSet.of(Items.WHEAT), villager); + } + ++ // Purpur start ++ if (world.purpurConfig.villagerClericsFarmWarts && world.purpurConfig.villagerClericFarmersThrowWarts && entity.getVillagerData().getProfession() == VillagerProfession.CLERIC && entity.getInventory().countItem(Items.NETHER_WART) > Items.NETHER_WART.getDefaultMaxStackSize() / 2) { ++ throwHalfStack(entity, ImmutableSet.of(Items.NETHER_WART), villager); ++ } ++ // Purpur end ++ + if (!this.trades.isEmpty() && entity.getInventory().hasAnyOf(this.trades)) { + throwHalfStack(entity, this.trades, villager); + } +diff --git a/src/main/java/net/minecraft/world/entity/ai/behavior/VillagerGoalPackages.java b/src/main/java/net/minecraft/world/entity/ai/behavior/VillagerGoalPackages.java +index f000a6c1e61198e6dd06ae5f084d12fdf309f50a..3091d985ba9c55d404332576320718840538722e 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/behavior/VillagerGoalPackages.java ++++ b/src/main/java/net/minecraft/world/entity/ai/behavior/VillagerGoalPackages.java +@@ -52,8 +52,13 @@ public class VillagerGoalPackages { + } + + public static ImmutableList>> getWorkPackage(VillagerProfession profession, float speed) { ++ // Purpur start ++ return getWorkPackage(profession, speed, false); ++ } ++ public static ImmutableList>> getWorkPackage(VillagerProfession profession, float speed, boolean clericsFarmWarts) { ++ // Purpur end + WorkAtPoi workAtPoi; +- if (profession == VillagerProfession.FARMER) { ++ if (profession == VillagerProfession.FARMER || (clericsFarmWarts && profession == VillagerProfession.CLERIC)) { // Purpur + workAtPoi = new WorkAtComposter(); + } else { + workAtPoi = new WorkAtPoi(); +diff --git a/src/main/java/net/minecraft/world/entity/ai/behavior/VillagerMakeLove.java b/src/main/java/net/minecraft/world/entity/ai/behavior/VillagerMakeLove.java +index 0a608418f87b71d5d71706712e1f82da0d7e4d34..03e7ca83e4c28dfaa5b52bcb100bd542db105970 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/behavior/VillagerMakeLove.java ++++ b/src/main/java/net/minecraft/world/entity/ai/behavior/VillagerMakeLove.java +@@ -125,8 +125,10 @@ public class VillagerMakeLove extends Behavior { + return Optional.empty(); + } + // Move age setting down +- parent.setAge(6000); +- partner.setAge(6000); ++ // Purpur start ++ parent.setAge(world.purpurConfig.villagerBreedingTicks); ++ partner.setAge(world.purpurConfig.villagerBreedingTicks); ++ // Purpur end + world.addFreshEntityWithPassengers(entityvillager2, CreatureSpawnEvent.SpawnReason.BREEDING); + // CraftBukkit end + world.broadcastEntityEvent(entityvillager2, (byte) 12); +diff --git a/src/main/java/net/minecraft/world/entity/ai/control/MoveControl.java b/src/main/java/net/minecraft/world/entity/ai/control/MoveControl.java +index c8fd5696de7c3623cdb4f498190a5c2708cf843e..e403d9dfeeaa3dcf53be790d761e7e922419efb0 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/control/MoveControl.java ++++ b/src/main/java/net/minecraft/world/entity/ai/control/MoveControl.java +@@ -29,6 +29,20 @@ public class MoveControl implements Control { + this.mob = entity; + } + ++ // Purpur start ++ public void setSpeedModifier(double speed) { ++ this.speedModifier = speed; ++ } ++ ++ public void setForward(float forward) { ++ this.strafeForwards = forward; ++ } ++ ++ public void setStrafe(float strafe) { ++ this.strafeRight = strafe; ++ } ++ // Purpur end ++ + public boolean hasWanted() { + return this.operation == MoveControl.Operation.MOVE_TO; + } +diff --git a/src/main/java/net/minecraft/world/entity/ai/control/SmoothSwimmingLookControl.java b/src/main/java/net/minecraft/world/entity/ai/control/SmoothSwimmingLookControl.java +index fbfc2f2515ad709b2c1212aef9521e795547d66b..e77bd11af62682d5eca41f6c9e1aed30eb6879ce 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/control/SmoothSwimmingLookControl.java ++++ b/src/main/java/net/minecraft/world/entity/ai/control/SmoothSwimmingLookControl.java +@@ -3,7 +3,7 @@ package net.minecraft.world.entity.ai.control; + import net.minecraft.util.Mth; + import net.minecraft.world.entity.Mob; + +-public class SmoothSwimmingLookControl extends LookControl { ++public class SmoothSwimmingLookControl extends org.purpurmc.purpur.controller.LookControllerWASD { // Purpur + private final int maxYRotFromCenter; + private static final int HEAD_TILT_X = 10; + private static final int HEAD_TILT_Y = 20; +@@ -14,7 +14,7 @@ public class SmoothSwimmingLookControl extends LookControl { + } + + @Override +- public void tick() { ++ public void vanillaTick() { // Purpur + if (this.lookAtCooldown > 0) { + this.lookAtCooldown--; + this.getYRotD().ifPresent(yaw -> this.mob.yHeadRot = this.rotateTowards(this.mob.yHeadRot, yaw + 20.0F, this.yMaxRotSpeed)); +diff --git a/src/main/java/net/minecraft/world/entity/ai/goal/BreakDoorGoal.java b/src/main/java/net/minecraft/world/entity/ai/goal/BreakDoorGoal.java +index a85885ee51df585fa11ae9f8fcd67ff2a71c5a18..d81509e08e70ec5b2f837c9dc66b1254c86854e4 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/goal/BreakDoorGoal.java ++++ b/src/main/java/net/minecraft/world/entity/ai/goal/BreakDoorGoal.java +@@ -32,7 +32,7 @@ public class BreakDoorGoal extends DoorInteractGoal { + + @Override + public boolean canUse() { +- return !super.canUse() ? false : (!this.mob.level().getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING) ? false : this.isValidDifficulty(this.mob.level().getDifficulty()) && !this.isOpen()); ++ return !super.canUse() ? false : ((!this.mob.level().purpurConfig.zombieBypassMobGriefing && !this.mob.level().getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING)) ? false : this.isValidDifficulty(this.mob.level().getDifficulty()) && !this.isOpen()); // Purpur + } + + @Override +diff --git a/src/main/java/net/minecraft/world/entity/ai/goal/EatBlockGoal.java b/src/main/java/net/minecraft/world/entity/ai/goal/EatBlockGoal.java +index 4e2c23ccdf4e4a4d65b291dbe20952bae1838bff..0da884a833f6c707fea512e826658c3bb73f7a77 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/goal/EatBlockGoal.java ++++ b/src/main/java/net/minecraft/world/entity/ai/goal/EatBlockGoal.java +@@ -74,7 +74,7 @@ public class EatBlockGoal extends Goal { + + final BlockState blockState = this.level.getBlockState(blockposition); // Paper - fix wrong block state + if (EatBlockGoal.IS_TALL_GRASS.test(blockState)) { // Paper - fix wrong block state +- if (CraftEventFactory.callEntityChangeBlockEvent(this.mob, blockposition, blockState.getFluidState().createLegacyBlock(), !this.level.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING))) { // CraftBukkit // Paper - fix wrong block state ++ if (CraftEventFactory.callEntityChangeBlockEvent(this.mob, blockposition, blockState.getFluidState().createLegacyBlock(), !this.level.purpurConfig.sheepBypassMobGriefing && !this.level.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING))) { // CraftBukkit // Paper - fix wrong block state // Purpur + this.level.destroyBlock(blockposition, false); + } + +@@ -83,7 +83,7 @@ public class EatBlockGoal extends Goal { + BlockPos blockposition1 = blockposition.below(); + + if (this.level.getBlockState(blockposition1).is(Blocks.GRASS_BLOCK)) { +- if (CraftEventFactory.callEntityChangeBlockEvent(this.mob, blockposition1, Blocks.DIRT.defaultBlockState(), !this.level.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING))) { // CraftBukkit // Paper - Fix wrong block state ++ if (CraftEventFactory.callEntityChangeBlockEvent(this.mob, blockposition1, Blocks.DIRT.defaultBlockState(), !this.level.purpurConfig.sheepBypassMobGriefing && !this.level.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING))) { // CraftBukkit // Paper - Fix wrong block state // Purpur + this.level.levelEvent(2001, blockposition1, Block.getId(Blocks.GRASS_BLOCK.defaultBlockState())); + this.level.setBlock(blockposition1, Blocks.DIRT.defaultBlockState(), 2); + } +diff --git a/src/main/java/net/minecraft/world/entity/ai/goal/GoalSelector.java b/src/main/java/net/minecraft/world/entity/ai/goal/GoalSelector.java +index 74d4f653d5c7f1923c59019effd78337402f7025..b4e4670536f6dcea109c029d75d9710cb386f1d0 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/goal/GoalSelector.java ++++ b/src/main/java/net/minecraft/world/entity/ai/goal/GoalSelector.java +@@ -87,8 +87,8 @@ public class GoalSelector { + } + + public void tick() { +- ProfilerFiller profilerFiller = this.profiler.get(); +- profilerFiller.push("goalCleanup"); ++ //ProfilerFiller profilerFiller = this.profiler.get(); // Purpur ++ //profilerFiller.push("goalCleanup"); // Purpur + + for (WrappedGoal wrappedGoal : this.availableGoals) { + if (wrappedGoal.isRunning() && (goalContainsAnyFlags(wrappedGoal, this.goalTypes) || !wrappedGoal.canContinueToUse())) { // Paper - Perf: optimize goal types by removing streams +@@ -97,8 +97,8 @@ public class GoalSelector { + } + + this.lockedFlags.entrySet().removeIf(entry -> !entry.getValue().isRunning()); +- profilerFiller.pop(); +- profilerFiller.push("goalUpdate"); ++ //profilerFiller.pop(); // Purpur ++ //profilerFiller.push("goalUpdate"); // Purpur + + for (WrappedGoal wrappedGoal2 : this.availableGoals) { + // Paper start +@@ -118,13 +118,13 @@ public class GoalSelector { + } + } + +- profilerFiller.pop(); ++ //profilerFiller.pop(); // Purpur + this.tickRunningGoals(true); + } + + public void tickRunningGoals(boolean tickAll) { +- ProfilerFiller profilerFiller = this.profiler.get(); +- profilerFiller.push("goalTick"); ++ //ProfilerFiller profilerFiller = this.profiler.get(); // Purpur ++ //profilerFiller.push("goalTick"); // Purpur + + for (WrappedGoal wrappedGoal : this.availableGoals) { + if (wrappedGoal.isRunning() && (tickAll || wrappedGoal.requiresUpdateEveryTick())) { +@@ -132,7 +132,7 @@ public class GoalSelector { + } + } + +- profilerFiller.pop(); ++ //profilerFiller.pop(); // Purpur + } + + public Set getAvailableGoals() { +diff --git a/src/main/java/net/minecraft/world/entity/ai/goal/LlamaFollowCaravanGoal.java b/src/main/java/net/minecraft/world/entity/ai/goal/LlamaFollowCaravanGoal.java +index df695b444fa2a993d381e2f197182c3e91a68502..0f4f546cd0eda4bd82b47446ae23ac32da8a9556 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/goal/LlamaFollowCaravanGoal.java ++++ b/src/main/java/net/minecraft/world/entity/ai/goal/LlamaFollowCaravanGoal.java +@@ -22,6 +22,7 @@ public class LlamaFollowCaravanGoal extends Goal { + + @Override + public boolean canUse() { ++ if (!this.llama.level().purpurConfig.llamaJoinCaravans || !this.llama.shouldJoinCaravan) return false; // Purpur + if (!this.llama.isLeashed() && !this.llama.inCaravan()) { + List list = this.llama.level().getEntities(this.llama, this.llama.getBoundingBox().inflate(9.0, 4.0, 9.0), entity -> { + EntityType entityType = entity.getType(); +@@ -71,6 +72,7 @@ public class LlamaFollowCaravanGoal extends Goal { + + @Override + public boolean canContinueToUse() { ++ if (!this.llama.shouldJoinCaravan) return false; // Purpur + if (this.llama.inCaravan() && this.llama.getCaravanHead().isAlive() && this.firstIsLeashed(this.llama, 0)) { + double d = this.llama.distanceToSqr(this.llama.getCaravanHead()); + if (d > 676.0) { +diff --git a/src/main/java/net/minecraft/world/entity/ai/goal/RangedBowAttackGoal.java b/src/main/java/net/minecraft/world/entity/ai/goal/RangedBowAttackGoal.java +index 515c1f671cb2c3a7cc23053aedf404bbbe77af3e..42a1e5b9c08a9245dd0ce6d4025e3bb5d60feb62 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/goal/RangedBowAttackGoal.java ++++ b/src/main/java/net/minecraft/world/entity/ai/goal/RangedBowAttackGoal.java +@@ -116,9 +116,9 @@ public class RangedBowAttackGoal extends Go + } + + this.mob.lookAt(livingEntity, 30.0F, 30.0F); +- } else { ++ } //else { // Purpur - fix MC-121706 + this.mob.getLookControl().setLookAt(livingEntity, 30.0F, 30.0F); +- } ++ //} // Purpur + + if (this.mob.isUsingItem()) { + if (!bl && this.seeTime < -60) { +diff --git a/src/main/java/net/minecraft/world/entity/ai/goal/RemoveBlockGoal.java b/src/main/java/net/minecraft/world/entity/ai/goal/RemoveBlockGoal.java +index 6634228ef002cbef67980272a26be4a75c954116..a61abba840a55fb4fbc9716a5e05eb2778068785 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/goal/RemoveBlockGoal.java ++++ b/src/main/java/net/minecraft/world/entity/ai/goal/RemoveBlockGoal.java +@@ -40,7 +40,7 @@ public class RemoveBlockGoal extends MoveToBlockGoal { + + @Override + public boolean canUse() { +- if (!this.removerMob.level().getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING)) { ++ if (!this.removerMob.level().purpurConfig.zombieBypassMobGriefing && !this.removerMob.level().getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING)) { // Purpur + return false; + } else if (this.nextStartTick > 0) { + --this.nextStartTick; +diff --git a/src/main/java/net/minecraft/world/entity/ai/goal/RunAroundLikeCrazyGoal.java b/src/main/java/net/minecraft/world/entity/ai/goal/RunAroundLikeCrazyGoal.java +index b0944fa1f3849dd24cd010fa0a6638f5fd7179d1..d409ae987088df3d47192128401d7491aaabc87c 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/goal/RunAroundLikeCrazyGoal.java ++++ b/src/main/java/net/minecraft/world/entity/ai/goal/RunAroundLikeCrazyGoal.java +@@ -67,7 +67,7 @@ public class RunAroundLikeCrazyGoal extends Goal { + int i = this.horse.getTemper(); + int j = this.horse.getMaxTemper(); + +- if (j > 0 && this.horse.getRandom().nextInt(j) < i && !CraftEventFactory.callEntityTameEvent(this.horse, ((CraftHumanEntity) this.horse.getBukkitEntity().getPassenger()).getHandle()).isCancelled()) { // CraftBukkit - fire EntityTameEvent ++ if ((this.horse.level().purpurConfig.alwaysTameInCreative && entityhuman.hasInfiniteMaterials()) || (j > 0 && this.horse.getRandom().nextInt(j) < i && !CraftEventFactory.callEntityTameEvent(this.horse, ((CraftHumanEntity) this.horse.getBukkitEntity().getPassenger()).getHandle()).isCancelled())) { // CraftBukkit - fire EntityTameEvent // Purpur + this.horse.tameWithName(entityhuman); + return; + } +diff --git a/src/main/java/net/minecraft/world/entity/ai/goal/SwellGoal.java b/src/main/java/net/minecraft/world/entity/ai/goal/SwellGoal.java +index 137ec75ee803789deb7b1ca93dd9369c9af362b9..ca95d25af3e9a0536868b0c7fd8e7d2ff1154ee3 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/goal/SwellGoal.java ++++ b/src/main/java/net/minecraft/world/entity/ai/goal/SwellGoal.java +@@ -54,6 +54,14 @@ public class SwellGoal extends Goal { + this.creeper.setSwellDir(-1); + } else { + this.creeper.setSwellDir(1); ++ // Purpur start ++ if (this.creeper.level().purpurConfig.creeperEncircleTarget) { ++ net.minecraft.world.phys.Vec3 relative = this.creeper.position().subtract(this.target.position()); ++ relative = relative.yRot((float) Math.PI / 3).normalize().multiply(2, 2, 2); ++ net.minecraft.world.phys.Vec3 destination = this.target.position().add(relative); ++ this.creeper.getNavigation().moveTo(destination.x, destination.y, destination.z, 1); ++ } ++ // Purpur end + } + } + } +diff --git a/src/main/java/net/minecraft/world/entity/ai/goal/TemptGoal.java b/src/main/java/net/minecraft/world/entity/ai/goal/TemptGoal.java +index 13f8c2cb42334ba3b573ca44ace1d3df76e41ff7..baca552e52c728867fcb0527b6c3eb394b2b9c7f 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/goal/TemptGoal.java ++++ b/src/main/java/net/minecraft/world/entity/ai/goal/TemptGoal.java +@@ -64,7 +64,7 @@ public class TemptGoal extends Goal { + } + + private boolean shouldFollow(LivingEntity entity) { +- return this.items.test(entity.getMainHandItem()) || this.items.test(entity.getOffhandItem()); ++ return (this.items.test(entity.getMainHandItem()) || this.items.test(entity.getOffhandItem())) && (!(this.mob instanceof net.minecraft.world.entity.npc.Villager villager) || !villager.isSleeping()); // Purpur Fix #512 + } + + @Override +diff --git a/src/main/java/net/minecraft/world/entity/ai/navigation/PathNavigation.java b/src/main/java/net/minecraft/world/entity/ai/navigation/PathNavigation.java +index 2e9991e6b3c05584002744a2ee2579b1dba218b2..544920a31b649985333f82beafa94a3392f5853e 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/navigation/PathNavigation.java ++++ b/src/main/java/net/minecraft/world/entity/ai/navigation/PathNavigation.java +@@ -172,12 +172,12 @@ public abstract class PathNavigation { + } + } + // Paper end - EntityPathfindEvent +- this.level.getProfiler().push("pathfind"); ++ //this.level.getProfiler().push("pathfind"); // Purpur + BlockPos blockPos = useHeadPos ? this.mob.blockPosition().above() : this.mob.blockPosition(); + int i = (int)(followRange + (float)range); + PathNavigationRegion pathNavigationRegion = new PathNavigationRegion(this.level, blockPos.offset(-i, -i, -i), blockPos.offset(i, i, i)); + Path path = this.pathFinder.findPath(pathNavigationRegion, this.mob, positions, followRange, distance, this.maxVisitedNodesMultiplier); +- this.level.getProfiler().pop(); ++ //this.level.getProfiler().pop(); // Purpur + if (path != null && path.getTarget() != null) { + this.targetPos = path.getTarget(); + this.reachRange = distance; +diff --git a/src/main/java/net/minecraft/world/entity/ai/sensing/NearestBedSensor.java b/src/main/java/net/minecraft/world/entity/ai/sensing/NearestBedSensor.java +index 92731b6b593289e9f583c9b705b219e81fcd8e73..9104d7010bda6f9f73b478c11490ef9c53f76da2 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/sensing/NearestBedSensor.java ++++ b/src/main/java/net/minecraft/world/entity/ai/sensing/NearestBedSensor.java +@@ -56,7 +56,7 @@ public class NearestBedSensor extends Sensor { + // Paper start - optimise POI access + java.util.List, BlockPos>> poiposes = new java.util.ArrayList<>(); + // don't ask me why it's unbounded. ask mojang. +- io.papermc.paper.util.PoiAccess.findAnyPoiPositions(poiManager, type -> type.is(PoiTypes.HOME), predicate, entity.blockPosition(), 48, PoiManager.Occupancy.ANY, false, Integer.MAX_VALUE, poiposes); ++ io.papermc.paper.util.PoiAccess.findAnyPoiPositions(poiManager, type -> type.is(PoiTypes.HOME), predicate, entity.blockPosition(), world.purpurConfig.villagerNearestBedSensorSearchRadius, PoiManager.Occupancy.ANY, false, Integer.MAX_VALUE, poiposes); // Purpur + Path path = AcquirePoi.findPathToPois(entity, new java.util.HashSet<>(poiposes)); + // Paper end - optimise POI access + if (path != null && path.canReach()) { +diff --git a/src/main/java/net/minecraft/world/entity/ai/sensing/SecondaryPoiSensor.java b/src/main/java/net/minecraft/world/entity/ai/sensing/SecondaryPoiSensor.java +index a0e0692d17760f440fe81d52887284c787e562db..ab9bebc07b5228dbc0d3ba4b0f7d1bbe41814c9b 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/sensing/SecondaryPoiSensor.java ++++ b/src/main/java/net/minecraft/world/entity/ai/sensing/SecondaryPoiSensor.java +@@ -22,6 +22,13 @@ public class SecondaryPoiSensor extends Sensor { + + @Override + protected void doTick(ServerLevel world, Villager entity) { ++ // Purpur start - make sure clerics don't wander to soul sand when the option is off ++ Brain brain = entity.getBrain(); ++ if (!world.purpurConfig.villagerClericsFarmWarts && entity.getVillagerData().getProfession() == net.minecraft.world.entity.npc.VillagerProfession.CLERIC) { ++ brain.eraseMemory(MemoryModuleType.SECONDARY_JOB_SITE); ++ return; ++ } ++ // Purpur end + ResourceKey resourceKey = world.dimension(); + BlockPos blockPos = entity.blockPosition(); + List list = Lists.newArrayList(); +@@ -38,7 +45,7 @@ public class SecondaryPoiSensor extends Sensor { + } + } + +- Brain brain = entity.getBrain(); ++ //Brain brain = entity.getBrain(); // Purpur - moved up + if (!list.isEmpty()) { + brain.setMemory(MemoryModuleType.SECONDARY_JOB_SITE, list); + } else { +diff --git a/src/main/java/net/minecraft/world/entity/ai/sensing/Sensing.java b/src/main/java/net/minecraft/world/entity/ai/sensing/Sensing.java +index 51772f03a3469b11e7166ec6f3a1b9c64a606221..02f2f46ccc48bb4d9bd08555818b0489f60d9f13 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/sensing/Sensing.java ++++ b/src/main/java/net/minecraft/world/entity/ai/sensing/Sensing.java +@@ -26,9 +26,9 @@ public class Sensing { + } else if (this.unseen.contains(i)) { + return false; + } else { +- this.mob.level().getProfiler().push("hasLineOfSight"); ++ //this.mob.level().getProfiler().push("hasLineOfSight"); // Purpur + boolean bl = this.mob.hasLineOfSight(entity); +- this.mob.level().getProfiler().pop(); ++ //this.mob.level().getProfiler().pop(); // Purpur + if (bl) { + this.seen.add(i); + } else { +diff --git a/src/main/java/net/minecraft/world/entity/ai/sensing/Sensor.java b/src/main/java/net/minecraft/world/entity/ai/sensing/Sensor.java +index 85b4b24361e785acf75571ff98f924c00ae80748..09a7b418ddf564c0be13297f7c216db2e7ae1578 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/sensing/Sensor.java ++++ b/src/main/java/net/minecraft/world/entity/ai/sensing/Sensor.java +@@ -53,10 +53,10 @@ public abstract class Sensor { + if (--this.timeToTick <= 0L) { + // Paper start - configurable sensor tick rate and timings + this.timeToTick = java.util.Objects.requireNonNullElse(world.paperConfig().tickRates.sensor.get(entity.getType(), this.configKey), this.scanRate); +- this.timing.startTiming(); ++ //this.timing.startTiming(); // Purpur + // Paper end + this.doTick(world, entity); +- this.timing.stopTiming(); // Paper - sensor timings ++ //this.timing.stopTiming(); // Paper - sensor timings // Purpur + } + } + +diff --git a/src/main/java/net/minecraft/world/entity/ai/targeting/TargetingConditions.java b/src/main/java/net/minecraft/world/entity/ai/targeting/TargetingConditions.java +index d2f0c3b26d4beedb49d86e0242d843590d469d02..7463eefb7d09ea55fe8780210e7e967c2fe7896d 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/targeting/TargetingConditions.java ++++ b/src/main/java/net/minecraft/world/entity/ai/targeting/TargetingConditions.java +@@ -64,6 +64,10 @@ public class TargetingConditions { + return false; + } else if (this.selector != null && !this.selector.test(targetEntity)) { + return false; ++ // Purpur start ++ } else if (!targetEntity.level().purpurConfig.idleTimeoutTargetPlayer && targetEntity instanceof net.minecraft.server.level.ServerPlayer player && player.isAfk()) { ++ return false; ++ // Purpur end + } else { + if (baseEntity == null) { + if (this.isCombat && (!targetEntity.canBeSeenAsEnemy() || targetEntity.level().getDifficulty() == Difficulty.PEACEFUL)) { +diff --git a/src/main/java/net/minecraft/world/entity/ambient/Bat.java b/src/main/java/net/minecraft/world/entity/ambient/Bat.java +index dc27ddf5131e7398a5390a5187261d4c7fb6ccaa..274db35be501d1dd0c5407d350a37b04bf4fbb7e 100644 +--- a/src/main/java/net/minecraft/world/entity/ambient/Bat.java ++++ b/src/main/java/net/minecraft/world/entity/ambient/Bat.java +@@ -44,12 +44,59 @@ public class Bat extends AmbientCreature { + + public Bat(EntityType type, Level world) { + super(type, world); ++ this.moveControl = new org.purpurmc.purpur.controller.FlyingWithSpacebarMoveControllerWASD(this, 0.075F); // Purpur + if (!world.isClientSide) { + this.setResting(true); + } + + } + ++ // Purpur start ++ @Override ++ public boolean shouldSendAttribute(net.minecraft.world.entity.ai.attributes.Attribute attribute) { return attribute != Attributes.FLYING_SPEED; } // Fixes log spam on clients ++ ++ @Override ++ public boolean isRidable() { ++ return level().purpurConfig.batRidable; ++ } ++ ++ @Override ++ public boolean dismountsUnderwater() { ++ return level().purpurConfig.useDismountsUnderwaterTag ? super.dismountsUnderwater() : !level().purpurConfig.batRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level().purpurConfig.batControllable; ++ } ++ ++ @Override ++ public double getMaxY() { ++ return level().purpurConfig.batMaxY; ++ } ++ ++ @Override ++ public void onMount(Player rider) { ++ super.onMount(rider); ++ if (isResting()) { ++ setResting(false); ++ level().levelEvent(null, 1025, new BlockPos(this).above(), 0); ++ } ++ } ++ ++ @Override ++ public void travel(Vec3 vec3) { ++ super.travel(vec3); ++ if (getRider() != null && this.isControllable() && !onGround) { ++ float speed = (float) getAttributeValue(Attributes.FLYING_SPEED) * 2; ++ setSpeed(speed); ++ Vec3 mot = getDeltaMovement(); ++ move(net.minecraft.world.entity.MoverType.SELF, mot.multiply(speed, 0.25, speed)); ++ setDeltaMovement(mot.scale(0.9D)); ++ } ++ } ++ // Purpur end ++ + @Override + public boolean isFlapping() { + return !this.isResting() && (float) this.tickCount % 10.0F == 0.0F; +@@ -99,7 +146,7 @@ public class Bat extends AmbientCreature { + protected void pushEntities() {} + + public static AttributeSupplier.Builder createAttributes() { +- return Mob.createMobAttributes().add(Attributes.MAX_HEALTH, 6.0D); ++ return Mob.createMobAttributes().add(Attributes.MAX_HEALTH, 6.0D).add(Attributes.FLYING_SPEED, 0.6D); // Purpur + } + + public boolean isResting() { +@@ -132,6 +179,14 @@ public class Bat extends AmbientCreature { + + @Override + protected void customServerAiStep() { ++ // Purpur start ++ if (getRider() != null && this.isControllable()) { ++ Vec3 mot = getDeltaMovement(); ++ setDeltaMovement(mot.x(), mot.y() + (getVerticalMot() > 0 ? 0.07D : 0.0D), mot.z()); ++ return; ++ } ++ // Purpur end ++ + super.customServerAiStep(); + BlockPos blockposition = this.blockPosition(); + BlockPos blockposition1 = blockposition.above(); +@@ -210,6 +265,28 @@ public class Bat extends AmbientCreature { + } + } + ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level().purpurConfig.batMaxHealth); ++ this.getAttribute(Attributes.FOLLOW_RANGE).setBaseValue(this.level().purpurConfig.batFollowRange); ++ this.getAttribute(Attributes.KNOCKBACK_RESISTANCE).setBaseValue(this.level().purpurConfig.batKnockbackResistance); ++ this.getAttribute(Attributes.MOVEMENT_SPEED).setBaseValue(this.level().purpurConfig.batMovementSpeed); ++ this.getAttribute(Attributes.FLYING_SPEED).setBaseValue(this.level().purpurConfig.batFlyingSpeed); ++ this.getAttribute(Attributes.ARMOR).setBaseValue(this.level().purpurConfig.batArmor); ++ this.getAttribute(Attributes.ARMOR_TOUGHNESS).setBaseValue(this.level().purpurConfig.batArmorToughness); ++ this.getAttribute(Attributes.ATTACK_KNOCKBACK).setBaseValue(this.level().purpurConfig.batAttackKnockback); ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level().purpurConfig.batTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level().purpurConfig.batAlwaysDropExp; ++ } ++ + @Override + public void readAdditionalSaveData(CompoundTag nbt) { + super.readAdditionalSaveData(nbt); +@@ -229,7 +306,7 @@ public class Bat extends AmbientCreature { + int i = world.getMaxLocalRawBrightness(pos); + byte b0 = 4; + +- if (Bat.isHalloween()) { ++ if (Bat.isHalloweenSeason(world.getMinecraftWorld())) { // Purpur + b0 = 7; + } else if (random.nextBoolean()) { + return false; +@@ -239,6 +316,7 @@ public class Bat extends AmbientCreature { + } + } + ++ public static boolean isHalloweenSeason(Level level) { return level.purpurConfig.forceHalloweenSeason || isHalloween(); } // Purpur + private static boolean isHalloween() { + LocalDate localdate = LocalDate.now(); + int i = localdate.get(ChronoField.DAY_OF_MONTH); +diff --git a/src/main/java/net/minecraft/world/entity/animal/AbstractFish.java b/src/main/java/net/minecraft/world/entity/animal/AbstractFish.java +index 3231eaa6af2ddfe4095ff2d650f580ebd4d43aea..e8cb124d232f7316cc8c35dd8bd12f79bbcda7d6 100644 +--- a/src/main/java/net/minecraft/world/entity/animal/AbstractFish.java ++++ b/src/main/java/net/minecraft/world/entity/animal/AbstractFish.java +@@ -87,6 +87,7 @@ public abstract class AbstractFish extends WaterAnimal implements Bucketable { + @Override + protected void registerGoals() { + super.registerGoals(); ++ this.goalSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.goalSelector.addGoal(0, new PanicGoal(this, 1.25)); + this.goalSelector.addGoal(2, new AvoidEntityGoal<>(this, Player.class, 8.0F, 1.6, 1.4, EntitySelector.NO_SPECTATORS::test)); + this.goalSelector.addGoal(4, new AbstractFish.FishSwimGoal(this)); +@@ -100,7 +101,7 @@ public abstract class AbstractFish extends WaterAnimal implements Bucketable { + @Override + public void travel(Vec3 movementInput) { + if (this.isEffectiveAi() && this.isInWater()) { +- this.moveRelative(0.01F, movementInput); ++ this.moveRelative(getRider() != null ? getSpeed() : 0.01F, movementInput); // Purpur + this.move(MoverType.SELF, this.getDeltaMovement()); + this.setDeltaMovement(this.getDeltaMovement().scale(0.9)); + if (this.getTarget() == null) { +@@ -161,7 +162,7 @@ public abstract class AbstractFish extends WaterAnimal implements Bucketable { + protected void playStepSound(BlockPos pos, BlockState state) { + } + +- static class FishMoveControl extends MoveControl { ++ static class FishMoveControl extends org.purpurmc.purpur.controller.WaterMoveControllerWASD { // Purpur + private final AbstractFish fish; + + FishMoveControl(AbstractFish owner) { +@@ -169,14 +170,22 @@ public abstract class AbstractFish extends WaterAnimal implements Bucketable { + this.fish = owner; + } + ++ // Purpur start + @Override +- public void tick() { ++ public void purpurTick(Player rider) { ++ super.purpurTick(rider); ++ fish.setDeltaMovement(fish.getDeltaMovement().add(0.0D, 0.005D, 0.0D)); ++ } ++ // Purpur end ++ ++ @Override ++ public void vanillaTick() { // Purpur + if (this.fish.isEyeInFluid(FluidTags.WATER)) { + this.fish.setDeltaMovement(this.fish.getDeltaMovement().add(0.0, 0.005, 0.0)); + } + + if (this.operation == MoveControl.Operation.MOVE_TO && !this.fish.getNavigation().isDone()) { +- float f = (float)(this.speedModifier * this.fish.getAttributeValue(Attributes.MOVEMENT_SPEED)); ++ float f = (float)(this.getSpeedModifier() * this.fish.getAttributeValue(Attributes.MOVEMENT_SPEED)); // Purpur + this.fish.setSpeed(Mth.lerp(0.125F, this.fish.getSpeed(), f)); + double d = this.wantedX - this.fish.getX(); + double e = this.wantedY - this.fish.getY(); +diff --git a/src/main/java/net/minecraft/world/entity/animal/Animal.java b/src/main/java/net/minecraft/world/entity/animal/Animal.java +index 5193cf1d3c922d750a11e492b7636215e23ad0d6..7f90a0d8f65c96844df06b7c4fa3da28a6f51dd1 100644 +--- a/src/main/java/net/minecraft/world/entity/animal/Animal.java ++++ b/src/main/java/net/minecraft/world/entity/animal/Animal.java +@@ -42,6 +42,7 @@ public abstract class Animal extends AgeableMob { + @Nullable + public UUID loveCause; + public ItemStack breedItem; // CraftBukkit - Add breedItem variable ++ public abstract int getPurpurBreedTime(); // Purpur + + protected Animal(EntityType type, Level world) { + super(type, world); +@@ -146,7 +147,7 @@ public abstract class Animal extends AgeableMob { + if (this.isFood(itemstack)) { + int i = this.getAge(); + +- if (!this.level().isClientSide && i == 0 && this.canFallInLove()) { ++ if (!this.level().isClientSide && i == 0 && this.canFallInLove() && (this.level().purpurConfig.animalBreedingCooldownSeconds <= 0 || !this.level().hasBreedingCooldown(player.getUUID(), this.getClass()))) { // Purpur + final ItemStack breedCopy = itemstack.copy(); // Paper - Fix EntityBreedEvent copying + this.usePlayerItem(player, hand, itemstack); + this.setInLove(player, breedCopy); // Paper - Fix EntityBreedEvent copying +@@ -234,12 +235,20 @@ public abstract class Animal extends AgeableMob { + AgeableMob entityageable = this.getBreedOffspring(world, other); + + if (entityageable != null) { +- entityageable.setBaby(true); +- entityageable.moveTo(this.getX(), this.getY(), this.getZ(), 0.0F, 0.0F); +- // CraftBukkit start - call EntityBreedEvent ++ // Purpur start + ServerPlayer breeder = Optional.ofNullable(this.getLoveCause()).or(() -> { + return Optional.ofNullable(other.getLoveCause()); + }).orElse(null); ++ if (breeder != null && world.purpurConfig.animalBreedingCooldownSeconds > 0) { ++ if (world.hasBreedingCooldown(breeder.getUUID(), this.getClass())) { ++ return; ++ } ++ world.addBreedingCooldown(breeder.getUUID(), this.getClass()); ++ } ++ entityageable.setBaby(true); ++ entityageable.moveTo(this.getX(), this.getY(), this.getZ(), 0.0F, 0.0F); ++ // CraftBukkit start - call EntityBreedEvent ++ // Purpur end + int experience = this.getRandom().nextInt(7) + 1; + EntityBreedEvent entityBreedEvent = org.bukkit.craftbukkit.event.CraftEventFactory.callEntityBreedEvent(entityageable, this, other, breeder, this.breedItem, experience); + if (entityBreedEvent.isCancelled()) { +@@ -267,8 +276,10 @@ public abstract class Animal extends AgeableMob { + entityplayer.awardStat(Stats.ANIMALS_BRED); + CriteriaTriggers.BRED_ANIMALS.trigger(entityplayer, this, entityanimal, entityageable); + } // Paper +- this.setAge(6000); +- entityanimal.setAge(6000); ++ // Purpur start ++ this.setAge(this.getPurpurBreedTime()); ++ entityanimal.setAge(entityanimal.getPurpurBreedTime()); ++ // Purpur end + this.resetLove(); + entityanimal.resetLove(); + worldserver.broadcastEntityEvent(this, (byte) 18); +diff --git a/src/main/java/net/minecraft/world/entity/animal/Bee.java b/src/main/java/net/minecraft/world/entity/animal/Bee.java +index 0dfb8109fd8c022b079da00f6a0e3fc85b57bf7a..539170813921de2dfcd7ef84dd7512d73cd27e68 100644 +--- a/src/main/java/net/minecraft/world/entity/animal/Bee.java ++++ b/src/main/java/net/minecraft/world/entity/animal/Bee.java +@@ -143,6 +143,7 @@ public class Bee extends Animal implements NeutralMob, FlyingAnimal { + public Bee(EntityType type, Level world) { + super(type, world); + this.remainingCooldownBeforeLocatingNewFlower = Mth.nextInt(this.random, 20, 60); ++ final org.purpurmc.purpur.controller.FlyingMoveControllerWASD flyingController = new org.purpurmc.purpur.controller.FlyingMoveControllerWASD(this, 0.25F, 1.0F, false); // Purpur + // Paper start - Fix MC-167279 + class BeeFlyingMoveControl extends FlyingMoveControl { + public BeeFlyingMoveControl(final Mob entity, final int maxPitchChange, final boolean noGravity) { +@@ -151,22 +152,69 @@ public class Bee extends Animal implements NeutralMob, FlyingAnimal { + + @Override + public void tick() { ++ // Purpur start ++ if (mob.getRider() != null && mob.isControllable()) { ++ flyingController.purpurTick(mob.getRider()); ++ return; ++ } ++ // Purpur end + if (this.mob.getY() <= Bee.this.level().getMinBuildHeight()) { + this.mob.setNoGravity(false); + } + super.tick(); + } ++ ++ // Purpur start ++ @Override ++ public boolean hasWanted() { ++ return mob.getRider() != null || !mob.isControllable() || super.hasWanted(); ++ } ++ // Purpur end + } + this.moveControl = new BeeFlyingMoveControl(this, 20, true); + // Paper end - Fix MC-167279 + this.lookControl = new Bee.BeeLookControl(this); + this.setPathfindingMalus(PathType.DANGER_FIRE, -1.0F); +- this.setPathfindingMalus(PathType.WATER, -1.0F); ++ if (isSensitiveToWater()) this.setPathfindingMalus(PathType.WATER, -1.0F); // Purpur + this.setPathfindingMalus(PathType.WATER_BORDER, 16.0F); + this.setPathfindingMalus(PathType.COCOA, -1.0F); + this.setPathfindingMalus(PathType.FENCE, -1.0F); + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level().purpurConfig.beeRidable; ++ } ++ ++ @Override ++ public boolean dismountsUnderwater() { ++ return level().purpurConfig.useDismountsUnderwaterTag ? super.dismountsUnderwater() : !level().purpurConfig.beeRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level().purpurConfig.beeControllable; ++ } ++ ++ @Override ++ public double getMaxY() { ++ return level().purpurConfig.beeMaxY; ++ } ++ ++ @Override ++ public void travel(Vec3 vec3) { ++ super.travel(vec3); ++ if (getRider() != null && this.isControllable() && !onGround) { ++ float speed = (float) getAttributeValue(Attributes.FLYING_SPEED) * 2; ++ setSpeed(speed); ++ Vec3 mot = getDeltaMovement(); ++ move(net.minecraft.world.entity.MoverType.SELF, mot.multiply(speed, speed, speed)); ++ setDeltaMovement(mot.scale(0.9D)); ++ } ++ } ++ // Purpur end ++ + @Override + protected void defineSynchedData(SynchedEntityData.Builder builder) { + super.defineSynchedData(builder); +@@ -181,6 +229,7 @@ public class Bee extends Animal implements NeutralMob, FlyingAnimal { + + @Override + protected void registerGoals() { ++ this.goalSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.goalSelector.addGoal(0, new Bee.BeeAttackGoal(this, 1.399999976158142D, true)); + this.goalSelector.addGoal(1, new Bee.BeeEnterHiveGoal()); + this.goalSelector.addGoal(2, new BreedGoal(this, 1.0D)); +@@ -198,6 +247,7 @@ public class Bee extends Animal implements NeutralMob, FlyingAnimal { + this.goalSelector.addGoal(7, new Bee.BeeGrowCropGoal()); + this.goalSelector.addGoal(8, new Bee.BeeWanderGoal()); + this.goalSelector.addGoal(9, new FloatGoal(this)); ++ this.targetSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.targetSelector.addGoal(1, (new Bee.BeeHurtByOtherGoal(this)).setAlertOthers(new Class[0])); + this.targetSelector.addGoal(2, new Bee.BeeBecomeAngryTargetGoal(this)); + this.targetSelector.addGoal(3, new ResetUniversalAngerTargetGoal<>(this, true)); +@@ -345,7 +395,7 @@ public class Bee extends Animal implements NeutralMob, FlyingAnimal { + + boolean wantsToEnterHive() { + if (this.stayOutOfHiveCountdown <= 0 && !this.beePollinateGoal.isPollinating() && !this.hasStung() && this.getTarget() == null) { +- boolean flag = this.isTiredOfLookingForNectar() || this.level().isRaining() || this.level().isNight() || this.hasNectar(); ++ boolean flag = this.isTiredOfLookingForNectar() || (this.level().isRaining() && !this.level().purpurConfig.beeCanWorkInRain) || (this.level().isNight() && !this.level().purpurConfig.beeCanWorkAtNight) || this.hasNectar(); // Purpur + + return flag && !this.isHiveNearFire(); + } else { +@@ -385,6 +435,7 @@ public class Bee extends Animal implements NeutralMob, FlyingAnimal { + this.hurt(this.damageSources().drown(), 1.0F); + } + ++ if (flag && !this.level().purpurConfig.beeDiesAfterSting) setHasStung(false); else // Purpur + if (flag) { + ++this.timeSinceSting; + if (this.timeSinceSting % 5 == 0 && this.random.nextInt(Mth.clamp(1200 - this.timeSinceSting, 1, 1200)) == 0) { +@@ -417,6 +468,26 @@ public class Bee extends Animal implements NeutralMob, FlyingAnimal { + } + } + ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level().purpurConfig.beeMaxHealth); ++ } ++ ++ @Override ++ public int getPurpurBreedTime() { ++ return this.level().purpurConfig.beeBreedingTicks; ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level().purpurConfig.beeTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level().purpurConfig.beeAlwaysDropExp; ++ } ++ + @Override + public int getRemainingPersistentAngerTime() { + return (Integer) this.entityData.get(Bee.DATA_REMAINING_ANGER_TIME); +@@ -726,6 +797,7 @@ public class Bee extends Animal implements NeutralMob, FlyingAnimal { + if (optional.isPresent()) { + Bee.this.savedFlowerPos = (BlockPos) optional.get(); + Bee.this.navigation.moveTo((double) Bee.this.savedFlowerPos.getX() + 0.5D, (double) Bee.this.savedFlowerPos.getY() + 0.5D, (double) Bee.this.savedFlowerPos.getZ() + 0.5D, 1.2000000476837158D); ++ new org.purpurmc.purpur.event.entity.BeeFoundFlowerEvent((org.bukkit.entity.Bee) Bee.this.getBukkitEntity(), io.papermc.paper.util.MCUtil.toLocation(Bee.this.level(), Bee.this.savedFlowerPos)).callEvent(); // Purpur + return true; + } else { + Bee.this.remainingCooldownBeforeLocatingNewFlower = Mth.nextInt(Bee.this.random, 20, 60); +@@ -782,6 +854,7 @@ public class Bee extends Animal implements NeutralMob, FlyingAnimal { + this.pollinating = false; + Bee.this.navigation.stop(); + Bee.this.remainingCooldownBeforeLocatingNewFlower = 200; ++ new org.purpurmc.purpur.event.entity.BeeStopPollinatingEvent((org.bukkit.entity.Bee) Bee.this.getBukkitEntity(), Bee.this.savedFlowerPos == null ? null : io.papermc.paper.util.MCUtil.toLocation(Bee.this.level(), Bee.this.savedFlowerPos), Bee.this.hasNectar()).callEvent(); // Purpur + } + + @Override +@@ -828,6 +901,7 @@ public class Bee extends Animal implements NeutralMob, FlyingAnimal { + this.setWantedPos(); + } + ++ if (this.successfulPollinatingTicks == 0) new org.purpurmc.purpur.event.entity.BeeStartedPollinatingEvent((org.bukkit.entity.Bee) Bee.this.getBukkitEntity(), io.papermc.paper.util.MCUtil.toLocation(Bee.this.level(), Bee.this.savedFlowerPos)).callEvent(); // Purpur + ++this.successfulPollinatingTicks; + if (Bee.this.random.nextFloat() < 0.05F && this.successfulPollinatingTicks > this.lastSoundPlayedTick + 60) { + this.lastSoundPlayedTick = this.successfulPollinatingTicks; +@@ -872,16 +946,16 @@ public class Bee extends Animal implements NeutralMob, FlyingAnimal { + } + } + +- private class BeeLookControl extends LookControl { ++ private class BeeLookControl extends org.purpurmc.purpur.controller.LookControllerWASD { // Purpur + + BeeLookControl(final Mob entity) { + super(entity); + } + + @Override +- public void tick() { ++ public void vanillaTick() { // Purpur + if (!Bee.this.isAngry()) { +- super.tick(); ++ super.vanillaTick(); // Purpur + } + } + +diff --git a/src/main/java/net/minecraft/world/entity/animal/Cat.java b/src/main/java/net/minecraft/world/entity/animal/Cat.java +index 07559b9629d4ecb40b511256f400a781e39820e0..3e1345f1c534320e07820d573f5c8dba49746425 100644 +--- a/src/main/java/net/minecraft/world/entity/animal/Cat.java ++++ b/src/main/java/net/minecraft/world/entity/animal/Cat.java +@@ -104,6 +104,53 @@ public class Cat extends TamableAnimal implements VariantHolder(this, Rabbit.class, false, (Predicate) null)); + this.targetSelector.addGoal(1, new NonTameRandomTargetGoal<>(this, Turtle.class, false, Turtle.BABY_ON_LAND_SELECTOR)); + } +@@ -318,6 +367,14 @@ public class Cat extends TamableAnimal implements VariantHolder { + return itemstack.is(ItemTags.CHICKEN_FOOD); +@@ -66,6 +107,14 @@ public class Chicken extends Animal { + this.goalSelector.addGoal(5, new WaterAvoidingRandomStrollGoal(this, 1.0D)); + this.goalSelector.addGoal(6, new LookAtPlayerGoal(this, Player.class, 6.0F)); + this.goalSelector.addGoal(7, new RandomLookAroundGoal(this)); ++ // Purpur start ++ if (level().purpurConfig.chickenRetaliate) { ++ this.goalSelector.addGoal(1, new net.minecraft.world.entity.ai.goal.MeleeAttackGoal(this, 1.0D, false)); ++ this.targetSelector.addGoal(1, new net.minecraft.world.entity.ai.goal.target.HurtByTargetGoal(this)); ++ } else { ++ this.goalSelector.addGoal(1, new PanicGoal(this, 1.4D)); ++ } ++ // Purpur end + } + + @Override +@@ -74,7 +123,7 @@ public class Chicken extends Animal { + } + + public static AttributeSupplier.Builder createAttributes() { +- return Mob.createMobAttributes().add(Attributes.MAX_HEALTH, 4.0D).add(Attributes.MOVEMENT_SPEED, 0.25D); ++ return Mob.createMobAttributes().add(Attributes.MAX_HEALTH, 4.0D).add(Attributes.MOVEMENT_SPEED, 0.25D).add(Attributes.ATTACK_DAMAGE, 0.0D); // Purpur + } + + @Override +diff --git a/src/main/java/net/minecraft/world/entity/animal/Cod.java b/src/main/java/net/minecraft/world/entity/animal/Cod.java +index 824e5e4fe7619ae46061c3c978c9a044db8c84ab..f0b6118a9995bb41836685bbf94d2e7fb15761eb 100644 +--- a/src/main/java/net/minecraft/world/entity/animal/Cod.java ++++ b/src/main/java/net/minecraft/world/entity/animal/Cod.java +@@ -13,6 +13,33 @@ public class Cod extends AbstractSchoolingFish { + super(type, world); + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level().purpurConfig.codRidable; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level().purpurConfig.codControllable; ++ } ++ // Purpur end ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(net.minecraft.world.entity.ai.attributes.Attributes.MAX_HEALTH).setBaseValue(this.level().purpurConfig.codMaxHealth); ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level().purpurConfig.codTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level().purpurConfig.codAlwaysDropExp; ++ } ++ + @Override + public ItemStack getBucketItemStack() { + return new ItemStack(Items.COD_BUCKET); +diff --git a/src/main/java/net/minecraft/world/entity/animal/Cow.java b/src/main/java/net/minecraft/world/entity/animal/Cow.java +index 5a7b1be351834a6b8889b1380cede1be025cb302..1691a98caabf27ea092a9b422649ac84bc0a7235 100644 +--- a/src/main/java/net/minecraft/world/entity/animal/Cow.java ++++ b/src/main/java/net/minecraft/world/entity/animal/Cow.java +@@ -2,6 +2,7 @@ package net.minecraft.world.entity.animal; + + import javax.annotation.Nullable; + import net.minecraft.core.BlockPos; ++import net.minecraft.core.particles.ParticleTypes; + import net.minecraft.server.level.ServerLevel; + import net.minecraft.sounds.SoundEvent; + import net.minecraft.sounds.SoundEvents; +@@ -29,6 +30,7 @@ import net.minecraft.world.item.ItemStack; + import net.minecraft.world.item.ItemUtils; + import net.minecraft.world.item.Items; + import net.minecraft.world.level.Level; ++import net.minecraft.world.level.block.Blocks; + import net.minecraft.world.level.block.state.BlockState; + // CraftBukkit start + import org.bukkit.craftbukkit.event.CraftEventFactory; +@@ -37,6 +39,7 @@ import org.bukkit.event.player.PlayerBucketFillEvent; + // CraftBukkit end + + public class Cow extends Animal { ++ private boolean isNaturallyAggressiveToPlayers; // Purpur + + private static final EntityDimensions BABY_DIMENSIONS = EntityType.COW.getDimensions().scale(0.5F).withEyeHeight(0.665F); + +@@ -44,18 +47,65 @@ public class Cow extends Animal { + super(type, world); + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level().purpurConfig.cowRidable; ++ } ++ ++ @Override ++ public boolean dismountsUnderwater() { ++ return level().purpurConfig.useDismountsUnderwaterTag ? super.dismountsUnderwater() : !level().purpurConfig.cowRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level().purpurConfig.cowControllable; ++ } ++ // Purpur end ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level().purpurConfig.cowMaxHealth); ++ this.getAttribute(Attributes.ATTACK_DAMAGE).setBaseValue(this.level().purpurConfig.cowNaturallyAggressiveToPlayersDamage); // Purpur ++ } ++ ++ @Override ++ public int getPurpurBreedTime() { ++ return this.level().purpurConfig.cowBreedingTicks; ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level().purpurConfig.cowTakeDamageFromWater; ++ } ++ ++ @Override ++ public net.minecraft.world.entity.SpawnGroupData finalizeSpawn(net.minecraft.world.level.ServerLevelAccessor world, net.minecraft.world.DifficultyInstance difficulty, net.minecraft.world.entity.MobSpawnType spawnReason, net.minecraft.world.entity.SpawnGroupData entityData) { ++ this.isNaturallyAggressiveToPlayers = world.getLevel().purpurConfig.cowNaturallyAggressiveToPlayersChance > 0.0D && random.nextDouble() <= world.getLevel().purpurConfig.cowNaturallyAggressiveToPlayersChance; ++ return super.finalizeSpawn(world, difficulty, spawnReason, entityData); ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level().purpurConfig.cowAlwaysDropExp; ++ } ++ + @Override + protected void registerGoals() { + this.goalSelector.addGoal(0, new FloatGoal(this)); ++ this.goalSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.goalSelector.addGoal(1, new PanicGoal(this, 2.0D)); ++ this.goalSelector.addGoal(1, new net.minecraft.world.entity.ai.goal.MeleeAttackGoal(this, 1.2000000476837158D, true)); // Purpur + this.goalSelector.addGoal(2, new BreedGoal(this, 1.0D)); + this.goalSelector.addGoal(3, new TemptGoal(this, 1.25D, (itemstack) -> { +- return itemstack.is(ItemTags.COW_FOOD); ++ return level().purpurConfig.cowFeedMushrooms > 0 && (itemstack.is(Blocks.RED_MUSHROOM.asItem()) || itemstack.is(Blocks.BROWN_MUSHROOM.asItem())) || itemstack.is(ItemTags.COW_FOOD); // Purpur + }, false)); + this.goalSelector.addGoal(4, new FollowParentGoal(this, 1.25D)); + this.goalSelector.addGoal(5, new WaterAvoidingRandomStrollGoal(this, 1.0D)); + this.goalSelector.addGoal(6, new LookAtPlayerGoal(this, Player.class, 6.0F)); + this.goalSelector.addGoal(7, new RandomLookAroundGoal(this)); ++ this.targetSelector.addGoal(0, new net.minecraft.world.entity.ai.goal.target.NearestAttackableTargetGoal<>(this, Player.class, 10, true, false, target -> isNaturallyAggressiveToPlayers)); // Purpur + } + + @Override +@@ -64,7 +114,7 @@ public class Cow extends Animal { + } + + public static AttributeSupplier.Builder createAttributes() { +- return Mob.createMobAttributes().add(Attributes.MAX_HEALTH, 10.0D).add(Attributes.MOVEMENT_SPEED, 0.20000000298023224D); ++ return Mob.createMobAttributes().add(Attributes.MAX_HEALTH, 10.0D).add(Attributes.MOVEMENT_SPEED, 0.20000000298023224D).add(Attributes.ATTACK_DAMAGE, 0.0D); // Purpur + } + + @Override +@@ -94,6 +144,7 @@ public class Cow extends Animal { + + @Override + public InteractionResult mobInteract(Player player, InteractionHand hand) { ++ if (getRider() != null) return InteractionResult.PASS; // Purpur + ItemStack itemstack = player.getItemInHand(hand); + + if (itemstack.is(Items.BUCKET) && !this.isBaby()) { +@@ -101,7 +152,7 @@ public class Cow extends Animal { + PlayerBucketFillEvent event = CraftEventFactory.callPlayerBucketFillEvent((ServerLevel) player.level(), player, this.blockPosition(), this.blockPosition(), null, itemstack, Items.MILK_BUCKET, hand); + + if (event.isCancelled()) { +- return InteractionResult.PASS; ++ return tryRide(player, hand); // Purpur + } + // CraftBukkit end + +@@ -110,6 +161,10 @@ public class Cow extends Animal { + + player.setItemInHand(hand, itemstack1); + return InteractionResult.sidedSuccess(this.level().isClientSide); ++ // Purpur start - feed mushroom to change to mooshroom ++ } else if (level().purpurConfig.cowFeedMushrooms > 0 && this.getType() != EntityType.MOOSHROOM && isMushroom(itemstack)) { ++ return this.feedMushroom(player, itemstack); ++ // Purpur end + } else { + return super.mobInteract(player, hand); + } +@@ -125,4 +180,69 @@ public class Cow extends Animal { + public EntityDimensions getDefaultDimensions(Pose pose) { + return this.isBaby() ? Cow.BABY_DIMENSIONS : super.getDefaultDimensions(pose); + } ++ ++ // Purpur start - feed mushroom to change to mooshroom ++ private int redMushroomsFed = 0; ++ private int brownMushroomsFed = 0; ++ ++ private boolean isMushroom(ItemStack stack) { ++ return stack.getItem() == Blocks.RED_MUSHROOM.asItem() || stack.getItem() == Blocks.BROWN_MUSHROOM.asItem(); ++ } ++ ++ private int incrementFeedCount(ItemStack stack) { ++ if (stack.getItem() == Blocks.RED_MUSHROOM.asItem()) { ++ return ++redMushroomsFed; ++ } else { ++ return ++brownMushroomsFed; ++ } ++ } ++ ++ private InteractionResult feedMushroom(Player player, ItemStack stack) { ++ level().broadcastEntityEvent(this, (byte) 18); // hearts ++ playSound(SoundEvents.COW_MILK, 1.0F, 1.0F); ++ if (incrementFeedCount(stack) < level().purpurConfig.cowFeedMushrooms) { ++ if (!player.getAbilities().instabuild) { ++ stack.shrink(1); ++ } ++ return InteractionResult.CONSUME; // require 5 mushrooms to transform (prevents mushroom duping) ++ } ++ MushroomCow mooshroom = EntityType.MOOSHROOM.create(level()); ++ if (mooshroom == null) { ++ return InteractionResult.PASS; ++ } ++ if (stack.getItem() == Blocks.BROWN_MUSHROOM.asItem()) { ++ mooshroom.setVariant(MushroomCow.MushroomType.BROWN); ++ } else { ++ mooshroom.setVariant(MushroomCow.MushroomType.RED); ++ } ++ mooshroom.moveTo(this.getX(), this.getY(), this.getZ(), this.getYRot(), this.getXRot()); ++ mooshroom.setHealth(this.getHealth()); ++ mooshroom.setAge(getAge()); ++ mooshroom.copyPosition(this); ++ mooshroom.setYBodyRot(this.yBodyRot); ++ mooshroom.setYHeadRot(this.getYHeadRot()); ++ mooshroom.yRotO = this.yRotO; ++ mooshroom.xRotO = this.xRotO; ++ if (this.hasCustomName()) { ++ mooshroom.setCustomName(this.getCustomName()); ++ } ++ if (CraftEventFactory.callEntityTransformEvent(this, mooshroom, org.bukkit.event.entity.EntityTransformEvent.TransformReason.INFECTION).isCancelled()) { ++ return InteractionResult.PASS; ++ } ++ if (!new com.destroystokyo.paper.event.entity.EntityTransformedEvent(this.getBukkitEntity(), mooshroom.getBukkitEntity(), com.destroystokyo.paper.event.entity.EntityTransformedEvent.TransformedReason.INFECTED).callEvent()) { ++ return InteractionResult.PASS; ++ } ++ this.level().addFreshEntity(mooshroom); ++ this.remove(RemovalReason.DISCARDED, org.bukkit.event.entity.EntityRemoveEvent.Cause.DISCARD); ++ if (!player.getAbilities().instabuild) { ++ stack.shrink(1); ++ } ++ for (int i = 0; i < 15; ++i) { ++ ((ServerLevel) level()).sendParticles(((ServerLevel) level()).players(), null, ParticleTypes.HAPPY_VILLAGER, ++ getX() + random.nextFloat(), getY() + (random.nextFloat() * 2), getZ() + random.nextFloat(), 1, ++ random.nextGaussian() * 0.05D, random.nextGaussian() * 0.05D, random.nextGaussian() * 0.05D, 0, true); ++ } ++ return InteractionResult.SUCCESS; ++ } ++ // Purpur end + } +diff --git a/src/main/java/net/minecraft/world/entity/animal/Dolphin.java b/src/main/java/net/minecraft/world/entity/animal/Dolphin.java +index 1b1cb0e4d54e52ebe794199e386c54c5d84b3719..c1a9a87ef0fefc499c0e1edbe1031f47cc432b31 100644 +--- a/src/main/java/net/minecraft/world/entity/animal/Dolphin.java ++++ b/src/main/java/net/minecraft/world/entity/animal/Dolphin.java +@@ -81,19 +81,104 @@ public class Dolphin extends WaterAnimal { + public static final Predicate ALLOWED_ITEMS = (entityitem) -> { + return !entityitem.hasPickUpDelay() && entityitem.isAlive() && entityitem.isInWater(); + }; ++ private boolean isNaturallyAggressiveToPlayers; // Purpur ++ private int spitCooldown; // Purpur + + public Dolphin(EntityType type, Level world) { + super(type, world); +- this.moveControl = new SmoothSwimmingMoveControl(this, 85, 10, 0.02F, 0.1F, true); ++ // Purpur start ++ class DolphinMoveControl extends SmoothSwimmingMoveControl { ++ private final org.purpurmc.purpur.controller.WaterMoveControllerWASD waterMoveControllerWASD; ++ private final Dolphin dolphin; ++ ++ public DolphinMoveControl(Dolphin dolphin, int pitchChange, int yawChange, float speedInWater, float speedInAir, boolean buoyant) { ++ super(dolphin, pitchChange, yawChange, speedInWater, speedInAir, buoyant); ++ this.dolphin = dolphin; ++ this.waterMoveControllerWASD = new org.purpurmc.purpur.controller.WaterMoveControllerWASD(dolphin); ++ } ++ ++ @Override ++ public void tick() { ++ if (dolphin.getRider() != null && dolphin.isControllable()) { ++ purpurTick(dolphin.getRider()); ++ } else { ++ super.tick(); ++ } ++ } ++ ++ public void purpurTick(Player rider) { ++ if (dolphin.getAirSupply() < 150) { ++ // if drowning override player WASD controls to find air ++ super.tick(); ++ } else { ++ waterMoveControllerWASD.purpurTick(rider); ++ dolphin.setDeltaMovement(dolphin.getDeltaMovement().add(0.0D, 0.005D, 0.0D)); ++ } ++ } ++ }; ++ this.moveControl = new DolphinMoveControl(this, 85, 10, 0.02F, 0.1F, true); ++ // Purpur end + this.lookControl = new SmoothSwimmingLookControl(this, 10); + this.setCanPickUpLoot(true); + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level().purpurConfig.dolphinRidable; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level().purpurConfig.dolphinControllable; ++ } ++ ++ @Override ++ public boolean onSpacebar() { ++ if (spitCooldown == 0 && getRider() != null) { ++ spitCooldown = level().purpurConfig.dolphinSpitCooldown; ++ ++ org.bukkit.craftbukkit.entity.CraftPlayer player = (org.bukkit.craftbukkit.entity.CraftPlayer) getRider().getBukkitEntity(); ++ if (!player.hasPermission("allow.special.dolphin")) { ++ return false; ++ } ++ ++ org.bukkit.Location loc = player.getEyeLocation(); ++ loc.setPitch(loc.getPitch() - 10); ++ org.bukkit.util.Vector target = loc.getDirection().normalize().multiply(10).add(loc.toVector()); ++ ++ org.purpurmc.purpur.entity.DolphinSpit spit = new org.purpurmc.purpur.entity.DolphinSpit(level(), this); ++ spit.shoot(target.getX() - getX(), target.getY() - getY(), target.getZ() - getZ(), level().purpurConfig.dolphinSpitSpeed, 5.0F); ++ ++ level().addFreshEntity(spit); ++ playSound(SoundEvents.DOLPHIN_ATTACK, 1.0F, 1.0F + (random.nextFloat() - random.nextFloat()) * 0.2F); ++ return true; ++ } ++ return false; ++ } ++ // Purpur end ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level().purpurConfig.dolphinMaxHealth); ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level().purpurConfig.dolphinTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level().purpurConfig.dolphinAlwaysDropExp; ++ } ++ + @Nullable + @Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor world, DifficultyInstance difficulty, MobSpawnType spawnReason, @Nullable SpawnGroupData entityData) { + this.setAirSupply(this.getMaxAirSupply()); + this.setXRot(0.0F); ++ this.isNaturallyAggressiveToPlayers = world.getLevel().purpurConfig.dolphinNaturallyAggressiveToPlayersChance > 0.0D && random.nextDouble() <= world.getLevel().purpurConfig.dolphinNaturallyAggressiveToPlayersChance; // Purpur + return super.finalizeSpawn(world, difficulty, spawnReason, entityData); + } + +@@ -158,17 +243,21 @@ public class Dolphin extends WaterAnimal { + protected void registerGoals() { + this.goalSelector.addGoal(0, new BreathAirGoal(this)); + this.goalSelector.addGoal(0, new TryFindWaterGoal(this)); ++ this.goalSelector.addGoal(1, new MeleeAttackGoal(this, 1.2000000476837158D, true)); // Purpur ++ this.goalSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.goalSelector.addGoal(1, new Dolphin.DolphinSwimToTreasureGoal(this)); + this.goalSelector.addGoal(2, new Dolphin.DolphinSwimWithPlayerGoal(this, 4.0D)); + this.goalSelector.addGoal(4, new RandomSwimmingGoal(this, 1.0D, 10)); + this.goalSelector.addGoal(4, new RandomLookAroundGoal(this)); + this.goalSelector.addGoal(5, new LookAtPlayerGoal(this, Player.class, 6.0F)); + this.goalSelector.addGoal(5, new DolphinJumpGoal(this, 10)); +- this.goalSelector.addGoal(6, new MeleeAttackGoal(this, 1.2000000476837158D, true)); ++ //this.goalSelector.addGoal(6, new MeleeAttackGoal(this, 1.2000000476837158D, true)); // Purpur - moved up + this.goalSelector.addGoal(8, new Dolphin.PlayWithItemsGoal()); + this.goalSelector.addGoal(8, new FollowBoatGoal(this)); + this.goalSelector.addGoal(9, new AvoidEntityGoal<>(this, Guardian.class, 8.0F, 1.0D, 1.0D)); ++ this.targetSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.targetSelector.addGoal(1, (new HurtByTargetGoal(this, new Class[]{Guardian.class})).setAlertOthers()); ++ this.targetSelector.addGoal(2, new net.minecraft.world.entity.ai.goal.target.NearestAttackableTargetGoal<>(this, Player.class, 10, true, false, target -> isNaturallyAggressiveToPlayers)); // Purpur + } + + public static AttributeSupplier.Builder createAttributes() { +@@ -214,7 +303,7 @@ public class Dolphin extends WaterAnimal { + + @Override + protected boolean canRide(Entity entity) { +- return true; ++ return boardingCooldown <= 0; // Purpur - make dolphin honor ride cooldown like all other non-boss mobs; + } + + @Override +@@ -249,6 +338,11 @@ public class Dolphin extends WaterAnimal { + @Override + public void tick() { + super.tick(); ++ // Purpur start ++ if (spitCooldown > 0) { ++ spitCooldown--; ++ } ++ // Purpur end + if (this.isNoAi()) { + this.setAirSupply(this.getMaxAirSupply()); + } else { +@@ -391,6 +485,7 @@ public class Dolphin extends WaterAnimal { + + @Override + public boolean canUse() { ++ if (this.dolphin.level().purpurConfig.dolphinDisableTreasureSearching) return false; // Purpur + return this.dolphin.gotFish() && this.dolphin.getAirSupply() >= 100; + } + +diff --git a/src/main/java/net/minecraft/world/entity/animal/Fox.java b/src/main/java/net/minecraft/world/entity/animal/Fox.java +index e705449496b1a06270ecbc13f4dce5357479845b..124839f22ed0499ca395a648e858469d81d31f93 100644 +--- a/src/main/java/net/minecraft/world/entity/animal/Fox.java ++++ b/src/main/java/net/minecraft/world/entity/animal/Fox.java +@@ -37,6 +37,7 @@ import net.minecraft.util.RandomSource; + import net.minecraft.util.StringRepresentable; + import net.minecraft.world.DifficultyInstance; + import net.minecraft.world.InteractionHand; ++import net.minecraft.world.InteractionResult; + import net.minecraft.world.damagesource.DamageSource; + import net.minecraft.world.entity.AgeableMob; + import net.minecraft.world.entity.Entity; +@@ -145,6 +146,64 @@ public class Fox extends Animal implements VariantHolder { + this.setCanPickUpLoot(true); + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level().purpurConfig.foxRidable; ++ } ++ ++ @Override ++ public boolean dismountsUnderwater() { ++ return level().purpurConfig.useDismountsUnderwaterTag ? super.dismountsUnderwater() : !level().purpurConfig.foxRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level().purpurConfig.foxControllable; ++ } ++ ++ @Override ++ public float getJumpPower() { ++ return getRider() != null && this.isControllable() ? 0.5F : super.getJumpPower(); ++ } ++ ++ @Override ++ public void onMount(Player rider) { ++ super.onMount(rider); ++ setCanPickUpLoot(false); ++ clearStates(); ++ setIsPouncing(false); ++ spitOutItem(getItemBySlot(EquipmentSlot.MAINHAND)); ++ setItemSlot(EquipmentSlot.MAINHAND, ItemStack.EMPTY); ++ } ++ ++ @Override ++ public void onDismount(Player rider) { ++ super.onDismount(rider); ++ setCanPickUpLoot(true); ++ } ++ // Purpur end ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level().purpurConfig.foxMaxHealth); ++ } ++ ++ @Override ++ public int getPurpurBreedTime() { ++ return this.level().purpurConfig.foxBreedingTicks; ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level().purpurConfig.foxTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level().purpurConfig.foxAlwaysDropExp; ++ } ++ + @Override + protected void defineSynchedData(SynchedEntityData.Builder builder) { + super.defineSynchedData(builder); +@@ -164,6 +223,7 @@ public class Fox extends Animal implements VariantHolder { + return entityliving instanceof AbstractSchoolingFish; + }); + this.goalSelector.addGoal(0, new Fox.FoxFloatGoal()); ++ this.goalSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.goalSelector.addGoal(0, new ClimbOnTopOfPowderSnowGoal(this, this.level())); + this.goalSelector.addGoal(1, new Fox.FaceplantGoal()); + this.goalSelector.addGoal(2, new Fox.FoxPanicGoal(2.2D)); +@@ -190,6 +250,7 @@ public class Fox extends Animal implements VariantHolder { + this.goalSelector.addGoal(11, new Fox.FoxSearchForItemsGoal()); + this.goalSelector.addGoal(12, new Fox.FoxLookAtPlayerGoal(this, Player.class, 24.0F)); + this.goalSelector.addGoal(13, new Fox.PerchAndSearchGoal()); ++ this.targetSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.targetSelector.addGoal(3, new Fox.DefendTrustedTargetGoal(LivingEntity.class, false, false, (entityliving) -> { + return Fox.TRUSTED_TARGET_SELECTOR.test(entityliving) && !this.trusts(entityliving.getUUID()); + })); +@@ -344,6 +405,11 @@ public class Fox extends Animal implements VariantHolder { + } + + private void setTargetGoals() { ++ // Purpur start - do not add duplicate goals ++ this.targetSelector.removeGoal(this.landTargetGoal); ++ this.targetSelector.removeGoal(this.turtleEggTargetGoal); ++ this.targetSelector.removeGoal(this.fishTargetGoal); ++ // Purpur end + if (this.getVariant() == Fox.Type.RED) { + this.targetSelector.addGoal(4, this.landTargetGoal); + this.targetSelector.addGoal(4, this.turtleEggTargetGoal); +@@ -377,6 +443,7 @@ public class Fox extends Animal implements VariantHolder { + + public void setVariant(Fox.Type variant) { + this.entityData.set(Fox.DATA_TYPE_ID, variant.getId()); ++ this.setTargetGoals(); // Purpur - fix API bug not updating pathfinders on type change + } + + List getTrustedUUIDs() { +@@ -717,6 +784,29 @@ public class Fox extends Animal implements VariantHolder { + } + // Paper end + ++ // Purpur start ++ @Override ++ public InteractionResult mobInteract(Player player, InteractionHand hand) { ++ if (level().purpurConfig.foxTypeChangesWithTulips) { ++ ItemStack itemstack = player.getItemInHand(hand); ++ if (getVariant() == Type.RED && itemstack.getItem() == Items.WHITE_TULIP) { ++ setVariant(Type.SNOW); ++ if (!player.getAbilities().instabuild) { ++ itemstack.shrink(1); ++ } ++ return InteractionResult.SUCCESS; ++ } else if (getVariant() == Type.SNOW && itemstack.getItem() == Items.ORANGE_TULIP) { ++ setVariant(Type.RED); ++ if (!player.getAbilities().instabuild) { ++ itemstack.shrink(1); ++ } ++ return InteractionResult.SUCCESS; ++ } ++ } ++ return super.mobInteract(player, hand); ++ } ++ // Purpur end ++ + @Override + // Paper start - Cancellable death event + protected org.bukkit.event.entity.EntityDeathEvent dropAllDeathLoot(DamageSource source) { +@@ -769,16 +859,16 @@ public class Fox extends Animal implements VariantHolder { + return new Vec3(0.0D, (double) (0.55F * this.getEyeHeight()), (double) (this.getBbWidth() * 0.4F)); + } + +- public class FoxLookControl extends LookControl { ++ public class FoxLookControl extends org.purpurmc.purpur.controller.LookControllerWASD { // Purpur + + public FoxLookControl() { + super(Fox.this); + } + + @Override +- public void tick() { ++ public void vanillaTick() { // Purpur + if (!Fox.this.isSleeping()) { +- super.tick(); ++ super.vanillaTick(); // Purpur + } + + } +@@ -789,16 +879,16 @@ public class Fox extends Animal implements VariantHolder { + } + } + +- private class FoxMoveControl extends MoveControl { ++ private class FoxMoveControl extends org.purpurmc.purpur.controller.MoveControllerWASD { // Purpur + + public FoxMoveControl() { + super(Fox.this); + } + + @Override +- public void tick() { ++ public void vanillaTick() { // Purpur + if (Fox.this.canMove()) { +- super.tick(); ++ super.vanillaTick(); // Purpur + } + + } +@@ -916,8 +1006,10 @@ public class Fox extends Animal implements VariantHolder { + CriteriaTriggers.BRED_ANIMALS.trigger(entityplayer2, this.animal, this.partner, entityfox); + } + +- this.animal.setAge(6000); +- this.partner.setAge(6000); ++ // Purpur start ++ this.animal.setAge(this.animal.getPurpurBreedTime()); ++ this.partner.setAge(this.partner.getPurpurBreedTime()); ++ // Purpur end + this.animal.resetLove(); + this.partner.resetLove(); + worldserver.addFreshEntityWithPassengers(entityfox, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.BREEDING); // CraftBukkit - added SpawnReason +@@ -1303,7 +1395,7 @@ public class Fox extends Animal implements VariantHolder { + } + + protected void onReachedTarget() { +- if (Fox.this.level().getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING)) { ++ if (Fox.this.level().purpurConfig.foxBypassMobGriefing || Fox.this.level().getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING)) { // Purpur + BlockState iblockdata = Fox.this.level().getBlockState(this.blockPos); + + if (iblockdata.is(Blocks.SWEET_BERRY_BUSH)) { +diff --git a/src/main/java/net/minecraft/world/entity/animal/IronGolem.java b/src/main/java/net/minecraft/world/entity/animal/IronGolem.java +index 932fae98c551052cadba4c6fc6e575fc30a25d58..12bc57d36d76f49596df0004fda31a6a678be60c 100644 +--- a/src/main/java/net/minecraft/world/entity/animal/IronGolem.java ++++ b/src/main/java/net/minecraft/world/entity/animal/IronGolem.java +@@ -56,13 +56,58 @@ public class IronGolem extends AbstractGolem implements NeutralMob { + private int remainingPersistentAngerTime; + @Nullable + private UUID persistentAngerTarget; ++ @Nullable private UUID summoner; // Purpur + + public IronGolem(EntityType type, Level world) { + super(type, world); + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level().purpurConfig.ironGolemRidable; ++ } ++ ++ @Override ++ public boolean dismountsUnderwater() { ++ return level().purpurConfig.useDismountsUnderwaterTag ? super.dismountsUnderwater() : !level().purpurConfig.ironGolemRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level().purpurConfig.ironGolemControllable; ++ } ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level().purpurConfig.ironGolemMaxHealth); ++ } ++ // Purpur end ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level().purpurConfig.ironGolemTakeDamageFromWater; ++ } ++ ++ @Nullable ++ public UUID getSummoner() { ++ return summoner; ++ } ++ ++ public void setSummoner(@Nullable UUID summoner) { ++ this.summoner = summoner; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level().purpurConfig.ironGolemAlwaysDropExp; ++ } ++ + @Override + protected void registerGoals() { ++ if (level().purpurConfig.ironGolemCanSwim) this.goalSelector.addGoal(0, new net.minecraft.world.entity.ai.goal.FloatGoal(this)); // Purpur ++ if (this.level().purpurConfig.ironGolemPoppyCalm) this.goalSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.ReceiveFlower(this)); // Purpur ++ this.goalSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.goalSelector.addGoal(1, new MeleeAttackGoal(this, 1.0D, true)); + this.goalSelector.addGoal(2, new MoveTowardsTargetGoal(this, 0.9D, 32.0F)); + this.goalSelector.addGoal(2, new MoveBackToVillageGoal(this, 0.6D, false)); +@@ -70,6 +115,7 @@ public class IronGolem extends AbstractGolem implements NeutralMob { + this.goalSelector.addGoal(5, new OfferFlowerGoal(this)); + this.goalSelector.addGoal(7, new LookAtPlayerGoal(this, Player.class, 6.0F)); + this.goalSelector.addGoal(8, new RandomLookAroundGoal(this)); ++ this.targetSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.targetSelector.addGoal(1, new DefendVillageTargetGoal(this)); + this.targetSelector.addGoal(2, new HurtByTargetGoal(this, new Class[0])); + this.targetSelector.addGoal(3, new NearestAttackableTargetGoal<>(this, Player.class, 10, true, false, this::isAngryAt)); +@@ -134,6 +180,7 @@ public class IronGolem extends AbstractGolem implements NeutralMob { + public void addAdditionalSaveData(CompoundTag nbt) { + super.addAdditionalSaveData(nbt); + nbt.putBoolean("PlayerCreated", this.isPlayerCreated()); ++ if (getSummoner() != null) nbt.putUUID("Purpur.Summoner", getSummoner()); // Purpur + this.addPersistentAngerSaveData(nbt); + } + +@@ -141,6 +188,7 @@ public class IronGolem extends AbstractGolem implements NeutralMob { + public void readAdditionalSaveData(CompoundTag nbt) { + super.readAdditionalSaveData(nbt); + this.setPlayerCreated(nbt.getBoolean("PlayerCreated")); ++ if (nbt.contains("Purpur.Summoner")) setSummoner(nbt.getUUID("Purpur.Summoner")); // Purpur + this.readPersistentAngerSaveData(this.level(), nbt); + } + +@@ -265,18 +313,19 @@ public class IronGolem extends AbstractGolem implements NeutralMob { + ItemStack itemstack = player.getItemInHand(hand); + + if (!itemstack.is(Items.IRON_INGOT)) { +- return InteractionResult.PASS; ++ return tryRide(player, hand); // Purpur + } else { + float f = this.getHealth(); + + this.heal(25.0F); + if (this.getHealth() == f) { +- return InteractionResult.PASS; ++ return tryRide(player, hand); // Purpur + } else { + float f1 = 1.0F + (this.random.nextFloat() - this.random.nextFloat()) * 0.2F; + + this.playSound(SoundEvents.IRON_GOLEM_REPAIR, 1.0F, f1); + itemstack.consume(1, player); ++ if (this.level().purpurConfig.ironGolemHealCalm && isAngry() && getHealth() == getMaxHealth()) stopBeingAngry(); // Purpur + return InteractionResult.sidedSuccess(this.level().isClientSide); + } + } +diff --git a/src/main/java/net/minecraft/world/entity/animal/MushroomCow.java b/src/main/java/net/minecraft/world/entity/animal/MushroomCow.java +index 0c21959f57ae88fcd0a4d6dc911c1ce347c96528..a7c95199234231db14faa4a07953bcde57d9861d 100644 +--- a/src/main/java/net/minecraft/world/entity/animal/MushroomCow.java ++++ b/src/main/java/net/minecraft/world/entity/animal/MushroomCow.java +@@ -64,6 +64,43 @@ public class MushroomCow extends Cow implements Shearable, VariantHolder drops = this.generateDefaultDrops(); ++ java.util.List drops = this.generateDefaultDrops(net.minecraft.world.item.enchantment.EnchantmentHelper.getMobLooting(player)); // Purpur + org.bukkit.event.player.PlayerShearEntityEvent event = CraftEventFactory.handlePlayerShearEntityEvent(player, this, itemstack, hand, drops); + if (event != null) { + if (event.isCancelled()) { +- return InteractionResult.PASS; ++ return tryRide(player, hand); // Purpur + } + drops = org.bukkit.craftbukkit.inventory.CraftItemStack.asNMSCopy(event.getDrops()); + } +@@ -150,7 +187,7 @@ public class MushroomCow extends Cow implements Shearable, VariantHolder optional = this.getEffectsFromItemStack(itemstack); + + if (optional.isEmpty()) { +- return InteractionResult.PASS; ++ return tryRide(player, hand); // Purpur + } + + itemstack.consume(1, player); +@@ -172,13 +209,13 @@ public class MushroomCow extends Cow implements Shearable, VariantHolder generateDefaultDrops() { ++ public java.util.List generateDefaultDrops(int looting) { // Purpur + java.util.List dropEntities = new java.util.ArrayList<>(5); +- for (int i = 0; i < 5; ++i) { ++ for (int i = 0; i < 5 + (org.purpurmc.purpur.PurpurConfig.allowShearsLooting ? looting : 0); ++i) { // Purpur + dropEntities.add(new ItemStack(this.getVariant().getBlockState().getBlock())); + } + return dropEntities; +@@ -196,7 +233,13 @@ public class MushroomCow extends Cow implements Shearable, VariantHolder(this, Chicken.class, false)); + this.targetSelector.addGoal(1, new NearestAttackableTargetGoal<>(this, Turtle.class, 10, false, false, Turtle.BABY_ON_LAND_SELECTOR)); + } +@@ -254,7 +293,7 @@ public class Ocelot extends Animal { + if (world.isUnobstructed(this) && !world.containsAnyLiquid(this.getBoundingBox())) { + BlockPos blockposition = this.blockPosition(); + +- if (blockposition.getY() < world.getSeaLevel()) { ++ if (!level().purpurConfig.ocelotSpawnUnderSeaLevel && blockposition.getY() < world.getSeaLevel()) { + return false; + } + +diff --git a/src/main/java/net/minecraft/world/entity/animal/Panda.java b/src/main/java/net/minecraft/world/entity/animal/Panda.java +index db60b91c2b26ca8cdb66e05deab7742ffe212767..7bd81d073ce4a8d5981f256415d3e99e13da79ba 100644 +--- a/src/main/java/net/minecraft/world/entity/animal/Panda.java ++++ b/src/main/java/net/minecraft/world/entity/animal/Panda.java +@@ -120,6 +120,53 @@ public class Panda extends Animal { + + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level().purpurConfig.pandaRidable; ++ } ++ ++ @Override ++ public boolean dismountsUnderwater() { ++ return level().purpurConfig.useDismountsUnderwaterTag ? super.dismountsUnderwater() : !level().purpurConfig.pandaRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level().purpurConfig.pandaControllable; ++ } ++ ++ @Override ++ public void onMount(Player rider) { ++ super.onMount(rider); ++ setForwardMot(0.0F); ++ sit(false); ++ eat(false); ++ setOnBack(false); ++ } ++ // Purpur end ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level().purpurConfig.pandaMaxHealth); ++ setAttributes(); ++ } ++ ++ @Override ++ public int getPurpurBreedTime() { ++ return this.level().purpurConfig.pandaBreedingTicks; ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level().purpurConfig.pandaTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level().purpurConfig.pandaAlwaysDropExp; ++ } ++ + @Override + public boolean canTakeItem(ItemStack stack) { + EquipmentSlot enumitemslot = Mob.getEquipmentSlotForItem(stack); +@@ -281,6 +328,7 @@ public class Panda extends Animal { + @Override + protected void registerGoals() { + this.goalSelector.addGoal(0, new FloatGoal(this)); ++ this.goalSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.goalSelector.addGoal(2, new Panda.PandaPanicGoal(this, 2.0D)); + this.goalSelector.addGoal(2, new Panda.PandaBreedGoal(this, 1.0D)); + this.goalSelector.addGoal(3, new Panda.PandaAttackGoal(this, 1.2000000476837158D, true)); +@@ -298,6 +346,7 @@ public class Panda extends Animal { + this.goalSelector.addGoal(12, new Panda.PandaRollGoal(this)); + this.goalSelector.addGoal(13, new FollowParentGoal(this, 1.25D)); + this.goalSelector.addGoal(14, new WaterAvoidingRandomStrollGoal(this, 1.0D)); ++ this.targetSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.targetSelector.addGoal(1, (new Panda.PandaHurtByTargetGoal(this, new Class[0])).setAlertOthers(new Class[0])); + } + +@@ -632,7 +681,10 @@ public class Panda extends Animal { + + public void setAttributes() { + if (this.isWeak()) { +- this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(10.0D); ++ // Purpur start ++ net.minecraft.world.entity.ai.attributes.AttributeInstance maxHealth = this.getAttribute(Attributes.MAX_HEALTH); ++ maxHealth.setBaseValue(maxHealth.getValue() / 2); ++ // Purpur end + } + + if (this.isLazy()) { +@@ -655,7 +707,7 @@ public class Panda extends Animal { + ItemStack itemstack = player.getItemInHand(hand); + + if (this.isScared()) { +- return InteractionResult.PASS; ++ return tryRide(player, hand); // Purpur + } else if (this.isOnBack()) { + this.setOnBack(false); + return InteractionResult.sidedSuccess(this.level().isClientSide); +@@ -673,7 +725,7 @@ public class Panda extends Animal { + this.setInLove(player, breedCopy); // Paper - Fix EntityBreedEvent copying + } else { + if (this.level().isClientSide || this.isSitting() || this.isInWater()) { +- return InteractionResult.PASS; ++ return tryRide(player, hand); // Purpur + } + + this.tryToSit(); +@@ -692,7 +744,7 @@ public class Panda extends Animal { + + return InteractionResult.SUCCESS; + } else { +- return InteractionResult.PASS; ++ return tryRide(player, hand); // Purpur + } + } + +@@ -737,7 +789,7 @@ public class Panda extends Animal { + return this.isBaby() ? Panda.BABY_DIMENSIONS : super.getDefaultDimensions(pose); + } + +- private static class PandaMoveControl extends MoveControl { ++ private static class PandaMoveControl extends org.purpurmc.purpur.controller.MoveControllerWASD { // Purpur + + private final Panda panda; + +@@ -747,9 +799,9 @@ public class Panda extends Animal { + } + + @Override +- public void tick() { ++ public void vanillaTick() { // Purpur + if (this.panda.canPerformAction()) { +- super.tick(); ++ super.vanillaTick(); // Purpur + } + } + } +diff --git a/src/main/java/net/minecraft/world/entity/animal/Parrot.java b/src/main/java/net/minecraft/world/entity/animal/Parrot.java +index 5ca96541abbb754f4d9fbe01f37ebaf19c532bbb..b8c69414b734eecb7412fab8ae6996712307da70 100644 +--- a/src/main/java/net/minecraft/world/entity/animal/Parrot.java ++++ b/src/main/java/net/minecraft/world/entity/animal/Parrot.java +@@ -124,12 +124,88 @@ public class Parrot extends ShoulderRidingEntity implements VariantHolder type, Level world) { + super(type, world); +- this.moveControl = new FlyingMoveControl(this, 10, false); ++ // Purpur start ++ final org.purpurmc.purpur.controller.FlyingWithSpacebarMoveControllerWASD flyingController = new org.purpurmc.purpur.controller.FlyingWithSpacebarMoveControllerWASD(this, 0.3F); ++ class ParrotMoveControl extends FlyingMoveControl { ++ public ParrotMoveControl(Mob entity, int maxPitchChange, boolean noGravity) { ++ super(entity, maxPitchChange, noGravity); ++ } ++ ++ @Override ++ public void tick() { ++ if (mob.getRider() != null && mob.isControllable()) { ++ flyingController.purpurTick(mob.getRider()); ++ } else { ++ super.tick(); ++ } ++ } ++ ++ @Override ++ public boolean hasWanted() { ++ return mob.getRider() != null && mob.isControllable() ? getForwardMot() != 0 || getStrafeMot() != 0 : super.hasWanted(); ++ } ++ } ++ this.moveControl = new ParrotMoveControl(this, 10, false); ++ // Purpur end + this.setPathfindingMalus(PathType.DANGER_FIRE, -1.0F); + this.setPathfindingMalus(PathType.DAMAGE_FIRE, -1.0F); + this.setPathfindingMalus(PathType.COCOA, -1.0F); + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level().purpurConfig.parrotRidable; ++ } ++ ++ @Override ++ public boolean dismountsUnderwater() { ++ return level().purpurConfig.useDismountsUnderwaterTag ? super.dismountsUnderwater() : !level().purpurConfig.parrotRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level().purpurConfig.parrotControllable; ++ } ++ ++ @Override ++ public double getMaxY() { ++ return level().purpurConfig.parrotMaxY; ++ } ++ ++ @Override ++ public void travel(Vec3 vec3) { ++ super.travel(vec3); ++ if (getRider() != null && this.isControllable() && !onGround) { ++ float speed = (float) getAttributeValue(Attributes.FLYING_SPEED) * 2; ++ setSpeed(speed); ++ Vec3 mot = getDeltaMovement(); ++ move(net.minecraft.world.entity.MoverType.SELF, mot.multiply(speed, 0.25, speed)); ++ setDeltaMovement(mot.scale(0.9D)); ++ } ++ } ++ // Purpur end ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level().purpurConfig.parrotMaxHealth); ++ } ++ ++ @Override ++ public int getPurpurBreedTime() { ++ return 6000; ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level().purpurConfig.parrotTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level().purpurConfig.parrotAlwaysDropExp; ++ } ++ + @Nullable + @Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor world, DifficultyInstance difficulty, MobSpawnType spawnReason, @Nullable SpawnGroupData entityData) { +@@ -148,8 +224,11 @@ public class Parrot extends ShoulderRidingEntity implements VariantHolder type, LevelAccessor world, MobSpawnType spawnReason, BlockPos pos, RandomSource random) { +@@ -292,13 +372,13 @@ public class Parrot extends ShoulderRidingEntity implements VariantHolder { +@@ -155,6 +193,17 @@ public class Pig extends Animal implements ItemSteerable, Saddleable { + public InteractionResult mobInteract(Player player, InteractionHand hand) { + boolean flag = this.isFood(player.getItemInHand(hand)); + ++ if (level().purpurConfig.pigGiveSaddleBack && player.isSecondaryUseActive() && !flag && isSaddled() && !isVehicle()) { ++ this.steering.setSaddle(false); ++ if (!player.getAbilities().instabuild) { ++ ItemStack saddle = new ItemStack(Items.SADDLE); ++ if (!player.getInventory().add(saddle)) { ++ player.drop(saddle, false); ++ } ++ } ++ return InteractionResult.SUCCESS; ++ } ++ + if (!flag && this.isSaddled() && !this.isVehicle() && !player.isSecondaryUseActive()) { + if (!this.level().isClientSide) { + player.startRiding(this); +diff --git a/src/main/java/net/minecraft/world/entity/animal/PolarBear.java b/src/main/java/net/minecraft/world/entity/animal/PolarBear.java +index c87a57e8ceac32a6c8a603aa24f8cb053610e47c..9b6e07b0de7328b18c5e526b89cfd48fdbc79753 100644 +--- a/src/main/java/net/minecraft/world/entity/animal/PolarBear.java ++++ b/src/main/java/net/minecraft/world/entity/animal/PolarBear.java +@@ -59,11 +59,81 @@ public class PolarBear extends Animal implements NeutralMob { + private int remainingPersistentAngerTime; + @Nullable + private UUID persistentAngerTarget; ++ private int standTimer = 0; // Purpur + + public PolarBear(EntityType type, Level world) { + super(type, world); + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level().purpurConfig.polarBearRidable; ++ } ++ ++ @Override ++ public boolean dismountsUnderwater() { ++ return level().purpurConfig.useDismountsUnderwaterTag ? super.dismountsUnderwater() : !level().purpurConfig.polarBearRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level().purpurConfig.polarBearControllable; ++ } ++ ++ @Override ++ public boolean onSpacebar() { ++ if (!isStanding()) { ++ if (getRider() != null && getRider().getForwardMot() == 0 && getRider().getStrafeMot() == 0) { ++ setStanding(true); ++ playSound(SoundEvents.POLAR_BEAR_WARNING, 1.0F, 1.0F); ++ } ++ } ++ return false; ++ } ++ // Purpur end ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level().purpurConfig.polarBearMaxHealth); ++ } ++ ++ public boolean canMate(Animal other) { ++ if (other == this) { ++ return false; ++ } else if (this.isStanding()) { ++ return false; ++ } else if (this.getTarget() != null) { ++ return false; ++ } else if (!(other instanceof PolarBear)) { ++ return false; ++ } else { ++ PolarBear bear = (PolarBear) other; ++ if (bear.isStanding()) { ++ return false; ++ } ++ if (bear.getTarget() != null) { ++ return false; ++ } ++ return this.isInLove() && bear.isInLove(); ++ } ++ } ++ ++ @Override ++ public int getPurpurBreedTime() { ++ return this.level().purpurConfig.polarBearBreedingTicks; ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level().purpurConfig.polarBearTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level().purpurConfig.polarBearAlwaysDropExp; ++ } ++ + @Nullable + @Override + public AgeableMob getBreedOffspring(ServerLevel world, AgeableMob entity) { +@@ -72,19 +142,27 @@ public class PolarBear extends Animal implements NeutralMob { + + @Override + public boolean isFood(ItemStack stack) { +- return false; ++ return level().purpurConfig.polarBearBreedableItem != null && stack.getItem() == level().purpurConfig.polarBearBreedableItem; // Purpur + } + + @Override + protected void registerGoals() { + super.registerGoals(); + this.goalSelector.addGoal(0, new FloatGoal(this)); ++ this.goalSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.goalSelector.addGoal(1, new PolarBear.PolarBearMeleeAttackGoal()); + this.goalSelector.addGoal(1, new PolarBear.PolarBearPanicGoal()); ++ // Purpur start ++ if (level().purpurConfig.polarBearBreedableItem != null) { ++ this.goalSelector.addGoal(2, new net.minecraft.world.entity.ai.goal.BreedGoal(this, 1.0D)); ++ this.goalSelector.addGoal(3, new net.minecraft.world.entity.ai.goal.TemptGoal(this, 1.0D, net.minecraft.world.item.crafting.Ingredient.of(level().purpurConfig.polarBearBreedableItem), false)); ++ } ++ // Purpur end + this.goalSelector.addGoal(4, new FollowParentGoal(this, 1.25)); + this.goalSelector.addGoal(5, new RandomStrollGoal(this, 1.0)); + this.goalSelector.addGoal(6, new LookAtPlayerGoal(this, Player.class, 6.0F)); + this.goalSelector.addGoal(7, new RandomLookAroundGoal(this)); ++ this.targetSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.targetSelector.addGoal(1, new PolarBear.PolarBearHurtByTargetGoal()); + this.targetSelector.addGoal(2, new PolarBear.PolarBearAttackPlayersGoal()); + this.targetSelector.addGoal(3, new NearestAttackableTargetGoal<>(this, Player.class, 10, true, false, this::isAngryAt)); +@@ -201,6 +279,12 @@ public class PolarBear extends Animal implements NeutralMob { + if (!this.level().isClientSide) { + this.updatePersistentAnger((ServerLevel)this.level(), true); + } ++ ++ // Purpur start ++ if (isStanding() && --standTimer <= 0) { ++ setStanding(false); ++ } ++ // Purpur end + } + + @Override +@@ -230,6 +314,7 @@ public class PolarBear extends Animal implements NeutralMob { + + public void setStanding(boolean warning) { + this.entityData.set(DATA_STANDING_ID, warning); ++ standTimer = warning ? 20 : -1; // Purpur + } + + public float getStandingAnimationScale(float tickDelta) { +diff --git a/src/main/java/net/minecraft/world/entity/animal/Pufferfish.java b/src/main/java/net/minecraft/world/entity/animal/Pufferfish.java +index 3f0fad476fe573c3ba946a9436d1b3f7c5260ee2..c758f759ccae81b7651bfcba254f54335f2c7cc8 100644 +--- a/src/main/java/net/minecraft/world/entity/animal/Pufferfish.java ++++ b/src/main/java/net/minecraft/world/entity/animal/Pufferfish.java +@@ -51,6 +51,33 @@ public class Pufferfish extends AbstractFish { + this.refreshDimensions(); + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level().purpurConfig.pufferfishRidable; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level().purpurConfig.pufferfishControllable; ++ } ++ // Purpur end ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(net.minecraft.world.entity.ai.attributes.Attributes.MAX_HEALTH).setBaseValue(this.level().purpurConfig.pufferfishMaxHealth); ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level().purpurConfig.pufferfishTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level().purpurConfig.pufferfishAlwaysDropExp; ++ } ++ + @Override + protected void defineSynchedData(SynchedEntityData.Builder builder) { + super.defineSynchedData(builder); +diff --git a/src/main/java/net/minecraft/world/entity/animal/Rabbit.java b/src/main/java/net/minecraft/world/entity/animal/Rabbit.java +index b58300e114e2e27ac68d7a9489bc52b127c9bc17..02702390c0b336762ce8c0d38d804e6f24ebbfd4 100644 +--- a/src/main/java/net/minecraft/world/entity/animal/Rabbit.java ++++ b/src/main/java/net/minecraft/world/entity/animal/Rabbit.java +@@ -86,6 +86,7 @@ public class Rabbit extends Animal implements VariantHolder { + private boolean wasOnGround; + private int jumpDelayTicks; + public int moreCarrotTicks; ++ private boolean actualJump; // Purpur + + public Rabbit(EntityType type, Level world) { + super(type, world); +@@ -93,9 +94,75 @@ public class Rabbit extends Animal implements VariantHolder { + this.moveControl = new Rabbit.RabbitMoveControl(this); + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level().purpurConfig.rabbitRidable; ++ } ++ ++ @Override ++ public boolean dismountsUnderwater() { ++ return level().purpurConfig.useDismountsUnderwaterTag ? super.dismountsUnderwater() : !level().purpurConfig.rabbitRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level().purpurConfig.rabbitControllable; ++ } ++ ++ @Override ++ public boolean onSpacebar() { ++ if (onGround) { ++ actualJump = true; ++ jumpFromGround(); ++ actualJump = false; ++ } ++ return true; ++ } ++ ++ private void handleJumping() { ++ if (onGround) { ++ RabbitJumpControl jumpController = (RabbitJumpControl) jumpControl; ++ if (!wasOnGround) { ++ setJumping(false); ++ jumpController.setCanJump(false); ++ } ++ if (!jumpController.wantJump()) { ++ if (moveControl.hasWanted()) { ++ startJumping(); ++ } ++ } else if (!jumpController.canJump()) { ++ jumpController.setCanJump(true); ++ } ++ } ++ wasOnGround = onGround; ++ } ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level().purpurConfig.rabbitMaxHealth); ++ } ++ ++ @Override ++ public int getPurpurBreedTime() { ++ return this.level().purpurConfig.rabbitBreedingTicks; ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level().purpurConfig.rabbitTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level().purpurConfig.rabbitAlwaysDropExp; ++ } ++ // Purpur end ++ + @Override + public void registerGoals() { + this.goalSelector.addGoal(1, new FloatGoal(this)); ++ this.goalSelector.addGoal(1, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.goalSelector.addGoal(1, new ClimbOnTopOfPowderSnowGoal(this, this.level())); + this.goalSelector.addGoal(1, new Rabbit.RabbitPanicGoal(this, 2.2D)); + this.goalSelector.addGoal(2, new BreedGoal(this, 0.8D)); +@@ -112,6 +179,14 @@ public class Rabbit extends Animal implements VariantHolder { + + @Override + protected float getJumpPower() { ++ // Purpur start ++ if (getRider() != null && this.isControllable()) { ++ if (getForwardMot() < 0) { ++ setSpeed(getForwardMot() * 2F); ++ } ++ return actualJump ? 0.5F : 0.3F; ++ } ++ // Purpur end + float f = 0.3F; + + if (this.horizontalCollision || this.moveControl.hasWanted() && this.moveControl.getWantedY() > this.getY() + 0.5D) { +@@ -136,7 +211,7 @@ public class Rabbit extends Animal implements VariantHolder { + } + + @Override +- protected void jumpFromGround() { ++ public void jumpFromGround() { // Purpur - protected -> public + super.jumpFromGround(); + double d0 = this.moveControl.getSpeedModifier(); + +@@ -186,6 +261,13 @@ public class Rabbit extends Animal implements VariantHolder { + + @Override + public void customServerAiStep() { ++ // Purpur start ++ if (getRider() != null && this.isControllable()) { ++ handleJumping(); ++ return; ++ } ++ // Purpur end ++ + if (this.jumpDelayTicks > 0) { + --this.jumpDelayTicks; + } +@@ -399,10 +481,23 @@ public class Rabbit extends Animal implements VariantHolder { + } + + this.setVariant(entityrabbit_variant); ++ ++ // Purpur start ++ if (entityrabbit_variant != Variant.EVIL && world.getLevel().purpurConfig.rabbitNaturalToast > 0D && random.nextDouble() <= world.getLevel().purpurConfig.rabbitNaturalToast) { ++ setCustomName(Component.translatable("Toast")); ++ } ++ // Purpur end ++ + return super.finalizeSpawn(world, difficulty, spawnReason, (SpawnGroupData) entityData); + } + + private static Rabbit.Variant getRandomRabbitVariant(LevelAccessor world, BlockPos pos) { ++ // Purpur start ++ Level level = world.getMinecraftWorld(); ++ if (level.purpurConfig.rabbitNaturalKiller > 0D && world.getRandom().nextDouble() <= level.purpurConfig.rabbitNaturalKiller) { ++ return Rabbit.Variant.EVIL; ++ } ++ // Purpur end + Holder holder = world.getBiome(pos); + int i = world.getRandom().nextInt(100); + +@@ -466,7 +561,7 @@ public class Rabbit extends Animal implements VariantHolder { + } + } + +- private static class RabbitMoveControl extends MoveControl { ++ private static class RabbitMoveControl extends org.purpurmc.purpur.controller.MoveControllerWASD { // Purpur + + private final Rabbit rabbit; + private double nextJumpSpeed; +@@ -477,14 +572,14 @@ public class Rabbit extends Animal implements VariantHolder { + } + + @Override +- public void tick() { ++ public void vanillaTick() { // Purpur + if (this.rabbit.onGround() && !this.rabbit.jumping && !((Rabbit.RabbitJumpControl) this.rabbit.jumpControl).wantJump()) { + this.rabbit.setSpeedModifier(0.0D); + } else if (this.hasWanted()) { + this.rabbit.setSpeedModifier(this.nextJumpSpeed); + } + +- super.tick(); ++ super.vanillaTick(); // Purpur + } + + @Override +@@ -546,7 +641,7 @@ public class Rabbit extends Animal implements VariantHolder { + @Override + public boolean canUse() { + if (this.nextStartTick <= 0) { +- if (!this.rabbit.level().getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING)) { ++ if (!this.rabbit.level().purpurConfig.rabbitBypassMobGriefing && !this.rabbit.level().getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING)) { // Purpur + return false; + } + +diff --git a/src/main/java/net/minecraft/world/entity/animal/Salmon.java b/src/main/java/net/minecraft/world/entity/animal/Salmon.java +index 0af79daa357f53a8871e293b57e16c099e5d3f64..382e47f26ee94506cb76463a677351b9bdcf8040 100644 +--- a/src/main/java/net/minecraft/world/entity/animal/Salmon.java ++++ b/src/main/java/net/minecraft/world/entity/animal/Salmon.java +@@ -13,6 +13,33 @@ public class Salmon extends AbstractSchoolingFish { + super(type, world); + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level().purpurConfig.salmonRidable; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level().purpurConfig.salmonControllable; ++ } ++ // Purpur end ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(net.minecraft.world.entity.ai.attributes.Attributes.MAX_HEALTH).setBaseValue(this.level().purpurConfig.salmonMaxHealth); ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level().purpurConfig.salmonTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level().purpurConfig.salmonAlwaysDropExp; ++ } ++ + @Override + public int getMaxSchoolSize() { + return 5; +diff --git a/src/main/java/net/minecraft/world/entity/animal/Sheep.java b/src/main/java/net/minecraft/world/entity/animal/Sheep.java +index 3ce86f952a18cae7fda1903916903b31a63a40b4..6b1244d3957e7f62c96ffd34692b8916337839fd 100644 +--- a/src/main/java/net/minecraft/world/entity/animal/Sheep.java ++++ b/src/main/java/net/minecraft/world/entity/animal/Sheep.java +@@ -117,10 +117,48 @@ public class Sheep extends Animal implements Shearable { + super(type, world); + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level().purpurConfig.sheepRidable; ++ } ++ ++ @Override ++ public boolean dismountsUnderwater() { ++ return level().purpurConfig.useDismountsUnderwaterTag ? super.dismountsUnderwater() : !level().purpurConfig.sheepRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level().purpurConfig.sheepControllable; ++ } ++ // Purpur end ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level().purpurConfig.sheepMaxHealth); ++ } ++ ++ @Override ++ public int getPurpurBreedTime() { ++ return this.level().purpurConfig.sheepBreedingTicks; ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level().purpurConfig.sheepTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level().purpurConfig.sheepAlwaysDropExp; ++ } ++ + @Override + protected void registerGoals() { + this.eatBlockGoal = new EatBlockGoal(this); + this.goalSelector.addGoal(0, new FloatGoal(this)); ++ this.goalSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.goalSelector.addGoal(1, new PanicGoal(this, 1.25D)); + this.goalSelector.addGoal(2, new BreedGoal(this, 1.0D)); + this.goalSelector.addGoal(3, new TemptGoal(this, 1.1D, (itemstack) -> { +@@ -259,7 +297,7 @@ public class Sheep extends Animal implements Shearable { + if (!this.level().isClientSide && this.readyForShearing()) { + // CraftBukkit start + // Paper start - custom shear drops +- java.util.List drops = this.generateDefaultDrops(); ++ java.util.List drops = this.generateDefaultDrops(net.minecraft.world.item.enchantment.EnchantmentHelper.getMobLooting(player)); // Purpur + org.bukkit.event.player.PlayerShearEntityEvent event = CraftEventFactory.handlePlayerShearEntityEvent(player, this, itemstack, hand, drops); + if (event != null) { + if (event.isCancelled()) { +@@ -284,12 +322,13 @@ public class Sheep extends Animal implements Shearable { + @Override + public void shear(SoundSource shearedSoundCategory) { + // Paper start - custom shear drops +- this.shear(shearedSoundCategory, this.generateDefaultDrops()); ++ this.shear(shearedSoundCategory, this.generateDefaultDrops(0)); // Purpur + } + + @Override +- public java.util.List generateDefaultDrops() { ++ public java.util.List generateDefaultDrops(int looting) { // Purpur + int count = 1 + this.random.nextInt(3); ++ if (org.purpurmc.purpur.PurpurConfig.allowShearsLooting) count += looting; // Purpur + java.util.List dropEntities = new java.util.ArrayList<>(count); + for (int j = 0; j < count; ++j) { + dropEntities.add(new ItemStack(Sheep.ITEM_BY_DYE.get(this.getColor()))); +diff --git a/src/main/java/net/minecraft/world/entity/animal/SnowGolem.java b/src/main/java/net/minecraft/world/entity/animal/SnowGolem.java +index 5c2ed3c39c8eb850f3be1e2ea5b5a7ea266e16d1..3b74931ae4e3a869d8db38c119e57b44af887859 100644 +--- a/src/main/java/net/minecraft/world/entity/animal/SnowGolem.java ++++ b/src/main/java/net/minecraft/world/entity/animal/SnowGolem.java +@@ -47,17 +47,56 @@ public class SnowGolem extends AbstractGolem implements Shearable, RangedAttackM + + private static final EntityDataAccessor DATA_PUMPKIN_ID = SynchedEntityData.defineId(SnowGolem.class, EntityDataSerializers.BYTE); + private static final byte PUMPKIN_FLAG = 16; ++ @Nullable private java.util.UUID summoner; // Purpur + + public SnowGolem(EntityType type, Level world) { + super(type, world); + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level().purpurConfig.snowGolemRidable; ++ } ++ ++ @Override ++ public boolean dismountsUnderwater() { ++ return level().purpurConfig.useDismountsUnderwaterTag ? super.dismountsUnderwater() : !level().purpurConfig.snowGolemRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level().purpurConfig.snowGolemControllable; ++ } ++ // Purpur end ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level().purpurConfig.snowGolemMaxHealth); ++ } ++ ++ @Nullable ++ public java.util.UUID getSummoner() { ++ return summoner; ++ } ++ ++ public void setSummoner(@Nullable java.util.UUID summoner) { ++ this.summoner = summoner; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level().purpurConfig.snowGolemAlwaysDropExp; ++ } ++ + @Override + protected void registerGoals() { +- this.goalSelector.addGoal(1, new RangedAttackGoal(this, 1.25D, 20, 10.0F)); ++ this.goalSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur ++ this.goalSelector.addGoal(1, new RangedAttackGoal(this, level().purpurConfig.snowGolemAttackDistance, level().purpurConfig.snowGolemSnowBallMin, level().purpurConfig.snowGolemSnowBallMax, level().purpurConfig.snowGolemSnowBallModifier)); // Purpur + this.goalSelector.addGoal(2, new WaterAvoidingRandomStrollGoal(this, 1.0D, 1.0000001E-5F)); + this.goalSelector.addGoal(3, new LookAtPlayerGoal(this, Player.class, 6.0F)); + this.goalSelector.addGoal(4, new RandomLookAroundGoal(this)); ++ this.targetSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.targetSelector.addGoal(1, new NearestAttackableTargetGoal<>(this, Mob.class, 10, true, false, (entityliving) -> { + return entityliving instanceof Enemy; + })); +@@ -77,6 +116,7 @@ public class SnowGolem extends AbstractGolem implements Shearable, RangedAttackM + public void addAdditionalSaveData(CompoundTag nbt) { + super.addAdditionalSaveData(nbt); + nbt.putBoolean("Pumpkin", this.hasPumpkin()); ++ if (getSummoner() != null) nbt.putUUID("Purpur.Summoner", getSummoner()); // Purpur + } + + @Override +@@ -85,12 +125,13 @@ public class SnowGolem extends AbstractGolem implements Shearable, RangedAttackM + if (nbt.contains("Pumpkin")) { + this.setPumpkin(nbt.getBoolean("Pumpkin")); + } ++ if (nbt.contains("Purpur.Summoner")) setSummoner(nbt.getUUID("Purpur.Summoner")); // Purpur + + } + + @Override + public boolean isSensitiveToWater() { +- return true; ++ return this.level().purpurConfig.snowGolemTakeDamageFromWater; // Purpur + } + + @Override +@@ -101,10 +142,11 @@ public class SnowGolem extends AbstractGolem implements Shearable, RangedAttackM + this.hurt(this.damageSources().melting(), 1.0F); // CraftBukkit - DamageSources.ON_FIRE -> CraftEventFactory.MELTING + } + +- if (!this.level().getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING)) { ++ if (!this.level().purpurConfig.snowGolemBypassMobGriefing && !this.level().getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING)) { // Purpur + return; + } + ++ if (getRider() != null && this.isControllable() && !level().purpurConfig.snowGolemLeaveTrailWhenRidden) return; // Purpur - don't leave snow trail when being ridden + BlockState iblockdata = Blocks.SNOW.defaultBlockState(); + + for (int i = 0; i < 4; ++i) { +@@ -147,11 +189,11 @@ public class SnowGolem extends AbstractGolem implements Shearable, RangedAttackM + if (itemstack.is(Items.SHEARS) && this.readyForShearing()) { + // CraftBukkit start + // Paper start - custom shear drops +- java.util.List drops = this.generateDefaultDrops(); ++ java.util.List drops = this.generateDefaultDrops(net.minecraft.world.item.enchantment.EnchantmentHelper.getMobLooting(player)); // Purpur + org.bukkit.event.player.PlayerShearEntityEvent event = CraftEventFactory.handlePlayerShearEntityEvent(player, this, itemstack, hand, drops); + if (event != null) { + if (event.isCancelled()) { +- return InteractionResult.PASS; ++ return tryRide(player, hand); // Purpur + } + drops = org.bukkit.craftbukkit.inventory.CraftItemStack.asNMSCopy(event.getDrops()); + } +@@ -164,19 +206,36 @@ public class SnowGolem extends AbstractGolem implements Shearable, RangedAttackM + } + + return InteractionResult.sidedSuccess(this.level().isClientSide); ++ // Purpur start ++ } else if (level().purpurConfig.snowGolemPutPumpkinBack && !hasPumpkin() && itemstack.getItem() == Blocks.CARVED_PUMPKIN.asItem()) { ++ setPumpkin(true); ++ if (!player.getAbilities().instabuild) { ++ itemstack.shrink(1); ++ } ++ return InteractionResult.SUCCESS; ++ // Purpur end + } else { +- return InteractionResult.PASS; ++ return tryRide(player, hand); // Purpur + } + } + + @Override + public void shear(SoundSource shearedSoundCategory) { + // Paper start - custom shear drops +- this.shear(shearedSoundCategory, this.generateDefaultDrops()); ++ this.shear(shearedSoundCategory, this.generateDefaultDrops(0)); // Purpur + } + + @Override +- public java.util.List generateDefaultDrops() { ++ // Purpur start ++ public java.util.List generateDefaultDrops(int looting) { ++ if (org.purpurmc.purpur.PurpurConfig.allowShearsLooting) { ++ java.util.ArrayList list = new java.util.ArrayList<>(); ++ for (int i = 0; i < 1 + looting; i++) { ++ list.add(new ItemStack(Items.CARVED_PUMPKIN)); ++ } ++ return java.util.Collections.unmodifiableList(list); ++ } ++ // Purpur end + return java.util.Collections.singletonList(new ItemStack(Items.CARVED_PUMPKIN)); + } + +diff --git a/src/main/java/net/minecraft/world/entity/animal/Squid.java b/src/main/java/net/minecraft/world/entity/animal/Squid.java +index 43b4ea96c5c4a6234e5b83d41db9b85c1fe27b8f..cb950ba3ee3bdfe0ff7acdb94c7ee233d73ab22e 100644 +--- a/src/main/java/net/minecraft/world/entity/animal/Squid.java ++++ b/src/main/java/net/minecraft/world/entity/animal/Squid.java +@@ -42,13 +42,66 @@ public class Squid extends WaterAnimal { + + public Squid(EntityType type, Level world) { + super(type, world); +- //this.random.setSeed((long)this.getId()); // Paper - Share random for entities to make them more random ++ if (!world.purpurConfig.entitySharedRandom) this.random.setSeed((long)this.getId()); // Paper - Share random for entities to make them more random // Purpur + this.tentacleSpeed = 1.0F / (this.random.nextFloat() + 1.0F) * 0.2F; + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level().purpurConfig.squidRidable; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level().purpurConfig.squidControllable; ++ } ++ ++ protected void rotateVectorAroundY(org.bukkit.util.Vector vector, double degrees) { ++ double rad = Math.toRadians(degrees); ++ double cos = Math.cos(rad); ++ double sine = Math.sin(rad); ++ double x = vector.getX(); ++ double z = vector.getZ(); ++ vector.setX(cos * x - sine * z); ++ vector.setZ(sine * x + cos * z); ++ } ++ // Purpur end ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level().purpurConfig.squidMaxHealth); ++ } ++ ++ @Override ++ public net.minecraft.world.phys.AABB getAxisForFluidCheck() { ++ // Stops squids from floating just over the water ++ return super.getAxisForFluidCheck().offsetY(level().purpurConfig.squidOffsetWaterCheck); ++ } ++ ++ public boolean canFly() { ++ return this.level().purpurConfig.squidsCanFly; ++ } ++ ++ @Override ++ public boolean isInWater() { ++ return this.wasTouchingWater || canFly(); ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level().purpurConfig.squidTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level().purpurConfig.squidAlwaysDropExp; ++ } ++ + @Override + protected void registerGoals() { + this.goalSelector.addGoal(0, new Squid.SquidRandomMovementGoal(this)); ++ this.goalSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.goalSelector.addGoal(1, new Squid.SquidFleeGoal()); + } + +@@ -117,6 +170,7 @@ public class Squid extends WaterAnimal { + } + + if (this.isInWaterOrBubble()) { ++ if (canFly()) setNoGravity(!wasTouchingWater); // Purpur + if (this.tentacleMovement < (float) Math.PI) { + float f = this.tentacleMovement / (float) Math.PI; + this.tentacleAngle = Mth.sin(f * f * (float) Math.PI) * (float) Math.PI * 0.25F; +@@ -290,10 +344,41 @@ public class Squid extends WaterAnimal { + + @Override + public void tick() { ++ // Purpur start ++ Player rider = squid.getRider(); ++ if (rider != null && squid.isControllable()) { ++ if (rider.jumping) { ++ squid.onSpacebar(); ++ } ++ float forward = rider.getForwardMot(); ++ float strafe = rider.getStrafeMot(); ++ float speed = (float) squid.getAttributeValue(Attributes.MOVEMENT_SPEED) * 10F; ++ if (forward < 0.0F) { ++ speed *= -0.5; ++ } ++ org.bukkit.util.Vector dir = rider.getBukkitEntity().getEyeLocation().getDirection().normalize().multiply(speed / 20.0F); ++ if (strafe != 0.0F) { ++ if (forward == 0.0F) { ++ dir.setY(0); ++ rotateVectorAroundY(dir, strafe > 0.0F ? -90 : 90); ++ } else if (forward < 0.0F) { ++ rotateVectorAroundY(dir, strafe > 0.0F ? 45 : -45); ++ } else { ++ rotateVectorAroundY(dir, strafe > 0.0F ? -45 : 45); ++ } ++ } ++ if (forward != 0.0F || strafe != 0.0F) { ++ squid.setMovementVector((float) dir.getX(), (float) dir.getY(), (float) dir.getZ()); ++ } else { ++ squid.setMovementVector(0.0F, 0.0F, 0.0F); ++ } ++ return; ++ } ++ // Purpur end + int i = this.squid.getNoActionTime(); + if (i > 100) { + this.squid.setMovementVector(0.0F, 0.0F, 0.0F); +- } else if (this.squid.getRandom().nextInt(reducedTickDelay(50)) == 0 || !this.squid.wasTouchingWater || !this.squid.hasMovementVector()) { ++ } else if (this.squid.getRandom().nextInt(reducedTickDelay(50)) == 0 || !this.squid.isInWater() || !this.squid.hasMovementVector()) { // Purpur + float f = this.squid.getRandom().nextFloat() * (float) (Math.PI * 2); + float g = Mth.cos(f) * 0.2F; + float h = -0.1F + this.squid.getRandom().nextFloat() * 0.2F; +diff --git a/src/main/java/net/minecraft/world/entity/animal/TropicalFish.java b/src/main/java/net/minecraft/world/entity/animal/TropicalFish.java +index 3d03ffe2e12eca82dfa2f414471d12bb362d4552..2d04addd17d2c358fff598012b323cd7d8bf007e 100644 +--- a/src/main/java/net/minecraft/world/entity/animal/TropicalFish.java ++++ b/src/main/java/net/minecraft/world/entity/animal/TropicalFish.java +@@ -67,6 +67,33 @@ public class TropicalFish extends AbstractSchoolingFish implements VariantHolder + super(type, world); + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level().purpurConfig.tropicalFishRidable; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level().purpurConfig.tropicalFishControllable; ++ } ++ // Purpur end ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(net.minecraft.world.entity.ai.attributes.Attributes.MAX_HEALTH).setBaseValue(this.level().purpurConfig.tropicalFishMaxHealth); ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level().purpurConfig.tropicalFishTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level().purpurConfig.tropicalFishAlwaysDropExp; ++ } ++ + public static String getPredefinedName(int variant) { + return "entity.minecraft.tropical_fish.predefined." + variant; + } +diff --git a/src/main/java/net/minecraft/world/entity/animal/Turtle.java b/src/main/java/net/minecraft/world/entity/animal/Turtle.java +index 30b87b5cb18c25cdd04eab64cfbe5acd6c1b6d84..01dc59695f295657b1cd7bb015558bfc2ce73b47 100644 +--- a/src/main/java/net/minecraft/world/entity/animal/Turtle.java ++++ b/src/main/java/net/minecraft/world/entity/animal/Turtle.java +@@ -87,6 +87,43 @@ public class Turtle extends Animal { + this.moveControl = new Turtle.TurtleMoveControl(this); + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level().purpurConfig.turtleRidable; ++ } ++ ++ @Override ++ public boolean dismountsUnderwater() { ++ return level().purpurConfig.useDismountsUnderwaterTag ? super.dismountsUnderwater() : !level().purpurConfig.turtleRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level().purpurConfig.turtleControllable; ++ } ++ // Purpur end ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level().purpurConfig.turtleMaxHealth); ++ } ++ ++ @Override ++ public int getPurpurBreedTime() { ++ return this.level().purpurConfig.turtleBreedingTicks; ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level().purpurConfig.turtleTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level().purpurConfig.turtleAlwaysDropExp; ++ } ++ + public void setHomePos(BlockPos pos) { + this.entityData.set(Turtle.HOME_POS, pos.immutable()); // Paper - called with mutablepos... + } +@@ -189,6 +226,7 @@ public class Turtle extends Animal { + + @Override + protected void registerGoals() { ++ this.goalSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.goalSelector.addGoal(0, new Turtle.TurtlePanicGoal(this, 1.2D)); + this.goalSelector.addGoal(1, new Turtle.TurtleBreedGoal(this, 1.0D)); + this.goalSelector.addGoal(1, new Turtle.TurtleLayEggGoal(this, 1.0D)); +@@ -342,13 +380,15 @@ public class Turtle extends Animal { + return this.isBaby() ? Turtle.BABY_DIMENSIONS : super.getDefaultDimensions(pose); + } + +- private static class TurtleMoveControl extends MoveControl { ++ private static class TurtleMoveControl extends org.purpurmc.purpur.controller.MoveControllerWASD { // Purpur + + private final Turtle turtle; ++ private final org.purpurmc.purpur.controller.WaterMoveControllerWASD waterController; // Purpur + + TurtleMoveControl(Turtle turtle) { + super(turtle); + this.turtle = turtle; ++ waterController = new org.purpurmc.purpur.controller.WaterMoveControllerWASD(turtle, 0.25D); // Purpur + } + + private void updateSpeed() { +@@ -368,7 +408,7 @@ public class Turtle extends Animal { + } + + @Override +- public void tick() { ++ public void vanillaTick() { // Purpur + this.updateSpeed(); + if (this.operation == MoveControl.Operation.MOVE_TO && !this.turtle.getNavigation().isDone()) { + double d0 = this.wantedX - this.turtle.getX(); +@@ -384,7 +424,7 @@ public class Turtle extends Animal { + + this.turtle.setYRot(this.rotlerp(this.turtle.getYRot(), f, 90.0F)); + this.turtle.yBodyRot = this.turtle.getYRot(); +- float f1 = (float) (this.speedModifier * this.turtle.getAttributeValue(Attributes.MOVEMENT_SPEED)); ++ float f1 = (float) (this.getSpeedModifier() * this.turtle.getAttributeValue(Attributes.MOVEMENT_SPEED)); + + this.turtle.setSpeed(Mth.lerp(0.125F, this.turtle.getSpeed(), f1)); + this.turtle.setDeltaMovement(this.turtle.getDeltaMovement().add(0.0D, (double) this.turtle.getSpeed() * d1 * 0.1D, 0.0D)); +diff --git a/src/main/java/net/minecraft/world/entity/animal/WaterAnimal.java b/src/main/java/net/minecraft/world/entity/animal/WaterAnimal.java +index 6f22705072fecbe91196e4966fca2eeec060f120..ed2ae44f7cef5aed17d10cc8a7df0a2276f9f16b 100644 +--- a/src/main/java/net/minecraft/world/entity/animal/WaterAnimal.java ++++ b/src/main/java/net/minecraft/world/entity/animal/WaterAnimal.java +@@ -72,6 +72,6 @@ public abstract class WaterAnimal extends PathfinderMob { + i = world.getMinecraftWorld().paperConfig().entities.spawning.wateranimalSpawnHeight.maximum.or(i); + j = world.getMinecraftWorld().paperConfig().entities.spawning.wateranimalSpawnHeight.minimum.or(j); + // Paper end - Make water animal spawn height configurable +- return pos.getY() >= j && pos.getY() <= i && world.getFluidState(pos.below()).is(FluidTags.WATER) && world.getBlockState(pos.above()).is(Blocks.WATER); ++ return ((reason == MobSpawnType.SPAWNER && world.getMinecraftWorld().purpurConfig.spawnerFixMC238526) || (pos.getY() >= j && pos.getY() <= i)) && world.getFluidState(pos.below()).is(FluidTags.WATER) && world.getBlockState(pos.above()).is(Blocks.WATER); // Purpur + } + } +diff --git a/src/main/java/net/minecraft/world/entity/animal/Wolf.java b/src/main/java/net/minecraft/world/entity/animal/Wolf.java +index b5ee82e5abfecc59e2362628f288b76881855f36..b12544d1280f39b6c365317a0f4965c8d65b6497 100644 +--- a/src/main/java/net/minecraft/world/entity/animal/Wolf.java ++++ b/src/main/java/net/minecraft/world/entity/animal/Wolf.java +@@ -30,6 +30,8 @@ import net.minecraft.world.DifficultyInstance; + import net.minecraft.world.InteractionHand; + import net.minecraft.world.InteractionResult; + import net.minecraft.world.damagesource.DamageSource; ++import net.minecraft.world.effect.MobEffectInstance; ++import net.minecraft.world.effect.MobEffects; + import net.minecraft.world.entity.AgeableMob; + import net.minecraft.world.entity.Crackiness; + import net.minecraft.world.entity.Entity; +@@ -104,6 +106,37 @@ public class Wolf extends TamableAnimal implements NeutralMob, VariantHolder RABID_PREDICATE = entity -> entity instanceof net.minecraft.server.level.ServerPlayer || entity instanceof Mob; ++ private final net.minecraft.world.entity.ai.goal.Goal PATHFINDER_VANILLA = new NonTameRandomTargetGoal<>(this, Animal.class, false, PREY_SELECTOR); ++ private final net.minecraft.world.entity.ai.goal.Goal PATHFINDER_RABID = new NonTameRandomTargetGoal<>(this, LivingEntity.class, false, RABID_PREDICATE); ++ private static final class AvoidRabidWolfGoal extends AvoidEntityGoal { ++ private final Wolf wolf; ++ ++ public AvoidRabidWolfGoal(Wolf wolf, float distance, double minSpeed, double maxSpeed) { ++ super(wolf, Wolf.class, distance, minSpeed, maxSpeed); ++ this.wolf = wolf; ++ } ++ ++ @Override ++ public boolean canUse() { ++ return super.canUse() && !this.wolf.isRabid() && this.toAvoid != null && this.toAvoid.isRabid(); // wolves which are not rabid run away from rabid wolves ++ } ++ ++ @Override ++ public void start() { ++ this.wolf.setTarget(null); ++ super.start(); ++ } ++ ++ @Override ++ public void tick() { ++ this.wolf.setTarget(null); ++ super.tick(); ++ } ++ } ++ // Purpur end + private static final float START_HEALTH = 8.0F; + private static final float TAME_HEALTH = 40.0F; + private static final float ARMOR_REPAIR_UNIT = 0.125F; +@@ -124,12 +157,86 @@ public class Wolf extends TamableAnimal implements NeutralMob, VariantHolder(this, Llama.class, 24.0F, 1.5D, 1.5D)); ++ this.goalSelector.addGoal(3, new AvoidRabidWolfGoal(this, 24.0F, 1.5D, 1.5D)); // Purpur + this.goalSelector.addGoal(4, new LeapAtTargetGoal(this, 0.4F)); + this.goalSelector.addGoal(5, new MeleeAttackGoal(this, 1.0D, true)); + this.goalSelector.addGoal(6, new FollowOwnerGoal(this, 1.0D, 10.0F, 2.0F, false)); +@@ -138,11 +245,12 @@ public class Wolf extends TamableAnimal implements NeutralMob, VariantHolder(this, Player.class, 10, true, false, this::isAngryAt)); +- this.targetSelector.addGoal(5, new NonTameRandomTargetGoal<>(this, Animal.class, false, Wolf.PREY_SELECTOR)); ++ // this.targetSelector.addGoal(5, new NonTameRandomTargetGoal<>(this, Animal.class, false, Wolf.PREY_SELECTOR)); // Purpur - moved to updatePathfinders() + this.targetSelector.addGoal(6, new NonTameRandomTargetGoal<>(this, Turtle.class, false, Turtle.BABY_ON_LAND_SELECTOR)); + this.targetSelector.addGoal(7, new NearestAttackableTargetGoal<>(this, AbstractSkeleton.class, false)); + this.targetSelector.addGoal(8, new ResetUniversalAngerTargetGoal<>(this, true)); +@@ -185,6 +293,7 @@ public class Wolf extends TamableAnimal implements NeutralMob, VariantHolder 0.0D && random.nextDouble() <= world.getLevel().purpurConfig.wolfNaturalRabid; ++ this.updatePathfinders(false); ++ // Purpur end ++ + return super.finalizeSpawn(world, difficulty, spawnReason, (SpawnGroupData) entityData); + } + +@@ -261,6 +380,11 @@ public class Wolf extends TamableAnimal implements NeutralMob, VariantHolder type, Level world) { + super(type, world); +- this.moveControl = new FlyingMoveControl(this, 20, true); ++ // Purpur start ++ this.purpurController = new org.purpurmc.purpur.controller.FlyingMoveControllerWASD(this, 0.1F, 0.5F); ++ this.moveControl = new FlyingMoveControl(this, 20, true) { ++ @Override ++ public void tick() { ++ if (mob.getRider() != null && mob.isControllable()) { ++ purpurController.purpurTick(mob.getRider()); ++ } else { ++ super.tick(); ++ } ++ } ++ }; ++ // Purpur end + this.setCanPickUpLoot(this.canPickUpLoot()); + this.vibrationUser = new Allay.VibrationUser(); + this.vibrationData = new VibrationSystem.Data(); +@@ -117,6 +130,28 @@ public class Allay extends PathfinderMob implements InventoryCarrier, VibrationS + } + // CraftBukkit end + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level().purpurConfig.allayRidable; ++ } ++ ++ @Override ++ public boolean dismountsUnderwater() { ++ return level().purpurConfig.useDismountsUnderwaterTag ? super.dismountsUnderwater() : !level().purpurConfig.allayRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level().purpurConfig.allayControllable; ++ } ++ ++ @Override ++ protected void registerGoals() { ++ this.goalSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur ++ } ++ // Purpur end ++ + @Override + protected Brain.Provider brainProvider() { + return Brain.provider(Allay.MEMORY_TYPES, Allay.SENSOR_TYPES); +@@ -217,12 +252,13 @@ public class Allay extends PathfinderMob implements InventoryCarrier, VibrationS + + @Override + protected void customServerAiStep() { +- this.level().getProfiler().push("allayBrain"); ++ //this.level().getProfiler().push("allayBrain"); // Purpur ++ //if ((getRider() == null || !this.isControllable()) && this.behaviorTick++ % this.activatedPriority == 0) // Pufferfish // Purpur - only use brain if no rider // Purpur - TODO: Pufferfish + this.getBrain().tick((ServerLevel) this.level(), this); +- this.level().getProfiler().pop(); +- this.level().getProfiler().push("allayActivityUpdate"); ++ //this.level().getProfiler().pop(); // Purpur ++ //this.level().getProfiler().push("allayActivityUpdate"); // Purpur + AllayAi.updateActivity(this); +- this.level().getProfiler().pop(); ++ //this.level().getProfiler().pop(); // Purpur + super.customServerAiStep(); + } + +diff --git a/src/main/java/net/minecraft/world/entity/animal/armadillo/Armadillo.java b/src/main/java/net/minecraft/world/entity/animal/armadillo/Armadillo.java +index b38281f963377cc82b360e8457da7cad033b8c36..f8790ab5b7c1279719271ee57c00f4f2d6ce9714 100644 +--- a/src/main/java/net/minecraft/world/entity/animal/armadillo/Armadillo.java ++++ b/src/main/java/net/minecraft/world/entity/animal/armadillo/Armadillo.java +@@ -130,12 +130,12 @@ public class Armadillo extends Animal { + + @Override + protected void customServerAiStep() { +- this.level().getProfiler().push("armadilloBrain"); ++ //this.level().getProfiler().push("armadilloBrain"); // Purpur + ((Brain) this.brain).tick((ServerLevel) this.level(), this); // CraftBukkit - decompile error +- this.level().getProfiler().pop(); +- this.level().getProfiler().push("armadilloActivityUpdate"); ++ //this.level().getProfiler().pop(); // Purpur ++ //this.level().getProfiler().push("armadilloActivityUpdate"); // Purpur + ArmadilloAi.updateActivity(this); +- this.level().getProfiler().pop(); ++ //this.level().getProfiler().pop(); // Purpur + if (this.isAlive() && !this.isBaby() && --this.scuteTime <= 0) { + this.playSound(SoundEvents.ARMADILLO_SCUTE_DROP, 1.0F, (this.random.nextFloat() - this.random.nextFloat()) * 0.2F + 1.0F); + this.forceDrops = true; // CraftBukkit +@@ -469,4 +469,11 @@ public class Armadillo extends Animal { + return this.animationDuration; + } + } ++ ++ // Purpur start ++ @Override ++ public int getPurpurBreedTime() { ++ return 6000; ++ } ++ // Purpur end + } +diff --git a/src/main/java/net/minecraft/world/entity/animal/axolotl/Axolotl.java b/src/main/java/net/minecraft/world/entity/animal/axolotl/Axolotl.java +index a8cc6ddbf45370fe632e5c5fb7ceef3d299e62a4..d330f79e860662bc93a1703215e66e6564d181b9 100644 +--- a/src/main/java/net/minecraft/world/entity/animal/axolotl/Axolotl.java ++++ b/src/main/java/net/minecraft/world/entity/animal/axolotl/Axolotl.java +@@ -96,6 +96,43 @@ public class Axolotl extends Animal implements LerpingModel, VariantHolder getModelRotationValues() { + return this.modelRotationValues; +@@ -270,12 +307,13 @@ public class Axolotl extends Animal implements LerpingModel, VariantHolder optional = this.getBrain().getMemory(MemoryModuleType.PLAY_DEAD_TICKS); + +@@ -503,14 +541,22 @@ public class Axolotl extends Animal implements LerpingModel, VariantHolder behaviorcontroller = (Brain) this.getBrain(); // CraftBukkit - decompile error + + behaviorcontroller.tick((ServerLevel) this.level(), this); +- this.level().getProfiler().pop(); +- this.level().getProfiler().push("camelActivityUpdate"); ++ //this.level().getProfiler().pop(); // Purpur ++ //this.level().getProfiler().push("camelActivityUpdate"); // Purpur + CamelAi.updateActivity(this); +- this.level().getProfiler().pop(); ++ //this.level().getProfiler().pop(); // Purpur + super.customServerAiStep(); + } + +@@ -309,6 +320,23 @@ public class Camel extends AbstractHorse implements PlayerRideableJumping, Saddl + return this.dashCooldown; + } + ++ // Purpur start ++ @Override ++ public float generateMaxHealth(net.minecraft.util.RandomSource random) { ++ return (float) generateMaxHealth(this.level().purpurConfig.camelMaxHealthMin, this.level().purpurConfig.camelMaxHealthMax); ++ } ++ ++ @Override ++ public double generateJumpStrength(net.minecraft.util.RandomSource random) { ++ return generateJumpStrength(this.level().purpurConfig.camelJumpStrengthMin, this.level().purpurConfig.camelJumpStrengthMax); ++ } ++ ++ @Override ++ public double generateSpeed(net.minecraft.util.RandomSource random) { ++ return generateSpeed(this.level().purpurConfig.camelMovementSpeedMin, this.level().purpurConfig.camelMovementSpeedMax); ++ } ++ // Purpur end ++ + @Override + protected SoundEvent getAmbientSound() { + return SoundEvents.CAMEL_AMBIENT; +diff --git a/src/main/java/net/minecraft/world/entity/animal/frog/Frog.java b/src/main/java/net/minecraft/world/entity/animal/frog/Frog.java +index 816977990639ec0559b652fc9666afd5046f0a5d..ee8c232ddaa518377bdfa54e83ffc04f7a2f2c9a 100644 +--- a/src/main/java/net/minecraft/world/entity/animal/frog/Frog.java ++++ b/src/main/java/net/minecraft/world/entity/animal/frog/Frog.java +@@ -103,6 +103,8 @@ public class Frog extends Animal implements VariantHolder> { + public final AnimationState croakAnimationState = new AnimationState(); + public final AnimationState tongueAnimationState = new AnimationState(); + public final AnimationState swimIdleAnimationState = new AnimationState(); ++ private org.purpurmc.purpur.controller.MoveControllerWASD purpurLandController; // Purpur ++ private org.purpurmc.purpur.controller.WaterMoveControllerWASD purpurWaterController; // Purpur + + public Frog(EntityType type, Level world) { + super(type, world); +@@ -110,6 +112,58 @@ public class Frog extends Animal implements VariantHolder> { + this.setPathfindingMalus(PathType.WATER, 4.0F); + this.setPathfindingMalus(PathType.TRAPDOOR, -1.0F); + this.moveControl = new SmoothSwimmingMoveControl(this, 85, 10, 0.02F, 0.1F, true); ++ // Purpur start ++ this.purpurLandController = new org.purpurmc.purpur.controller.MoveControllerWASD(this, 0.2F); ++ this.purpurWaterController = new org.purpurmc.purpur.controller.WaterMoveControllerWASD(this, 0.5F); ++ this.moveControl = new SmoothSwimmingMoveControl(this, 85, 10, 0.02F, 0.1F, true) { ++ @Override ++ public void tick() { ++ net.minecraft.world.entity.player.Player rider = mob.getRider(); ++ if (rider != null && mob.isControllable()) { ++ if (mob.isInWater()) { ++ purpurWaterController.purpurTick(rider); ++ mob.setDeltaMovement(mob.getDeltaMovement().add(0.0D, -0.005D, 0.0D)); ++ } else { ++ purpurLandController.purpurTick(rider); ++ } ++ } else { ++ super.tick(); ++ } ++ } ++ }; ++ // Purpur end ++ } ++ ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level().purpurConfig.frogRidable; ++ } ++ ++ @Override ++ public boolean dismountsUnderwater() { ++ return level().purpurConfig.useDismountsUnderwaterTag ? super.dismountsUnderwater() : !level().purpurConfig.frogRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level().purpurConfig.frogControllable; ++ } ++ ++ @Override ++ protected void registerGoals() { ++ this.goalSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur ++ this.targetSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur ++ } ++ ++ @Override ++ public float getJumpPower() { ++ return (getRider() != null && isControllable()) ? level().purpurConfig.frogRidableJumpHeight * this.getBlockJumpFactor() : super.getJumpPower(); ++ } ++ // Purpur end ++ ++ public int getPurpurBreedTime() { ++ return this.level().purpurConfig.frogBreedingTicks; + } + + @Override +@@ -183,12 +237,13 @@ public class Frog extends Animal implements VariantHolder> { + + @Override + protected void customServerAiStep() { +- this.level().getProfiler().push("frogBrain"); ++ //this.level().getProfiler().push("frogBrain"); // Purpur ++ //if ((getRider() == null || !this.isControllable()) && this.behaviorTick++ % this.activatedPriority == 0) // Pufferfish // Purpur - only use brain if no rider // Purpur - TODO: Pufferfish + this.getBrain().tick((ServerLevel)this.level(), this); +- this.level().getProfiler().pop(); +- this.level().getProfiler().push("frogActivityUpdate"); ++ //this.level().getProfiler().pop(); // Purpur ++ //this.level().getProfiler().push("frogActivityUpdate"); // Purpur + FrogAi.updateActivity(this); +- this.level().getProfiler().pop(); ++ //this.level().getProfiler().pop(); // Purpur + super.customServerAiStep(); + } + +@@ -371,7 +426,7 @@ public class Frog extends Animal implements VariantHolder> { + return world.getBlockState(pos.below()).is(BlockTags.FROGS_SPAWNABLE_ON) && isBrightEnoughToSpawn(world, pos); + } + +- class FrogLookControl extends LookControl { ++ class FrogLookControl extends org.purpurmc.purpur.controller.LookControllerWASD { // Purpur + FrogLookControl(final Mob entity) { + super(entity); + } +diff --git a/src/main/java/net/minecraft/world/entity/animal/frog/Tadpole.java b/src/main/java/net/minecraft/world/entity/animal/frog/Tadpole.java +index 290d41136f5ec7671bc4990dfe50da0a770c124d..09c4cf772df4644413e40055fedcdf42ee8064fd 100644 +--- a/src/main/java/net/minecraft/world/entity/animal/frog/Tadpole.java ++++ b/src/main/java/net/minecraft/world/entity/animal/frog/Tadpole.java +@@ -51,13 +51,50 @@ public class Tadpole extends AbstractFish { + protected static final ImmutableList>> SENSOR_TYPES = ImmutableList.of(SensorType.NEAREST_LIVING_ENTITIES, SensorType.NEAREST_PLAYERS, SensorType.HURT_BY, SensorType.FROG_TEMPTATIONS); + protected static final ImmutableList> MEMORY_TYPES = ImmutableList.of(MemoryModuleType.LOOK_TARGET, MemoryModuleType.NEAREST_VISIBLE_LIVING_ENTITIES, MemoryModuleType.WALK_TARGET, MemoryModuleType.CANT_REACH_WALK_TARGET_SINCE, MemoryModuleType.PATH, MemoryModuleType.NEAREST_VISIBLE_ADULT, MemoryModuleType.TEMPTATION_COOLDOWN_TICKS, MemoryModuleType.IS_TEMPTED, MemoryModuleType.TEMPTING_PLAYER, MemoryModuleType.BREED_TARGET, MemoryModuleType.IS_PANICKING); + public boolean ageLocked; // Paper ++ private org.purpurmc.purpur.controller.WaterMoveControllerWASD purpurController; // Purpur + + public Tadpole(EntityType type, Level world) { + super(type, world); +- this.moveControl = new SmoothSwimmingMoveControl(this, 85, 10, 0.02F, 0.1F, true); ++ // Purpur start ++ this.purpurController = new org.purpurmc.purpur.controller.WaterMoveControllerWASD(this, 0.5F); ++ this.moveControl = new SmoothSwimmingMoveControl(this, 85, 10, 0.02F, 0.1F, true) { ++ @Override ++ public void tick() { ++ Player rider = mob.getRider(); ++ if (rider != null && mob.isControllable()) { ++ purpurController.purpurTick(rider); ++ mob.setDeltaMovement(mob.getDeltaMovement().add(0.0D, 0.002D, 0.0D)); ++ } else { ++ super.tick(); ++ } ++ } ++ }; ++ // Purpur end + this.lookControl = new SmoothSwimmingLookControl(this, 10); + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level().purpurConfig.tadpoleRidable; ++ } ++ ++ @Override ++ public boolean dismountsUnderwater() { ++ return level().purpurConfig.useDismountsUnderwaterTag ? super.dismountsUnderwater() : !level().purpurConfig.tadpoleRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level().purpurConfig.tadpoleControllable; ++ } ++ ++ @Override ++ protected void registerGoals() { ++ this.goalSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur ++ } ++ // Purpur end ++ + @Override + protected PathNavigation createNavigation(Level world) { + return new WaterBoundPathNavigation(this, world); +@@ -85,12 +122,13 @@ public class Tadpole extends AbstractFish { + + @Override + protected void customServerAiStep() { +- this.level().getProfiler().push("tadpoleBrain"); ++ //this.level().getProfiler().push("tadpoleBrain"); // Purpur ++ //if ((getRider() == null || !this.isControllable()) && this.behaviorTick++ % this.activatedPriority == 0) // Pufferfish // Purpur - only use brain if no rider // Purpur - TODO: Pufferfish + this.getBrain().tick((ServerLevel) this.level(), this); +- this.level().getProfiler().pop(); +- this.level().getProfiler().push("tadpoleActivityUpdate"); ++ //this.level().getProfiler().pop(); // Purpur ++ //this.level().getProfiler().push("tadpoleActivityUpdate"); // Purpur + TadpoleAi.updateActivity(this); +- this.level().getProfiler().pop(); ++ //this.level().getProfiler().pop(); // Purpur + super.customServerAiStep(); + } + +diff --git a/src/main/java/net/minecraft/world/entity/animal/goat/Goat.java b/src/main/java/net/minecraft/world/entity/animal/goat/Goat.java +index 02e49c7ae5e120302b6479cf3e3934b9217eebf0..6a3c68839d3b993c82fabd4e17f53e38ce9c38f7 100644 +--- a/src/main/java/net/minecraft/world/entity/animal/goat/Goat.java ++++ b/src/main/java/net/minecraft/world/entity/animal/goat/Goat.java +@@ -91,6 +91,38 @@ public class Goat extends Animal { + return InstrumentItem.create(Items.GOAT_HORN, (Holder) holderset.getRandomElement(randomsource).get()); + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level().purpurConfig.goatRidable; ++ } ++ ++ @Override ++ public boolean dismountsUnderwater() { ++ return level().purpurConfig.useDismountsUnderwaterTag ? super.dismountsUnderwater() : !level().purpurConfig.goatRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level().purpurConfig.goatControllable; ++ } ++ // Purpur end ++ ++ @Override ++ public int getPurpurBreedTime() { ++ return this.level().purpurConfig.goatBreedingTicks; ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level().purpurConfig.goatTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level().purpurConfig.goatAlwaysDropExp; ++ } ++ + @Override + protected Brain.Provider brainProvider() { + return Brain.provider(Goat.MEMORY_TYPES, Goat.SENSOR_TYPES); +@@ -192,12 +224,13 @@ public class Goat extends Animal { + + @Override + protected void customServerAiStep() { +- this.level().getProfiler().push("goatBrain"); ++ //this.level().getProfiler().push("goatBrain"); // Purpur ++ //if ((getRider() == null || !this.isControllable()) && this.behaviorTick++ % this.activatedPriority == 0) // Pufferfish // Purpur - only use brain if no rider // Purpur - TODO: Pufferfish + this.getBrain().tick((ServerLevel) this.level(), this); +- this.level().getProfiler().pop(); +- this.level().getProfiler().push("goatActivityUpdate"); ++ //this.level().getProfiler().pop(); // Purpur ++ //this.level().getProfiler().push("goatActivityUpdate"); // Purpur + GoatAi.updateActivity(this); +- this.level().getProfiler().pop(); ++ //this.level().getProfiler().pop(); // Purpur + super.customServerAiStep(); + } + +@@ -394,6 +427,7 @@ public class Goat extends Animal { + + // Paper start - Goat ram API + public void ram(net.minecraft.world.entity.LivingEntity entity) { ++ if(!new org.purpurmc.purpur.event.entity.GoatRamEntityEvent((org.bukkit.entity.Goat) getBukkitEntity(), (org.bukkit.entity.LivingEntity) entity.getBukkitLivingEntity()).callEvent()) return; // Purpur + Brain brain = this.getBrain(); + brain.setMemory(MemoryModuleType.RAM_TARGET, entity.position()); + brain.eraseMemory(MemoryModuleType.RAM_COOLDOWN_TICKS); +diff --git a/src/main/java/net/minecraft/world/entity/animal/horse/AbstractHorse.java b/src/main/java/net/minecraft/world/entity/animal/horse/AbstractHorse.java +index 9357cf0179d19fbdfe76413e909a99b924c85780..59829fb7342696d29aa709d392f89bf263257fd3 100644 +--- a/src/main/java/net/minecraft/world/entity/animal/horse/AbstractHorse.java ++++ b/src/main/java/net/minecraft/world/entity/animal/horse/AbstractHorse.java +@@ -217,11 +217,59 @@ public abstract class AbstractHorse extends Animal implements ContainerListener, + + protected AbstractHorse(EntityType type, Level world) { + super(type, world); ++ this.moveControl = new net.minecraft.world.entity.ai.control.MoveControl(this); // Purpur - use vanilla controller ++ this.lookControl = new net.minecraft.world.entity.ai.control.LookControl(this); // Purpur - use vanilla controller + this.createInventory(); + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return false; // vanilla handles ++ } ++ // Purpur end ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.generateMaxHealth(random)); ++ this.getAttribute(Attributes.MOVEMENT_SPEED).setBaseValue(this.generateSpeed(random)); ++ this.getAttribute(Attributes.JUMP_STRENGTH).setBaseValue(this.generateJumpStrength(random)); ++ } ++ ++ protected double generateMaxHealth(double min, double max) { ++ if (min == max) return min; ++ int diff = Mth.floor(max - min); ++ double base = max - diff; ++ int first = Mth.floor((double) diff / 2); ++ int rest = diff - first; ++ return base + random.nextInt(first + 1) + random.nextInt(rest + 1); ++ } ++ ++ protected double generateJumpStrength(double min, double max) { ++ if (min == max) return min; ++ return min + (max - min) * this.random.nextDouble(); ++ } ++ ++ protected double generateSpeed(double min, double max) { ++ if (min == max) return min; ++ return min + (max - min) * this.random.nextDouble(); ++ } ++ ++ protected float generateMaxHealth(RandomSource random) { ++ return 15.0F + (float) random.nextInt(8) + (float) random.nextInt(9); ++ } ++ ++ protected double generateJumpStrength(RandomSource random) { ++ return 0.4000000059604645D + random.nextDouble() * 0.2D + random.nextDouble() * 0.2D + random.nextDouble() * 0.2D; ++ } ++ ++ protected double generateSpeed(RandomSource random) { ++ return (0.44999998807907104D + random.nextDouble() * 0.3D + random.nextDouble() * 0.3D + random.nextDouble() * 0.3D) * 0.25D; ++ } ++ + @Override + protected void registerGoals() { ++ this.goalSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HorseHasRider(this)); // Purpur + this.goalSelector.addGoal(1, new PanicGoal(this, 1.2D)); + this.goalSelector.addGoal(1, new RunAroundLikeCrazyGoal(this, 1.2D)); + this.goalSelector.addGoal(2, new BreedGoal(this, 1.0D, AbstractHorse.class)); +@@ -232,6 +280,7 @@ public abstract class AbstractHorse extends Animal implements ContainerListener, + if (this.canPerformRearing()) { + this.goalSelector.addGoal(9, new RandomStandGoal(this)); + } ++ this.targetSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HorseHasRider(this)); // Purpur + + this.addBehaviourGoals(); + } +@@ -1249,7 +1298,7 @@ public abstract class AbstractHorse extends Animal implements ContainerListener, + entityData = new AgeableMob.AgeableMobGroupData(0.2F); + } + +- this.randomizeAttributes(world.getRandom()); ++ // this.randomizeAttributes(world.getRandom()); // Purpur - replaced by initAttributes() + return super.finalizeSpawn(world, difficulty, spawnReason, (SpawnGroupData) entityData); + } + +diff --git a/src/main/java/net/minecraft/world/entity/animal/horse/Donkey.java b/src/main/java/net/minecraft/world/entity/animal/horse/Donkey.java +index ff02169ba14f5264cea8beaf1779e2890c5d74b8..94021abe521aea4a70f5eaa78fb05f9f71b7c38c 100644 +--- a/src/main/java/net/minecraft/world/entity/animal/horse/Donkey.java ++++ b/src/main/java/net/minecraft/world/entity/animal/horse/Donkey.java +@@ -15,6 +15,43 @@ public class Donkey extends AbstractChestedHorse { + super(type, world); + } + ++ // Purpur start ++ @Override ++ public boolean dismountsUnderwater() { ++ return level().purpurConfig.useDismountsUnderwaterTag ? super.dismountsUnderwater() : !level().purpurConfig.donkeyRidableInWater; ++ } ++ // Purpur end ++ ++ @Override ++ public float generateMaxHealth(net.minecraft.util.RandomSource random) { ++ return (float) generateMaxHealth(this.level().purpurConfig.donkeyMaxHealthMin, this.level().purpurConfig.donkeyMaxHealthMax); ++ } ++ ++ @Override ++ public double generateJumpStrength(net.minecraft.util.RandomSource random) { ++ return generateJumpStrength(this.level().purpurConfig.donkeyJumpStrengthMin, this.level().purpurConfig.donkeyJumpStrengthMax); ++ } ++ ++ @Override ++ public double generateSpeed(net.minecraft.util.RandomSource random) { ++ return generateSpeed(this.level().purpurConfig.donkeyMovementSpeedMin, this.level().purpurConfig.donkeyMovementSpeedMax); ++ } ++ ++ @Override ++ public int getPurpurBreedTime() { ++ return this.level().purpurConfig.donkeyBreedingTicks; ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level().purpurConfig.donkeyTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level().purpurConfig.donkeyAlwaysDropExp; ++ } ++ + @Override + protected SoundEvent getAmbientSound() { + return SoundEvents.DONKEY_AMBIENT; +diff --git a/src/main/java/net/minecraft/world/entity/animal/horse/Horse.java b/src/main/java/net/minecraft/world/entity/animal/horse/Horse.java +index 6e299770fca78699f7e1988db4cdef37b99d74c1..fdf9ec418b0fc567e286ac79dbdbeddac568ad67 100644 +--- a/src/main/java/net/minecraft/world/entity/animal/horse/Horse.java ++++ b/src/main/java/net/minecraft/world/entity/animal/horse/Horse.java +@@ -44,6 +44,43 @@ public class Horse extends AbstractHorse implements VariantHolder { + super(type, world); + } + ++ // Purpur start ++ @Override ++ public boolean dismountsUnderwater() { ++ return level().purpurConfig.useDismountsUnderwaterTag ? super.dismountsUnderwater() : !level().purpurConfig.horseRidableInWater; ++ } ++ // Purpur end ++ ++ @Override ++ public float generateMaxHealth(RandomSource random) { ++ return (float) generateMaxHealth(this.level().purpurConfig.horseMaxHealthMin, this.level().purpurConfig.horseMaxHealthMax); ++ } ++ ++ @Override ++ public double generateJumpStrength(RandomSource random) { ++ return generateJumpStrength(this.level().purpurConfig.horseJumpStrengthMin, this.level().purpurConfig.horseJumpStrengthMax); ++ } ++ ++ @Override ++ public double generateSpeed(RandomSource random) { ++ return generateSpeed(this.level().purpurConfig.horseMovementSpeedMin, this.level().purpurConfig.horseMovementSpeedMax); ++ } ++ ++ @Override ++ public int getPurpurBreedTime() { ++ return this.level().purpurConfig.horseBreedingTicks; ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level().purpurConfig.horseTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level().purpurConfig.horseAlwaysDropExp; ++ } ++ + @Override + protected void randomizeAttributes(RandomSource random) { + this.getAttribute(Attributes.MAX_HEALTH).setBaseValue((double)generateMaxHealth(random::nextInt)); +diff --git a/src/main/java/net/minecraft/world/entity/animal/horse/Llama.java b/src/main/java/net/minecraft/world/entity/animal/horse/Llama.java +index 1dd4290287725898ace29e46b439b55df8fdd1af..7d2a5c806fd0f1228c45b8a8b56d7ba13b899a2d 100644 +--- a/src/main/java/net/minecraft/world/entity/animal/horse/Llama.java ++++ b/src/main/java/net/minecraft/world/entity/animal/horse/Llama.java +@@ -75,9 +75,84 @@ public class Llama extends AbstractChestedHorse implements VariantHolder type, Level world) { + super(type, world); ++ // Purpur start ++ this.moveControl = new org.purpurmc.purpur.controller.MoveControllerWASD(this) { ++ @Override ++ public void tick() { ++ if (entity.getRider() != null && entity.isControllable() && isSaddled()) { ++ purpurTick(entity.getRider()); ++ } else { ++ vanillaTick(); ++ } ++ } ++ }; ++ this.lookControl = new org.purpurmc.purpur.controller.LookControllerWASD(this) { ++ @Override ++ public void tick() { ++ if (entity.getRider() != null && entity.isControllable() && isSaddled()) { ++ purpurTick(entity.getRider()); ++ } else { ++ vanillaTick(); ++ } ++ } ++ }; ++ // Purpur end ++ } ++ ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level().purpurConfig.llamaRidable; ++ } ++ ++ @Override ++ public boolean dismountsUnderwater() { ++ return level().purpurConfig.useDismountsUnderwaterTag ? super.dismountsUnderwater() : !level().purpurConfig.llamaRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level().purpurConfig.llamaControllable; ++ } ++ ++ @Override ++ public boolean isSaddled() { ++ return super.isSaddled() || (isTamed() && getSwag() != null); ++ } ++ // Purpur end ++ ++ @Override ++ public float generateMaxHealth(RandomSource random) { ++ return (float) generateMaxHealth(this.level().purpurConfig.llamaMaxHealthMin, this.level().purpurConfig.llamaMaxHealthMax); ++ } ++ ++ @Override ++ public double generateJumpStrength(RandomSource random) { ++ return generateJumpStrength(this.level().purpurConfig.llamaJumpStrengthMin, this.level().purpurConfig.llamaJumpStrengthMax); ++ } ++ ++ @Override ++ public double generateSpeed(RandomSource random) { ++ return generateSpeed(this.level().purpurConfig.llamaMovementSpeedMin, this.level().purpurConfig.llamaMovementSpeedMax); ++ } ++ ++ @Override ++ public int getPurpurBreedTime() { ++ return this.level().purpurConfig.llamaBreedingTicks; ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level().purpurConfig.llamaTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level().purpurConfig.llamaAlwaysDropExp; + } + + public boolean isTraderLlama() { +@@ -108,6 +183,7 @@ public class Llama extends AbstractChestedHorse implements VariantHolder public + super.jumpFromGround(); + double d0 = this.moveControl.getSpeedModifier(); + +@@ -477,11 +504,11 @@ public class Sniffer extends Animal { + + @Override + protected void customServerAiStep() { +- this.level().getProfiler().push("snifferBrain"); ++ //this.level().getProfiler().push("snifferBrain"); // Purpur + this.getBrain().tick((ServerLevel) this.level(), this); +- this.level().getProfiler().popPush("snifferActivityUpdate"); ++ //this.level().getProfiler().popPush("snifferActivityUpdate"); // Purpur + SnifferAi.updateActivity(this); +- this.level().getProfiler().pop(); ++ //this.level().getProfiler().pop(); // Purpur + super.customServerAiStep(); + } + +diff --git a/src/main/java/net/minecraft/world/entity/boss/EnderDragonPart.java b/src/main/java/net/minecraft/world/entity/boss/EnderDragonPart.java +index 6725013c608e9321ce0d088059672af4412cf6db..1c8ac6b9603a6e282c5bbe8cdcc61046c9efe41e 100644 +--- a/src/main/java/net/minecraft/world/entity/boss/EnderDragonPart.java ++++ b/src/main/java/net/minecraft/world/entity/boss/EnderDragonPart.java +@@ -25,6 +25,13 @@ public class EnderDragonPart extends Entity { + this.name = name; + } + ++ // Purpur start ++ @Override ++ public net.minecraft.world.InteractionResult interact(net.minecraft.world.entity.player.Player player, net.minecraft.world.InteractionHand hand) { ++ return parentMob.isAlive() ? parentMob.tryRide(player, hand) : net.minecraft.world.InteractionResult.PASS; ++ } ++ // Purpur end ++ + @Override + protected void defineSynchedData(SynchedEntityData.Builder builder) { + } +diff --git a/src/main/java/net/minecraft/world/entity/boss/enderdragon/EndCrystal.java b/src/main/java/net/minecraft/world/entity/boss/enderdragon/EndCrystal.java +index d8e440e14b72dc48ae97244f1bed2c06abd997ab..15ca426701f1fc821da94a4dee577fdbc4f8ff8d 100644 +--- a/src/main/java/net/minecraft/world/entity/boss/enderdragon/EndCrystal.java ++++ b/src/main/java/net/minecraft/world/entity/boss/enderdragon/EndCrystal.java +@@ -31,6 +31,12 @@ public class EndCrystal extends Entity { + private static final EntityDataAccessor DATA_SHOW_BOTTOM = SynchedEntityData.defineId(EndCrystal.class, EntityDataSerializers.BOOLEAN); + public int time; + public boolean generatedByDragonFight = false; // Paper - Fix invulnerable end crystals ++ // Purpur start ++ private net.minecraft.world.entity.monster.Phantom targetPhantom; ++ private int phantomBeamTicks = 0; ++ private int phantomDamageCooldown = 0; ++ private int idleCooldown = 0; ++ // Purpur end + + public EndCrystal(EntityType type, Level world) { + super(type, world); +@@ -43,6 +49,22 @@ public class EndCrystal extends Entity { + this.setPos(x, y, z); + } + ++ public boolean shouldExplode() { ++ return showsBottom() ? level().purpurConfig.basedEndCrystalExplode : level().purpurConfig.baselessEndCrystalExplode; ++ } ++ ++ public float getExplosionPower() { ++ return (float) (showsBottom() ? level().purpurConfig.basedEndCrystalExplosionPower : level().purpurConfig.baselessEndCrystalExplosionPower); ++ } ++ ++ public boolean hasExplosionFire() { ++ return showsBottom() ? level().purpurConfig.basedEndCrystalExplosionFire : level().purpurConfig.baselessEndCrystalExplosionFire; ++ } ++ ++ public Level.ExplosionInteraction getExplosionEffect() { ++ return showsBottom() ? level().purpurConfig.basedEndCrystalExplosionEffect : level().purpurConfig.baselessEndCrystalExplosionEffect; ++ } ++ + @Override + protected Entity.MovementEmission getMovementEmission() { + return Entity.MovementEmission.NONE; +@@ -78,8 +100,52 @@ public class EndCrystal extends Entity { + } + } + // Paper end - Fix invulnerable end crystals ++ if (this.level().purpurConfig.endCrystalCramming > 0 && this.level().getEntitiesOfClass(EndCrystal.class, getBoundingBox()).size() > this.level().purpurConfig.endCrystalCramming) this.hurt(this.damageSources().cramming(), 6.0F); // Purpur ++ } ++ ++ // Purpur start ++ if (level().purpurConfig.phantomAttackedByCrystalRadius <= 0 || --idleCooldown > 0) { ++ return; // on cooldown ++ } ++ ++ if (targetPhantom == null) { ++ for (net.minecraft.world.entity.monster.Phantom phantom : level().getEntitiesOfClass(net.minecraft.world.entity.monster.Phantom.class, getBoundingBox().inflate(level().purpurConfig.phantomAttackedByCrystalRadius))) { ++ if (phantom.hasLineOfSight(this)) { ++ attackPhantom(phantom); ++ break; ++ } ++ } ++ } else { ++ setBeamTarget(new BlockPos(targetPhantom).offset(0, -2, 0)); ++ if (--phantomBeamTicks > 0 && targetPhantom.isAlive()) { ++ phantomDamageCooldown--; ++ if (targetPhantom.hasLineOfSight(this)) { ++ if (phantomDamageCooldown <= 0) { ++ phantomDamageCooldown = 20; ++ targetPhantom.hurt(targetPhantom.damageSources().indirectMagic(this, this), level().purpurConfig.phantomAttackedByCrystalDamage); ++ } ++ } else { ++ forgetPhantom(); // no longer in sight ++ } ++ } else { ++ forgetPhantom(); // attacked long enough ++ } + } ++ } ++ ++ private void attackPhantom(net.minecraft.world.entity.monster.Phantom phantom) { ++ phantomDamageCooldown = 0; ++ phantomBeamTicks = 60; ++ targetPhantom = phantom; ++ } + ++ private void forgetPhantom() { ++ targetPhantom = null; ++ setBeamTarget(null); ++ phantomBeamTicks = 0; ++ phantomDamageCooldown = 0; ++ idleCooldown = 60; ++ // Purpur end + } + + @Override +@@ -121,16 +187,18 @@ public class EndCrystal extends Entity { + } + // CraftBukkit end + if (!source.is(DamageTypeTags.IS_EXPLOSION)) { ++ if (shouldExplode()) {// Purpur + DamageSource damagesource1 = source.getEntity() != null ? this.damageSources().explosion(this, source.getEntity()) : null; + + // CraftBukkit start +- ExplosionPrimeEvent event = CraftEventFactory.callExplosionPrimeEvent(this, 6.0F, false); ++ ExplosionPrimeEvent event = CraftEventFactory.callExplosionPrimeEvent(this, getExplosionPower(), hasExplosionFire()); // Purpur + if (event.isCancelled()) { + return false; + } + + this.remove(Entity.RemovalReason.KILLED, EntityRemoveEvent.Cause.EXPLODE); // CraftBukkit - add Bukkit remove cause +- this.level().explode(this, damagesource1, (ExplosionDamageCalculator) null, this.getX(), this.getY(), this.getZ(), event.getRadius(), event.getFire(), Level.ExplosionInteraction.BLOCK); ++ this.level().explode(this, damagesource1, (ExplosionDamageCalculator) null, this.getX(), this.getY(), this.getZ(), event.getRadius(), event.getFire(), getExplosionEffect()); // Purpur ++ } else this.unsetRemoved(); // Purpur + } else { + this.remove(Entity.RemovalReason.KILLED, EntityRemoveEvent.Cause.DEATH); // CraftBukkit - add Bukkit remove cause + } +diff --git a/src/main/java/net/minecraft/world/entity/boss/enderdragon/EnderDragon.java b/src/main/java/net/minecraft/world/entity/boss/enderdragon/EnderDragon.java +index 6f14607a88761171a72e274b3c9b476b20a272f1..3da1f7a6e443954e4976dd59391ea19b9c903cf7 100644 +--- a/src/main/java/net/minecraft/world/entity/boss/enderdragon/EnderDragon.java ++++ b/src/main/java/net/minecraft/world/entity/boss/enderdragon/EnderDragon.java +@@ -106,6 +106,7 @@ public class EnderDragon extends Mob implements Enemy { + @Nullable + private BlockPos podium; + // Paper end - Allow changing the EnderDragon podium ++ private boolean hadRider; // Purpur + + public EnderDragon(EntityType entitytypes, Level world) { + super(EntityType.ENDER_DRAGON, world); +@@ -128,6 +129,37 @@ public class EnderDragon extends Mob implements Enemy { + this.noCulling = true; + this.phaseManager = new EnderDragonPhaseManager(this); + this.explosionSource = new Explosion(world, this, null, null, Double.NaN, Double.NaN, Double.NaN, Float.NaN, true, Explosion.BlockInteraction.DESTROY, ParticleTypes.EXPLOSION, ParticleTypes.EXPLOSION_EMITTER, SoundEvents.GENERIC_EXPLODE); // CraftBukkit ++ ++ // Purpur start ++ this.moveControl = new org.purpurmc.purpur.controller.FlyingMoveControllerWASD(this) { ++ @Override ++ public void vanillaTick() { ++ // dragon doesn't use the controller. do nothing ++ } ++ }; ++ this.lookControl = new org.purpurmc.purpur.controller.LookControllerWASD(this) { ++ @Override ++ public void vanillaTick() { ++ // dragon doesn't use the controller. do nothing ++ } ++ ++ @Override ++ public void purpurTick(Player rider) { ++ setYawPitch(rider.getYRot() - 180F, rider.xRotO * 0.5F); ++ } ++ }; ++ // Purpur end ++ } ++ ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level().purpurConfig.enderDragonRidable; ++ } ++ ++ @Override ++ public boolean dismountsUnderwater() { ++ return level().purpurConfig.useDismountsUnderwaterTag ? super.dismountsUnderwater() : !level().purpurConfig.enderDragonRidableInWater; + } + + public void setDragonFight(EndDragonFight fight) { +@@ -142,6 +174,27 @@ public class EnderDragon extends Mob implements Enemy { + return this.fightOrigin; + } + ++ @Override ++ public boolean isControllable() { ++ return level().purpurConfig.enderDragonControllable; ++ } ++ ++ @Override ++ public double getMaxY() { ++ return level().purpurConfig.enderDragonMaxY; ++ } ++ // Purpur end ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level().purpurConfig.enderDragonMaxHealth); ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level().purpurConfig.enderDragonTakeDamageFromWater; ++ } ++ + public static AttributeSupplier.Builder createAttributes() { + return Mob.createMobAttributes().add(Attributes.MAX_HEALTH, 200.0D); + } +@@ -203,6 +256,37 @@ public class EnderDragon extends Mob implements Enemy { + + @Override + public void aiStep() { ++ // Purpur start ++ boolean hasRider = getRider() != null && this.isControllable(); ++ if (hasRider) { ++ if (!hadRider) { ++ hadRider = true; ++ noPhysics = false; ++ this.dimensions = net.minecraft.world.entity.EntityDimensions.scalable(4.0F, 2.0F); ++ } ++ ++ // dragon doesn't use controllers, so must tick manually ++ moveControl.tick(); ++ lookControl.tick(); ++ ++ moveRelative((float) getAttributeValue(Attributes.MOVEMENT_SPEED) * 0.1F, new Vec3(-getStrafeMot(), getVerticalMot(), -getForwardMot())); ++ Vec3 mot = getDeltaMovement(); ++ setDeltaMovement(mot); ++ move(MoverType.PLAYER, mot); ++ ++ mot = mot.multiply(0.9F, 0.9F, 0.9F); ++ setDeltaMovement(mot); ++ ++ // control wing flap speed on client ++ phaseManager.setPhase(mot.x() * mot.x() + mot.z() * mot.z() < 0.005F ? EnderDragonPhase.HOVERING : EnderDragonPhase.HOLDING_PATTERN); ++ } else if (hadRider) { ++ hadRider = false; ++ noPhysics = true; ++ this.dimensions = net.minecraft.world.entity.EntityDimensions.scalable(16.0F, 8.0F); ++ phaseManager.setPhase(EnderDragonPhase.HOLDING_PATTERN); // HoldingPattern ++ } ++ // Purpur end ++ + this.processFlappingMovement(); + if (this.level().isClientSide) { + this.setHealth(this.getHealth()); +@@ -229,6 +313,8 @@ public class EnderDragon extends Mob implements Enemy { + float f; + + if (this.isDeadOrDying()) { ++ if (hasRider) ejectPassengers(); // Purpur ++ + float f1 = (this.random.nextFloat() - 0.5F) * 8.0F; + + f = (this.random.nextFloat() - 0.5F) * 4.0F; +@@ -241,9 +327,9 @@ public class EnderDragon extends Mob implements Enemy { + + f = 0.2F / ((float) vec3d.horizontalDistance() * 10.0F + 1.0F); + f *= (float) Math.pow(2.0D, vec3d.y); +- if (this.phaseManager.getCurrentPhase().isSitting()) { ++ if (!hasRider && this.phaseManager.getCurrentPhase().isSitting()) { // Purpur + this.flapTime += 0.1F; +- } else if (this.inWall) { ++ } else if (!hasRider && this.inWall) { // Purpur + this.flapTime += f * 0.5F; + } else { + this.flapTime += f; +@@ -277,7 +363,7 @@ public class EnderDragon extends Mob implements Enemy { + } + + this.phaseManager.getCurrentPhase().doClientTick(); +- } else { ++ } else if (!hasRider) { // Purpur + DragonPhaseInstance idragoncontroller = this.phaseManager.getCurrentPhase(); + + idragoncontroller.doServerTick(); +@@ -346,7 +432,7 @@ public class EnderDragon extends Mob implements Enemy { + this.tickPart(this.body, (double) (f11 * 0.5F), 0.0D, (double) (-f12 * 0.5F)); + this.tickPart(this.wing1, (double) (f12 * 4.5F), 2.0D, (double) (f11 * 4.5F)); + this.tickPart(this.wing2, (double) (f12 * -4.5F), 2.0D, (double) (f11 * -4.5F)); +- if (!this.level().isClientSide && this.hurtTime == 0) { ++ if (!hasRider && !this.level().isClientSide && this.hurtTime == 0) { // Purpur + this.knockBack(this.level().getEntities((Entity) this, this.wing1.getBoundingBox().inflate(4.0D, 2.0D, 4.0D).move(0.0D, -2.0D, 0.0D), EntitySelector.NO_CREATIVE_OR_SPECTATOR)); + this.knockBack(this.level().getEntities((Entity) this, this.wing2.getBoundingBox().inflate(4.0D, 2.0D, 4.0D).move(0.0D, -2.0D, 0.0D), EntitySelector.NO_CREATIVE_OR_SPECTATOR)); + this.hurt(this.level().getEntities((Entity) this, this.head.getBoundingBox().inflate(1.0D), EntitySelector.NO_CREATIVE_OR_SPECTATOR)); +@@ -390,7 +476,7 @@ public class EnderDragon extends Mob implements Enemy { + } + + if (!this.level().isClientSide) { +- this.inWall = this.checkWalls(this.head.getBoundingBox()) | this.checkWalls(this.neck.getBoundingBox()) | this.checkWalls(this.body.getBoundingBox()); ++ this.inWall = !hasRider && this.checkWalls(this.head.getBoundingBox()) | this.checkWalls(this.neck.getBoundingBox()) | this.checkWalls(this.body.getBoundingBox()); // Purpur + if (this.dragonFight != null) { + this.dragonFight.updateDragon(this); + } +@@ -522,7 +608,7 @@ public class EnderDragon extends Mob implements Enemy { + BlockState iblockdata = this.level().getBlockState(blockposition); + + if (!iblockdata.isAir() && !iblockdata.is(BlockTags.DRAGON_TRANSPARENT)) { +- if (this.level().getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING) && !iblockdata.is(BlockTags.DRAGON_IMMUNE)) { ++ if ((this.level().purpurConfig.enderDragonBypassMobGriefing || this.level().getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING)) && !iblockdata.is(BlockTags.DRAGON_IMMUNE)) { // Purpur + // CraftBukkit start - Add blocks to list rather than destroying them + // flag1 = this.level().removeBlock(blockposition, false) || flag1; + flag1 = true; +@@ -666,7 +752,7 @@ public class EnderDragon extends Mob implements Enemy { + boolean flag = this.level().getGameRules().getBoolean(GameRules.RULE_DOMOBLOOT); + short short0 = 500; + +- if (this.dragonFight != null && !this.dragonFight.hasPreviouslyKilledDragon()) { ++ if (this.dragonFight != null && (level().purpurConfig.enderDragonAlwaysDropsFullExp || !this.dragonFight.hasPreviouslyKilledDragon())) { + short0 = 12000; + } + +@@ -1102,6 +1188,7 @@ public class EnderDragon extends Mob implements Enemy { + + @Override + protected boolean canRide(Entity entity) { ++ if (this.level().purpurConfig.enderDragonCanRideVehicles) return this.boardingCooldown <= 0; // Purpur + return false; + } + +diff --git a/src/main/java/net/minecraft/world/entity/boss/wither/WitherBoss.java b/src/main/java/net/minecraft/world/entity/boss/wither/WitherBoss.java +index 7ddca52f7fe3f289b4b867e134326b1ead1a2aee..4a98027a12c2535d1df3a9f6390eb85146398403 100644 +--- a/src/main/java/net/minecraft/world/entity/boss/wither/WitherBoss.java ++++ b/src/main/java/net/minecraft/world/entity/boss/wither/WitherBoss.java +@@ -88,20 +88,59 @@ public class WitherBoss extends Monster implements PowerableMob, RangedAttackMob + return !entityliving.getType().is(EntityTypeTags.WITHER_FRIENDS) && entityliving.attackable(); + }; + private static final TargetingConditions TARGETING_CONDITIONS = TargetingConditions.forCombat().range(20.0D).selector(WitherBoss.LIVING_ENTITY_SELECTOR); ++ @Nullable private java.util.UUID summoner; // Purpur ++ private int shootCooldown = 0; // Purpur + // Paper start + private boolean canPortal = false; + + public void setCanTravelThroughPortals(boolean canPortal) { this.canPortal = canPortal; } + // Paper end ++ private org.purpurmc.purpur.controller.FlyingWithSpacebarMoveControllerWASD purpurController; // Purpur + + public WitherBoss(EntityType type, Level world) { + super(type, world); + this.bossEvent = (ServerBossEvent) (new ServerBossEvent(this.getDisplayName(), BossEvent.BossBarColor.PURPLE, BossEvent.BossBarOverlay.PROGRESS)).setDarkenScreen(true); +- this.moveControl = new FlyingMoveControl(this, 10, false); ++ // Purpur start ++ this.purpurController = new org.purpurmc.purpur.controller.FlyingWithSpacebarMoveControllerWASD(this, 0.1F); ++ this.moveControl = new FlyingMoveControl(this, 10, false) { ++ @Override ++ public void tick() { ++ if (mob.getRider() != null && mob.isControllable()) { ++ purpurController.purpurTick(mob.getRider()); ++ } else { ++ super.tick(); ++ } ++ } ++ }; ++ // Purpur end + this.setHealth(this.getMaxHealth()); + this.xpReward = 50; + } + ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level().purpurConfig.witherMaxHealth); ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level().purpurConfig.witherTakeDamageFromWater; ++ } ++ ++ @Nullable ++ public java.util.UUID getSummoner() { ++ return summoner; ++ } ++ ++ public void setSummoner(@Nullable java.util.UUID summoner) { ++ this.summoner = summoner; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level().purpurConfig.witherAlwaysDropExp; ++ } ++ + @Override + protected PathNavigation createNavigation(Level world) { + FlyingPathNavigation navigationflying = new FlyingPathNavigation(this, world); +@@ -112,13 +151,113 @@ public class WitherBoss extends Monster implements PowerableMob, RangedAttackMob + return navigationflying; + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level().purpurConfig.witherRidable; ++ } ++ ++ @Override ++ public boolean dismountsUnderwater() { ++ return level().purpurConfig.useDismountsUnderwaterTag ? super.dismountsUnderwater() : !level().purpurConfig.witherRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level().purpurConfig.witherControllable; ++ } ++ ++ @Override ++ public double getMaxY() { ++ return level().purpurConfig.witherMaxY; ++ } ++ ++ @Override ++ public void travel(Vec3 vec3) { ++ super.travel(vec3); ++ if (getRider() != null && this.isControllable() && !onGround) { ++ float speed = (float) getAttributeValue(Attributes.FLYING_SPEED) * 5F; ++ setSpeed(speed); ++ Vec3 mot = getDeltaMovement(); ++ move(net.minecraft.world.entity.MoverType.SELF, mot.multiply(speed, 0.5, speed)); ++ setDeltaMovement(mot.scale(0.9D)); ++ } ++ } ++ ++ @Override ++ public void onMount(Player rider) { ++ super.onMount(rider); ++ this.entityData.set(DATA_TARGETS.get(0), 0); ++ this.entityData.set(DATA_TARGETS.get(1), 0); ++ this.entityData.set(DATA_TARGETS.get(2), 0); ++ getNavigation().stop(); ++ shootCooldown = 20; ++ } ++ ++ @Override ++ public boolean onClick(net.minecraft.world.InteractionHand hand) { ++ return shoot(getRider(), hand == net.minecraft.world.InteractionHand.MAIN_HAND ? new int[]{1} : new int[]{2}); ++ } ++ ++ public boolean shoot(@Nullable Player rider, int[] heads) { ++ if (shootCooldown > 0) { ++ return false; ++ } ++ ++ shootCooldown = 20; ++ if (rider == null) { ++ return false; ++ } ++ ++ org.bukkit.craftbukkit.entity.CraftHumanEntity player = rider.getBukkitEntity(); ++ if (!player.hasPermission("allow.special.wither")) { ++ return false; ++ } ++ ++ net.minecraft.world.phys.HitResult rayTrace = getRayTrace(120, net.minecraft.world.level.ClipContext.Fluid.NONE); ++ if (rayTrace == null) { ++ return false; ++ } ++ ++ Vec3 loc; ++ if (rayTrace.getType() == net.minecraft.world.phys.HitResult.Type.BLOCK) { ++ BlockPos pos = ((net.minecraft.world.phys.BlockHitResult) rayTrace).getBlockPos(); ++ loc = new Vec3(pos.getX() + 0.5D, pos.getY() + 0.5D, pos.getZ() + 0.5D); ++ } else if (rayTrace.getType() == net.minecraft.world.phys.HitResult.Type.ENTITY) { ++ Entity target = ((net.minecraft.world.phys.EntityHitResult) rayTrace).getEntity(); ++ loc = new Vec3(target.getX(), target.getY() + (target.getEyeHeight() / 2), target.getZ()); ++ } else { ++ org.bukkit.block.Block block = player.getTargetBlock(null, 120); ++ loc = new Vec3(block.getX() + 0.5D, block.getY() + 0.5D, block.getZ() + 0.5D); ++ } ++ ++ for (int head : heads) { ++ shoot(head, loc.x(), loc.y(), loc.z(), rider); ++ } ++ ++ return true; // handled ++ } ++ ++ public void shoot(int head, double x, double y, double z, Player rider) { ++ level().levelEvent(null, 1024, blockPosition(), 0); ++ double headX = getHeadX(head); ++ double headY = getHeadY(head); ++ double headZ = getHeadZ(head); ++ WitherSkull skull = new WitherSkull(level(), this, x - headX, y - headY, z - headZ); ++ skull.setPosRaw(headX, headY, headZ); ++ level().addFreshEntity(skull); ++ } ++ // Purpur end ++ + @Override + protected void registerGoals() { ++ this.goalSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.goalSelector.addGoal(0, new WitherBoss.WitherDoNothingGoal()); + this.goalSelector.addGoal(2, new RangedAttackGoal(this, 1.0D, 40, 20.0F)); + this.goalSelector.addGoal(5, new WaterAvoidingRandomFlyingGoal(this, 1.0D)); + this.goalSelector.addGoal(6, new LookAtPlayerGoal(this, Player.class, 8.0F)); + this.goalSelector.addGoal(7, new RandomLookAroundGoal(this)); ++ this.targetSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.targetSelector.addGoal(1, new HurtByTargetGoal(this, new Class[0])); + this.targetSelector.addGoal(2, new NearestAttackableTargetGoal<>(this, LivingEntity.class, 0, false, false, WitherBoss.LIVING_ENTITY_SELECTOR)); + } +@@ -136,6 +275,7 @@ public class WitherBoss extends Monster implements PowerableMob, RangedAttackMob + public void addAdditionalSaveData(CompoundTag nbt) { + super.addAdditionalSaveData(nbt); + nbt.putInt("Invul", this.getInvulnerableTicks()); ++ if (getSummoner() != null) nbt.putUUID("Purpur.Summoner", getSummoner()); // Purpur + } + + @Override +@@ -145,6 +285,7 @@ public class WitherBoss extends Monster implements PowerableMob, RangedAttackMob + if (this.hasCustomName()) { + this.bossEvent.setName(this.getDisplayName()); + } ++ if (nbt.contains("Purpur.Summoner")) setSummoner(nbt.getUUID("Purpur.Summoner")); // Purpur + + } + +@@ -263,6 +404,16 @@ public class WitherBoss extends Monster implements PowerableMob, RangedAttackMob + + @Override + protected void customServerAiStep() { ++ // Purpur start ++ if (getRider() != null && this.isControllable()) { ++ Vec3 mot = getDeltaMovement(); ++ setDeltaMovement(mot.x(), mot.y() + (getVerticalMot() > 0 ? 0.07D : 0.0D), mot.z()); ++ } ++ if (shootCooldown > 0) { ++ shootCooldown--; ++ } ++ // Purpur end ++ + int i; + + if (this.getInvulnerableTicks() > 0) { +@@ -279,7 +430,7 @@ public class WitherBoss extends Monster implements PowerableMob, RangedAttackMob + } + // CraftBukkit end + +- if (!this.isSilent()) { ++ if (!this.isSilent() && level().purpurConfig.witherPlaySpawnSound) { + // CraftBukkit start - Use relative location for far away sounds + // this.level().globalLevelEvent(1023, new BlockPosition(this), 0); + int viewDistance = ((ServerLevel) this.level()).getCraftServer().getViewDistance() * 16; +@@ -304,7 +455,7 @@ public class WitherBoss extends Monster implements PowerableMob, RangedAttackMob + + this.setInvulnerableTicks(i); + if (this.tickCount % 10 == 0) { +- this.heal(10.0F, EntityRegainHealthEvent.RegainReason.WITHER_SPAWN); // CraftBukkit ++ this.heal(this.getMaxHealth() / 33, EntityRegainHealthEvent.RegainReason.WITHER_SPAWN); // CraftBukkit // Purpur + } + + } else { +@@ -364,7 +515,7 @@ public class WitherBoss extends Monster implements PowerableMob, RangedAttackMob + + if (this.destroyBlocksTick > 0) { + --this.destroyBlocksTick; +- if (this.destroyBlocksTick == 0 && this.level().getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING)) { ++ if (this.destroyBlocksTick == 0 && (this.level().purpurConfig.witherBypassMobGriefing || this.level().getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING))) { // Purpur + boolean flag = false; + + j = Mth.floor(this.getBbWidth() / 2.0F + 1.0F); +@@ -391,8 +542,10 @@ public class WitherBoss extends Monster implements PowerableMob, RangedAttackMob + } + } + +- if (this.tickCount % 20 == 0) { +- this.heal(1.0F, EntityRegainHealthEvent.RegainReason.REGEN); // CraftBukkit ++ // Purpur start - customizable heal rate and amount ++ if (this.tickCount % level().purpurConfig.witherHealthRegenDelay == 0) { ++ this.heal(level().purpurConfig.witherHealthRegenAmount, EntityRegainHealthEvent.RegainReason.REGEN); // CraftBukkit ++ // Purpur end + } + + this.bossEvent.setProgress(this.getHealth() / this.getMaxHealth()); +@@ -580,11 +733,11 @@ public class WitherBoss extends Monster implements PowerableMob, RangedAttackMob + } + + public int getAlternativeTarget(int headIndex) { +- return (Integer) this.entityData.get((EntityDataAccessor) WitherBoss.DATA_TARGETS.get(headIndex)); ++ return getRider() != null && this.isControllable() ? 0 : this.entityData.get(WitherBoss.DATA_TARGETS.get(headIndex)); // Purpur + } + + public void setAlternativeTarget(int headIndex, int id) { +- this.entityData.set((EntityDataAccessor) WitherBoss.DATA_TARGETS.get(headIndex), id); ++ if (getRider() == null || !this.isControllable()) this.entityData.set(WitherBoss.DATA_TARGETS.get(headIndex), id); // Purpur + } + + @Override +@@ -594,6 +747,7 @@ public class WitherBoss extends Monster implements PowerableMob, RangedAttackMob + + @Override + protected boolean canRide(Entity entity) { ++ if (this.level().purpurConfig.witherCanRideVehicles) return this.boardingCooldown <= 0; // Purpur + return false; + } + +diff --git a/src/main/java/net/minecraft/world/entity/decoration/ArmorStand.java b/src/main/java/net/minecraft/world/entity/decoration/ArmorStand.java +index c2bd2e303f956d390319f6bbbe9a6492ebec5154..6697cd8a632becd72ee132007a61d1221e817abf 100644 +--- a/src/main/java/net/minecraft/world/entity/decoration/ArmorStand.java ++++ b/src/main/java/net/minecraft/world/entity/decoration/ArmorStand.java +@@ -103,10 +103,12 @@ public class ArmorStand extends LivingEntity { + private boolean noTickPoseDirty = false; + private boolean noTickEquipmentDirty = false; + // Paper end - Allow ArmorStands not to tick ++ public boolean canMovementTick = true; // Purpur + + public ArmorStand(EntityType type, Level world) { + super(type, world); + if (world != null) this.canTick = world.paperConfig().entities.armorStands.tick; // Paper - Allow ArmorStands not to tick ++ if (world != null) this.canMovementTick = world.purpurConfig.armorstandMovement; // Purpur + this.handItems = NonNullList.withSize(2, ItemStack.EMPTY); + this.armorItems = NonNullList.withSize(4, ItemStack.EMPTY); + this.headPose = ArmorStand.DEFAULT_HEAD_POSE; +@@ -115,6 +117,7 @@ public class ArmorStand extends LivingEntity { + this.rightArmPose = ArmorStand.DEFAULT_RIGHT_ARM_POSE; + this.leftLegPose = ArmorStand.DEFAULT_LEFT_LEG_POSE; + this.rightLegPose = ArmorStand.DEFAULT_RIGHT_LEG_POSE; ++ this.setShowArms(world != null && world.purpurConfig.armorstandPlaceWithArms); // Purpur + } + + public ArmorStand(Level world, double x, double y, double z) { +@@ -613,6 +616,7 @@ public class ArmorStand extends LivingEntity { + private org.bukkit.event.entity.EntityDeathEvent brokenByPlayer(DamageSource damageSource) { // Paper + ItemStack itemstack = new ItemStack(Items.ARMOR_STAND); + ++ if (this.level().purpurConfig.persistentDroppableEntityDisplayNames) + itemstack.set(DataComponents.CUSTOM_NAME, this.getCustomName()); + this.drops.add(new DefaultDrop(itemstack, stack -> Block.popResource(this.level(), this.blockPosition(), stack))); // CraftBukkit - add to drops // Paper - Restore vanilla drops behavior + return this.brokenByAnything(damageSource); // Paper +@@ -676,6 +680,7 @@ public class ArmorStand extends LivingEntity { + + @Override + public void tick() { ++ maxUpStep = level().purpurConfig.armorstandStepHeight; + // Paper start - Allow ArmorStands not to tick + if (!this.canTick) { + if (this.noTickPoseDirty) { +@@ -1003,4 +1008,18 @@ public class ArmorStand extends LivingEntity { + } + } + // Paper end ++ ++ // Purpur start ++ @Override ++ public void updateInWaterStateAndDoWaterCurrentPushing() { ++ if (this.level().purpurConfig.armorstandWaterMovement && ++ (this.level().purpurConfig.armorstandWaterFence || !(level().getBlockState(blockPosition().below()).getBlock() instanceof net.minecraft.world.level.block.FenceBlock))) ++ super.updateInWaterStateAndDoWaterCurrentPushing(); ++ } ++ ++ @Override ++ public void aiStep() { ++ if (this.canMovementTick && this.canMove) super.aiStep(); ++ } ++ // Purpur end + } +diff --git a/src/main/java/net/minecraft/world/entity/decoration/ItemFrame.java b/src/main/java/net/minecraft/world/entity/decoration/ItemFrame.java +index da0d1c9a1c4ae081bff9ca4230c9a1503885c354..9af8fcf6abb9b768829592bc1b091ebe4599ed2e 100644 +--- a/src/main/java/net/minecraft/world/entity/decoration/ItemFrame.java ++++ b/src/main/java/net/minecraft/world/entity/decoration/ItemFrame.java +@@ -262,7 +262,13 @@ public class ItemFrame extends HangingEntity { + } + + if (alwaysDrop) { +- this.spawnAtLocation(this.getFrameItemStack()); ++ // Purpur start ++ final ItemStack itemFrame = this.getFrameItemStack(); ++ if (!this.level().purpurConfig.persistentDroppableEntityDisplayNames) { ++ itemFrame.set(DataComponents.CUSTOM_NAME, null); ++ } ++ this.spawnAtLocation(itemFrame); ++ // Purpur end + } + + if (!itemstack.isEmpty()) { +diff --git a/src/main/java/net/minecraft/world/entity/decoration/Painting.java b/src/main/java/net/minecraft/world/entity/decoration/Painting.java +index 40e7112669abb58a0ab6df1846afec3979e95e55..183464f202d4c2774840edfde1dfcab44d05d0d3 100644 +--- a/src/main/java/net/minecraft/world/entity/decoration/Painting.java ++++ b/src/main/java/net/minecraft/world/entity/decoration/Painting.java +@@ -151,7 +151,13 @@ public class Painting extends HangingEntity implements VariantHolder type, Level world) { + super(type, world); +@@ -371,7 +377,16 @@ public class ItemEntity extends Entity implements TraceableEntity { + + @Override + public boolean hurt(DamageSource source, float amount) { +- if (this.isInvulnerableTo(source)) { ++ // Purpur start ++ if ( ++ (immuneToCactus && source.is(net.minecraft.world.damagesource.DamageTypes.CACTUS)) || ++ (immuneToFire && (source.is(DamageTypeTags.IS_FIRE) || source.is(net.minecraft.world.damagesource.DamageTypes.ON_FIRE) || source.is(net.minecraft.world.damagesource.DamageTypes.IN_FIRE))) || ++ (immuneToLightning && source.is(net.minecraft.world.damagesource.DamageTypes.LIGHTNING_BOLT)) || ++ (immuneToExplosion && source.is(DamageTypeTags.IS_EXPLOSION)) ++ ) { ++ return false; ++ } else if (this.isInvulnerableTo(source)) { ++ // Purpur end + return false; + } else if (!this.getItem().isEmpty() && this.getItem().is(Items.NETHER_STAR) && source.is(DamageTypeTags.IS_EXPLOSION)) { + return false; +@@ -579,6 +594,12 @@ public class ItemEntity extends Entity implements TraceableEntity { + public void setItem(ItemStack stack) { + this.getEntityData().set(ItemEntity.DATA_ITEM, stack); + this.despawnRate = this.level().paperConfig().entities.spawning.altItemDespawnRate.enabled ? this.level().paperConfig().entities.spawning.altItemDespawnRate.items.getOrDefault(stack.getItem(), this.level().spigotConfig.itemDespawnRate) : this.level().spigotConfig.itemDespawnRate; // Paper - Alternative item-despawn-rate ++ // Purpur start ++ if (level().purpurConfig.itemImmuneToCactus.contains(stack.getItem())) immuneToCactus = true; ++ if (level().purpurConfig.itemImmuneToExplosion.contains(stack.getItem())) immuneToExplosion = true; ++ if (level().purpurConfig.itemImmuneToFire.contains(stack.getItem())) immuneToFire = true; ++ if (level().purpurConfig.itemImmuneToLightning.contains(stack.getItem())) immuneToLightning = true; ++ // level end + } + + @Override +diff --git a/src/main/java/net/minecraft/world/entity/item/PrimedTnt.java b/src/main/java/net/minecraft/world/entity/item/PrimedTnt.java +index f1f352ec0e51f5db59254841a06c176c5a876fc9..dff0e7b08b973a1b29f916e63d3e4778d6c56cdc 100644 +--- a/src/main/java/net/minecraft/world/entity/item/PrimedTnt.java ++++ b/src/main/java/net/minecraft/world/entity/item/PrimedTnt.java +@@ -193,4 +193,29 @@ public class PrimedTnt extends Entity implements TraceableEntity { + return !level().paperConfig().fixes.preventTntFromMovingInWater && super.isPushedByFluid(); + } + // Paper end - Option to prevent TNT from moving in water ++ // Purpur start - Shears can defuse TNT ++ @Override ++ public net.minecraft.world.InteractionResult interact(net.minecraft.world.entity.player.Player player, net.minecraft.world.InteractionHand hand) { ++ if (!level().isClientSide && level().purpurConfig.shearsCanDefuseTnt) { ++ final net.minecraft.world.item.ItemStack inHand = player.getItemInHand(hand); ++ ++ if (!inHand.is(net.minecraft.world.item.Items.SHEARS) || !player.getBukkitEntity().hasPermission("purpur.tnt.defuse") || ++ level().random.nextFloat() > level().purpurConfig.shearsCanDefuseTntChance) return net.minecraft.world.InteractionResult.PASS; ++ ++ net.minecraft.world.entity.item.ItemEntity tntItem = new net.minecraft.world.entity.item.ItemEntity(level(), getX(), getY(), getZ(), ++ new net.minecraft.world.item.ItemStack(net.minecraft.world.item.Items.TNT)); ++ tntItem.setPickUpDelay(10); ++ ++ inHand.hurtAndBreak(1, player, LivingEntity.getSlotForHand(hand)); ++ level().addFreshEntity(tntItem, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.CUSTOM); ++ ++ this.playSound(net.minecraft.sounds.SoundEvents.SHEEP_SHEAR); ++ ++ this.kill(); ++ return net.minecraft.world.InteractionResult.SUCCESS; ++ } ++ ++ return super.interact(player, hand); ++ } ++ // Purpur end - Shears can defuse TNT + } +diff --git a/src/main/java/net/minecraft/world/entity/monster/AbstractSkeleton.java b/src/main/java/net/minecraft/world/entity/monster/AbstractSkeleton.java +index 0c5fe46d2da113beff3e220843593d616e37d4ca..e80307198b051cbcd9f72b36e459276848dcb4c9 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/AbstractSkeleton.java ++++ b/src/main/java/net/minecraft/world/entity/monster/AbstractSkeleton.java +@@ -65,16 +65,19 @@ public abstract class AbstractSkeleton extends Monster implements RangedAttackMo + protected AbstractSkeleton(EntityType type, Level world) { + super(type, world); + this.reassessWeaponGoal(); ++ this.setShouldBurnInDay(true); // Purpur + } + + @Override + protected void registerGoals() { ++ this.goalSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.goalSelector.addGoal(2, new RestrictSunGoal(this)); + this.goalSelector.addGoal(3, new FleeSunGoal(this, 1.0D)); + this.goalSelector.addGoal(3, new AvoidEntityGoal<>(this, Wolf.class, 6.0F, 1.0D, 1.2D)); + this.goalSelector.addGoal(5, new WaterAvoidingRandomStrollGoal(this, 1.0D)); + this.goalSelector.addGoal(6, new LookAtPlayerGoal(this, Player.class, 8.0F)); + this.goalSelector.addGoal(6, new RandomLookAroundGoal(this)); ++ this.targetSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.targetSelector.addGoal(1, new HurtByTargetGoal(this, new Class[0])); + this.targetSelector.addGoal(2, new NearestAttackableTargetGoal<>(this, Player.class, true)); + this.targetSelector.addGoal(3, new NearestAttackableTargetGoal<>(this, IronGolem.class, true)); +@@ -93,35 +96,14 @@ public abstract class AbstractSkeleton extends Monster implements RangedAttackMo + abstract SoundEvent getStepSound(); + + // Paper start - shouldBurnInDay API +- private boolean shouldBurnInDay = true; ++ // private boolean shouldBurnInDay = true; // Purpur - moved to LivingEntity - keep methods for ABI compatibility + public boolean shouldBurnInDay() { return shouldBurnInDay; } + public void setShouldBurnInDay(boolean shouldBurnInDay) { this.shouldBurnInDay = shouldBurnInDay; } + // Paper end - shouldBurnInDay API + + @Override + public void aiStep() { +- boolean flag = shouldBurnInDay && this.isSunBurnTick(); // Paper - shouldBurnInDay API +- +- if (flag) { +- ItemStack itemstack = this.getItemBySlot(EquipmentSlot.HEAD); +- +- if (!itemstack.isEmpty()) { +- if (itemstack.isDamageableItem()) { +- itemstack.setDamageValue(itemstack.getDamageValue() + this.random.nextInt(2)); +- if (itemstack.getDamageValue() >= itemstack.getMaxDamage()) { +- this.broadcastBreakEvent(EquipmentSlot.HEAD); +- this.setItemSlot(EquipmentSlot.HEAD, ItemStack.EMPTY); +- } +- } +- +- flag = false; +- } +- +- if (flag) { +- this.igniteForSeconds(8); +- } +- } +- ++ // Purpur start - implemented in LivingEntity + super.aiStep(); + } + +@@ -153,11 +135,7 @@ public abstract class AbstractSkeleton extends Monster implements RangedAttackMo + this.reassessWeaponGoal(); + this.setCanPickUpLoot(this.level().paperConfig().entities.behavior.mobsCanAlwaysPickUpLoot.skeletons || randomsource.nextFloat() < 0.55F * difficulty.getSpecialMultiplier()); // Paper - Add world settings for mobs picking up loot + if (this.getItemBySlot(EquipmentSlot.HEAD).isEmpty()) { +- LocalDate localdate = LocalDate.now(); +- int i = localdate.get(ChronoField.DAY_OF_MONTH); +- int j = localdate.get(ChronoField.MONTH_OF_YEAR); +- +- if (j == 10 && i == 31 && randomsource.nextFloat() < 0.25F) { ++ if (net.minecraft.world.entity.ambient.Bat.isHalloweenSeason(world.getMinecraftWorld()) && this.random.nextFloat() < this.level().purpurConfig.chanceHeadHalloweenOnEntity) { // Purpur + this.setItemSlot(EquipmentSlot.HEAD, new ItemStack(randomsource.nextFloat() < 0.1F ? Blocks.JACK_O_LANTERN : Blocks.CARVED_PUMPKIN)); + this.armorDropChances[EquipmentSlot.HEAD.getIndex()] = 0.0F; + } +@@ -205,7 +183,7 @@ public abstract class AbstractSkeleton extends Monster implements RangedAttackMo + double d2 = target.getZ() - this.getZ(); + double d3 = Math.sqrt(d0 * d0 + d2 * d2); + +- entityarrow.shoot(d0, d1 + d3 * 0.20000000298023224D, d2, 1.6F, (float) (14 - this.level().getDifficulty().getId() * 4)); ++ entityarrow.shoot(d0, d1 + d3 * 0.20000000298023224D, d2, 1.6F, this.level().purpurConfig.skeletonBowAccuracyMap.getOrDefault(this.level().getDifficulty().getId(), (float) (14 - this.level().getDifficulty().getId() * 4))); // Purpur + // CraftBukkit start + org.bukkit.event.entity.EntityShootBowEvent event = org.bukkit.craftbukkit.event.CraftEventFactory.callEntityShootBowEvent(this, this.getMainHandItem(), entityarrow.getPickupItem(), entityarrow, net.minecraft.world.InteractionHand.MAIN_HAND, 0.8F, true); // Paper + if (event.isCancelled()) { +@@ -236,7 +214,7 @@ public abstract class AbstractSkeleton extends Monster implements RangedAttackMo + this.reassessWeaponGoal(); + // Paper start - shouldBurnInDay API + if (nbt.contains("Paper.ShouldBurnInDay")) { +- this.shouldBurnInDay = nbt.getBoolean("Paper.ShouldBurnInDay"); ++ // this.shouldBurnInDay = nbt.getBoolean("Paper.ShouldBurnInDay"); // Purpur - implemented in LivingEntity + } + // Paper end - shouldBurnInDay API + } +@@ -245,7 +223,7 @@ public abstract class AbstractSkeleton extends Monster implements RangedAttackMo + @Override + public void addAdditionalSaveData(CompoundTag nbt) { + super.addAdditionalSaveData(nbt); +- nbt.putBoolean("Paper.ShouldBurnInDay", this.shouldBurnInDay); ++ // nbt.putBoolean("Paper.ShouldBurnInDay", this.shouldBurnInDay); // Purpur - implemented in LivingEntity + } + // Paper end - shouldBurnInDay API + +diff --git a/src/main/java/net/minecraft/world/entity/monster/Blaze.java b/src/main/java/net/minecraft/world/entity/monster/Blaze.java +index aee2fa184bc5723dfd3d54f460a173982d874c8b..27db17e19dd95e99f7bd67747eba3c3072b48ed5 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/Blaze.java ++++ b/src/main/java/net/minecraft/world/entity/monster/Blaze.java +@@ -32,26 +32,73 @@ public class Blaze extends Monster { + + public Blaze(EntityType type, Level world) { + super(type, world); +- this.setPathfindingMalus(PathType.WATER, -1.0F); ++ this.moveControl = new org.purpurmc.purpur.controller.FlyingWithSpacebarMoveControllerWASD(this, 0.3F); // Purpur ++ if (isSensitiveToWater()) this.setPathfindingMalus(PathType.WATER, -1.0F); // Purpur + this.setPathfindingMalus(PathType.LAVA, 8.0F); + this.setPathfindingMalus(PathType.DANGER_FIRE, 0.0F); + this.setPathfindingMalus(PathType.DAMAGE_FIRE, 0.0F); + this.xpReward = 10; + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level().purpurConfig.blazeRidable; ++ } ++ ++ @Override ++ public boolean dismountsUnderwater() { ++ return level().purpurConfig.useDismountsUnderwaterTag ? super.dismountsUnderwater() : !level().purpurConfig.blazeRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level().purpurConfig.blazeControllable; ++ } ++ ++ @Override ++ public double getMaxY() { ++ return level().purpurConfig.blazeMaxY; ++ } ++ ++ @Override ++ public void travel(Vec3 vec3) { ++ super.travel(vec3); ++ if (getRider() != null && this.isControllable() && !onGround) { ++ float speed = (float) getAttributeValue(Attributes.FLYING_SPEED); ++ setSpeed(speed); ++ Vec3 mot = getDeltaMovement(); ++ move(net.minecraft.world.entity.MoverType.SELF, mot.multiply(speed, 1.0, speed)); ++ setDeltaMovement(mot.scale(0.9D)); ++ } ++ } ++ // Purpur end ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level().purpurConfig.blazeMaxHealth); ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level().purpurConfig.blazeAlwaysDropExp; ++ } ++ + @Override + protected void registerGoals() { ++ this.goalSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.goalSelector.addGoal(4, new Blaze.BlazeAttackGoal(this)); + this.goalSelector.addGoal(5, new MoveTowardsRestrictionGoal(this, 1.0)); + this.goalSelector.addGoal(7, new WaterAvoidingRandomStrollGoal(this, 1.0, 0.0F)); + this.goalSelector.addGoal(8, new LookAtPlayerGoal(this, Player.class, 8.0F)); + this.goalSelector.addGoal(8, new RandomLookAroundGoal(this)); ++ this.targetSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.targetSelector.addGoal(1, new HurtByTargetGoal(this).setAlertOthers()); + this.targetSelector.addGoal(2, new NearestAttackableTargetGoal<>(this, Player.class, true)); + } + + public static AttributeSupplier.Builder createAttributes() { +- return Monster.createMonsterAttributes().add(Attributes.ATTACK_DAMAGE, 6.0).add(Attributes.MOVEMENT_SPEED, 0.23F).add(Attributes.FOLLOW_RANGE, 48.0); ++ return Monster.createMonsterAttributes().add(Attributes.ATTACK_DAMAGE, 6.0).add(Attributes.MOVEMENT_SPEED, 0.23F).add(Attributes.FOLLOW_RANGE, 48.0).add(Attributes.FLYING_SPEED, 0.6D); // Purpur + } + + @Override +@@ -111,11 +158,18 @@ public class Blaze extends Monster { + + @Override + public boolean isSensitiveToWater() { +- return true; ++ return this.level().purpurConfig.blazeTakeDamageFromWater; // Purpur + } + + @Override + protected void customServerAiStep() { ++ // Purpur start ++ if (getRider() != null && this.isControllable()) { ++ Vec3 mot = getDeltaMovement(); ++ setDeltaMovement(mot.x(), getVerticalMot() > 0 ? 0.07D : -0.07D, mot.z()); ++ return; ++ } ++ // Purpur end + this.nextHeightOffsetChangeTick--; + if (this.nextHeightOffsetChangeTick <= 0) { + this.nextHeightOffsetChangeTick = 100; +diff --git a/src/main/java/net/minecraft/world/entity/monster/Bogged.java b/src/main/java/net/minecraft/world/entity/monster/Bogged.java +index 754eb747179d9318bc5a3883e5622cc400c4e06c..4e929539cb093d58f3311d5f6a62bd1aeb71cfb0 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/Bogged.java ++++ b/src/main/java/net/minecraft/world/entity/monster/Bogged.java +@@ -155,7 +155,7 @@ public class Bogged extends AbstractSkeleton implements Shearable { + + // Paper start - shear drops API + @Override +- public java.util.List generateDefaultDrops() { ++ public java.util.List generateDefaultDrops(int looting) { // Purpur + final java.util.List drops = new java.util.ArrayList<>(); + this.generateShearedMushrooms(drops::add); + return drops; +diff --git a/src/main/java/net/minecraft/world/entity/monster/CaveSpider.java b/src/main/java/net/minecraft/world/entity/monster/CaveSpider.java +index 87e4b300ac248f6c13d9b4a8f24fd78b24b565b4..43b5a0e7993ae9daef1c1ea67722347f9851d3a9 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/CaveSpider.java ++++ b/src/main/java/net/minecraft/world/entity/monster/CaveSpider.java +@@ -26,6 +26,38 @@ public class CaveSpider extends Spider { + return Spider.createAttributes().add(Attributes.MAX_HEALTH, 12.0D); + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level().purpurConfig.caveSpiderRidable; ++ } ++ ++ @Override ++ public boolean dismountsUnderwater() { ++ return level().purpurConfig.useDismountsUnderwaterTag ? super.dismountsUnderwater() : !level().purpurConfig.caveSpiderRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level().purpurConfig.caveSpiderControllable; ++ } ++ // Purpur end ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level().purpurConfig.caveSpiderMaxHealth); ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level().purpurConfig.caveSpiderTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level().purpurConfig.caveSpiderAlwaysDropExp; ++ } ++ + @Override + public boolean doHurtTarget(Entity target) { + if (super.doHurtTarget(target)) { +diff --git a/src/main/java/net/minecraft/world/entity/monster/Creeper.java b/src/main/java/net/minecraft/world/entity/monster/Creeper.java +index cbcb2bfa8f91099e5c374f590f48885390bdf7a7..1829bedfa8084c4932a0e67c36f48f19993e22b6 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/Creeper.java ++++ b/src/main/java/net/minecraft/world/entity/monster/Creeper.java +@@ -61,21 +61,99 @@ public class Creeper extends Monster implements PowerableMob { + public int explosionRadius = 3; + private int droppedSkulls; + private Player entityIgniter; // CraftBukkit ++ // Purpur start ++ private int spacebarCharge = 0; ++ private int prevSpacebarCharge = 0; ++ private int powerToggleDelay = 0; ++ private boolean exploding = false; ++ // Purpur end + + public Creeper(EntityType type, Level world) { + super(type, world); + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level().purpurConfig.creeperRidable; ++ } ++ ++ @Override ++ public boolean dismountsUnderwater() { ++ return level().purpurConfig.useDismountsUnderwaterTag ? super.dismountsUnderwater() : !level().purpurConfig.creeperRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level().purpurConfig.creeperControllable; ++ } ++ ++ @Override ++ protected void customServerAiStep() { ++ if (powerToggleDelay > 0) { ++ powerToggleDelay--; ++ } ++ if (getRider() != null && this.isControllable()) { ++ if (getRider().getForwardMot() != 0 || getRider().getStrafeMot() != 0) { ++ spacebarCharge = 0; ++ setIgnited(false); ++ setSwellDir(-1); ++ } ++ if (spacebarCharge == prevSpacebarCharge) { ++ spacebarCharge = 0; ++ } ++ prevSpacebarCharge = spacebarCharge; ++ } ++ super.customServerAiStep(); ++ } ++ ++ @Override ++ public void onMount(Player rider) { ++ super.onMount(rider); ++ setIgnited(false); ++ setSwellDir(-1); ++ } ++ ++ @Override ++ public boolean onSpacebar() { ++ if (powerToggleDelay > 0) { ++ return true; // just toggled power, do not jump or ignite ++ } ++ spacebarCharge++; ++ if (spacebarCharge > maxSwell - 2) { ++ spacebarCharge = 0; ++ if (getRider() != null && getRider().getBukkitEntity().hasPermission("allow.powered.creeper")) { ++ powerToggleDelay = 20; ++ setPowered(!isPowered()); ++ setIgnited(false); ++ setSwellDir(-1); ++ return true; ++ } ++ } ++ if (!isIgnited()) { ++ if (getRider() != null && getRider().getForwardMot() == 0 && getRider().getStrafeMot() == 0 && ++ getRider().getBukkitEntity().hasPermission("allow.special.creeper")) { ++ setIgnited(true); ++ setSwellDir(1); ++ return true; ++ } ++ } ++ return getForwardMot() == 0 && getStrafeMot() == 0; // do not jump if standing still ++ } ++ // Purpur end ++ + @Override + protected void registerGoals() { + this.goalSelector.addGoal(1, new FloatGoal(this)); + this.goalSelector.addGoal(2, new SwellGoal(this)); ++ this.goalSelector.addGoal(3, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.goalSelector.addGoal(3, new AvoidEntityGoal<>(this, Ocelot.class, 6.0F, 1.0D, 1.2D)); + this.goalSelector.addGoal(3, new AvoidEntityGoal<>(this, Cat.class, 6.0F, 1.0D, 1.2D)); + this.goalSelector.addGoal(4, new MeleeAttackGoal(this, 1.0D, false)); + this.goalSelector.addGoal(5, new WaterAvoidingRandomStrollGoal(this, 0.8D)); + this.goalSelector.addGoal(6, new LookAtPlayerGoal(this, Player.class, 8.0F)); + this.goalSelector.addGoal(6, new RandomLookAroundGoal(this)); ++ this.targetSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.targetSelector.addGoal(1, new NearestAttackableTargetGoal<>(this, Player.class, true)); + this.targetSelector.addGoal(2, new HurtByTargetGoal(this, new Class[0])); + } +@@ -175,6 +253,37 @@ public class Creeper extends Monster implements PowerableMob { + } + } + ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level().purpurConfig.creeperMaxHealth); ++ } ++ ++ public net.minecraft.world.entity.SpawnGroupData finalizeSpawn(net.minecraft.world.level.ServerLevelAccessor world, net.minecraft.world.DifficultyInstance difficulty, net.minecraft.world.entity.MobSpawnType spawnReason, @Nullable net.minecraft.world.entity.SpawnGroupData entityData) { ++ double chance = world.getLevel().purpurConfig.creeperChargedChance; ++ if (chance > 0D && random.nextDouble() <= chance) { ++ setPowered(true); ++ } ++ return super.finalizeSpawn(world, difficulty, spawnReason, entityData); ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level().purpurConfig.creeperTakeDamageFromWater; ++ } ++ ++ @Override ++ protected org.bukkit.event.entity.EntityDeathEvent dropAllDeathLoot(DamageSource damagesource) { ++ if (!exploding && this.level().purpurConfig.creeperExplodeWhenKilled && damagesource.getEntity() instanceof net.minecraft.server.level.ServerPlayer) { ++ this.explodeCreeper(); ++ } ++ return super.dropAllDeathLoot(damagesource); ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level().purpurConfig.creeperAlwaysDropExp; ++ } ++ + @Override + protected SoundEvent getHurtSound(DamageSource source) { + return SoundEvents.CREEPER_HURT; +@@ -263,15 +372,17 @@ public class Creeper extends Monster implements PowerableMob { + } + + public void explodeCreeper() { ++ this.exploding = true; // Purpur + if (!this.level().isClientSide) { + float f = this.isPowered() ? 2.0F : 1.0F; ++ float multiplier = this.level().purpurConfig.creeperHealthRadius ? this.getHealth() / this.getMaxHealth() : 1; // Purpur + + // CraftBukkit start +- ExplosionPrimeEvent event = CraftEventFactory.callExplosionPrimeEvent(this, this.explosionRadius * f, false); ++ ExplosionPrimeEvent event = CraftEventFactory.callExplosionPrimeEvent(this, (this.explosionRadius * f) * multiplier, false); // Purpur + if (!event.isCancelled()) { + // CraftBukkit end + this.dead = true; +- this.level().explode(this, this.getX(), this.getY(), this.getZ(), event.getRadius(), event.getFire(), Level.ExplosionInteraction.MOB); // CraftBukkit // Paper - fix DamageSource API ++ this.level().explode(this, this.getX(), this.getY(), this.getZ(), event.getRadius(), event.getFire(), this.level().getGameRules().getBoolean(net.minecraft.world.level.GameRules.RULE_MOBGRIEFING) && level().purpurConfig.creeperAllowGriefing ? Level.ExplosionInteraction.MOB : Level.ExplosionInteraction.NONE); // CraftBukkit // Paper - fix DamageSource API // Purpur + this.discard(EntityRemoveEvent.Cause.EXPLODE); // CraftBukkit - add Bukkit remove cause + this.spawnLingeringCloud(); + // CraftBukkit start +@@ -281,7 +392,7 @@ public class Creeper extends Monster implements PowerableMob { + } + // CraftBukkit end + } +- ++ this.exploding = false; // Purpur + } + + private void spawnLingeringCloud() { +@@ -323,6 +434,7 @@ public class Creeper extends Monster implements PowerableMob { + com.destroystokyo.paper.event.entity.CreeperIgniteEvent event = new com.destroystokyo.paper.event.entity.CreeperIgniteEvent((org.bukkit.entity.Creeper) getBukkitEntity(), ignited); + if (event.callEvent()) { + this.entityData.set(Creeper.DATA_IS_IGNITED, event.isIgnited()); ++ if (!event.isIgnited()) setSwellDir(-1); // Purpur + } + } + // Paper end - CreeperIgniteEvent +diff --git a/src/main/java/net/minecraft/world/entity/monster/Drowned.java b/src/main/java/net/minecraft/world/entity/monster/Drowned.java +index cff1b5e0e3fd32d82157d5f13d83d4abdfad7378..15afee3c4f6307557321424560d2340e23cd472b 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/Drowned.java ++++ b/src/main/java/net/minecraft/world/entity/monster/Drowned.java +@@ -29,6 +29,7 @@ import net.minecraft.world.entity.ai.goal.MoveToBlockGoal; + import net.minecraft.world.entity.ai.goal.RandomStrollGoal; + import net.minecraft.world.entity.ai.goal.RangedAttackGoal; + import net.minecraft.world.entity.ai.goal.ZombieAttackGoal; ++import net.minecraft.world.entity.ai.goal.MoveThroughVillageGoal; + import net.minecraft.world.entity.ai.goal.target.HurtByTargetGoal; + import net.minecraft.world.entity.ai.goal.target.NearestAttackableTargetGoal; + import net.minecraft.world.entity.ai.navigation.GroundPathNavigation; +@@ -71,6 +72,58 @@ public class Drowned extends Zombie implements RangedAttackMob { + return Zombie.createAttributes().add(Attributes.STEP_HEIGHT, 1.0D); + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level().purpurConfig.drownedRidable; ++ } ++ ++ @Override ++ public boolean dismountsUnderwater() { ++ return level().purpurConfig.useDismountsUnderwaterTag ? super.dismountsUnderwater() : !level().purpurConfig.drownedRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level().purpurConfig.drownedControllable; ++ } ++ // Purpur end ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level().purpurConfig.drownedMaxHealth); ++ } ++ ++ @Override ++ protected void randomizeReinforcementsChance() { ++ this.getAttribute(Attributes.SPAWN_REINFORCEMENTS_CHANCE).setBaseValue(this.random.nextDouble() * this.level().purpurConfig.drownedSpawnReinforcements); ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level().purpurConfig.drownedTakeDamageFromWater; ++ } ++ ++ @Override ++ public boolean jockeyOnlyBaby() { ++ return level().purpurConfig.drownedJockeyOnlyBaby; ++ } ++ ++ @Override ++ public double jockeyChance() { ++ return level().purpurConfig.drownedJockeyChance; ++ } ++ ++ @Override ++ public boolean jockeyTryExistingChickens() { ++ return level().purpurConfig.drownedJockeyTryExistingChickens; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level().purpurConfig.drownedAlwaysDropExp; ++ } ++ + @Override + protected void addBehaviourGoals() { + this.goalSelector.addGoal(1, new Drowned.DrownedGoToWaterGoal(this, 1.0D)); +@@ -78,10 +131,23 @@ public class Drowned extends Zombie implements RangedAttackMob { + this.goalSelector.addGoal(2, new Drowned.DrownedAttackGoal(this, 1.0D, false)); + this.goalSelector.addGoal(5, new Drowned.DrownedGoToBeachGoal(this, 1.0D)); + this.goalSelector.addGoal(6, new Drowned.DrownedSwimUpGoal(this, 1.0D, this.level().getSeaLevel())); ++ if (level().purpurConfig.drownedBreakDoors) this.goalSelector.addGoal(6, new MoveThroughVillageGoal(this, 1.0D, true, 4, this::canBreakDoors)); + this.goalSelector.addGoal(7, new RandomStrollGoal(this, 1.0D)); + this.targetSelector.addGoal(1, (new HurtByTargetGoal(this, new Class[]{Drowned.class})).setAlertOthers(ZombifiedPiglin.class)); + this.targetSelector.addGoal(2, new NearestAttackableTargetGoal<>(this, Player.class, 10, true, false, this::okTarget)); +- if (this.level().spigotConfig.zombieAggressiveTowardsVillager) this.targetSelector.addGoal(3, new NearestAttackableTargetGoal<>(this, AbstractVillager.class, false)); // Paper - Check drowned for villager aggression config ++ // Purpur start ++ if (this.level().spigotConfig.zombieAggressiveTowardsVillager) this.targetSelector.addGoal(3, new NearestAttackableTargetGoal<>(this, AbstractVillager.class, false) { // Paper - Check drowned for villager aggression config ++ @Override ++ public boolean canUse() { ++ return (level().purpurConfig.zombieAggressiveTowardsVillagerWhenLagging || !level().getServer().server.isLagging()) && super.canUse(); ++ } ++ ++ @Override ++ public boolean canContinueToUse() { ++ return (level().purpurConfig.zombieAggressiveTowardsVillagerWhenLagging || !level().getServer().server.isLagging()) && super.canContinueToUse(); ++ } ++ }); ++ // Purpur end + this.targetSelector.addGoal(3, new NearestAttackableTargetGoal<>(this, IronGolem.class, true)); + this.targetSelector.addGoal(3, new NearestAttackableTargetGoal<>(this, Axolotl.class, true, false)); + this.targetSelector.addGoal(5, new NearestAttackableTargetGoal<>(this, Turtle.class, 10, true, false, Turtle.BABY_ON_LAND_SELECTOR)); +@@ -115,7 +181,7 @@ public class Drowned extends Zombie implements RangedAttackMob { + + @Override + public boolean supportsBreakDoorGoal() { +- return false; ++ return level().purpurConfig.drownedBreakDoors ? true : false; + } + + @Override +@@ -262,8 +328,7 @@ public class Drowned extends Zombie implements RangedAttackMob { + this.searchingForLand = targetingUnderwater; + } + +- private static class DrownedMoveControl extends MoveControl { +- ++ private static class DrownedMoveControl extends org.purpurmc.purpur.controller.MoveControllerWASD { // Purpur + private final Drowned drowned; + + public DrownedMoveControl(Drowned drowned) { +@@ -272,7 +337,7 @@ public class Drowned extends Zombie implements RangedAttackMob { + } + + @Override +- public void tick() { ++ public void vanillaTick() { // Purpur + LivingEntity entityliving = this.drowned.getTarget(); + + if (this.drowned.wantsToSwim() && this.drowned.isInWater()) { +@@ -295,7 +360,7 @@ public class Drowned extends Zombie implements RangedAttackMob { + + this.drowned.setYRot(this.rotlerp(this.drowned.getYRot(), f, 90.0F)); + this.drowned.yBodyRot = this.drowned.getYRot(); +- float f1 = (float) (this.speedModifier * this.drowned.getAttributeValue(Attributes.MOVEMENT_SPEED)); ++ float f1 = (float) (this.getSpeedModifier() * this.drowned.getAttributeValue(Attributes.MOVEMENT_SPEED)); // Purpur + float f2 = Mth.lerp(0.125F, this.drowned.getSpeed(), f1); + + this.drowned.setSpeed(f2); +@@ -305,7 +370,7 @@ public class Drowned extends Zombie implements RangedAttackMob { + this.drowned.setDeltaMovement(this.drowned.getDeltaMovement().add(0.0D, -0.008D, 0.0D)); + } + +- super.tick(); ++ super.vanillaTick(); // Purpur + } + + } +diff --git a/src/main/java/net/minecraft/world/entity/monster/ElderGuardian.java b/src/main/java/net/minecraft/world/entity/monster/ElderGuardian.java +index fd995b1f29c47884e9db2cb92f1dd615d62ae032..7e8603ef5df722f19e85b9c5cdd4ebfdd6481e42 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/ElderGuardian.java ++++ b/src/main/java/net/minecraft/world/entity/monster/ElderGuardian.java +@@ -33,6 +33,33 @@ public class ElderGuardian extends Guardian { + + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level().purpurConfig.elderGuardianRidable; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level().purpurConfig.elderGuardianControllable; ++ } ++ // Purpur end ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level().purpurConfig.elderGuardianMaxHealth); ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level().purpurConfig.elderGuardianTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level().purpurConfig.elderGuardianAlwaysDropExp; ++ } ++ + public static AttributeSupplier.Builder createAttributes() { + return Guardian.createAttributes().add(Attributes.MOVEMENT_SPEED, 0.30000001192092896D).add(Attributes.ATTACK_DAMAGE, 8.0D).add(Attributes.MAX_HEALTH, 80.0D); + } +diff --git a/src/main/java/net/minecraft/world/entity/monster/EnderMan.java b/src/main/java/net/minecraft/world/entity/monster/EnderMan.java +index 260202fab3ac300552c557b44dcf251f083c6a78..5b49a6b1884c33bedafca5cff0214cdfccd87302 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/EnderMan.java ++++ b/src/main/java/net/minecraft/world/entity/monster/EnderMan.java +@@ -90,12 +90,40 @@ public class EnderMan extends Monster implements NeutralMob { + + public EnderMan(EntityType type, Level world) { + super(type, world); +- this.setPathfindingMalus(PathType.WATER, -1.0F); ++ if (isSensitiveToWater()) this.setPathfindingMalus(PathType.WATER, -1.0F); // Purpur ++ } ++ ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level().purpurConfig.endermanRidable; ++ } ++ ++ @Override ++ public boolean dismountsUnderwater() { ++ return level().purpurConfig.useDismountsUnderwaterTag ? super.dismountsUnderwater() : !level().purpurConfig.endermanRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level().purpurConfig.endermanControllable; ++ } ++ // Purpur end ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level().purpurConfig.endermanMaxHealth); ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level().purpurConfig.endermanAlwaysDropExp; + } + + @Override + protected void registerGoals() { + this.goalSelector.addGoal(0, new FloatGoal(this)); ++ this.goalSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.goalSelector.addGoal(1, new EnderMan.EndermanFreezeWhenLookedAt(this)); + this.goalSelector.addGoal(2, new MeleeAttackGoal(this, 1.0D, false)); + this.goalSelector.addGoal(7, new WaterAvoidingRandomStrollGoal(this, 1.0D, 0.0F)); +@@ -103,9 +131,10 @@ public class EnderMan extends Monster implements NeutralMob { + this.goalSelector.addGoal(8, new RandomLookAroundGoal(this)); + this.goalSelector.addGoal(10, new EnderMan.EndermanLeaveBlockGoal(this)); + this.goalSelector.addGoal(11, new EnderMan.EndermanTakeBlockGoal(this)); ++ this.targetSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.targetSelector.addGoal(1, new EnderMan.EndermanLookForPlayerGoal(this, this::isAngryAt)); + this.targetSelector.addGoal(2, new HurtByTargetGoal(this, new Class[0])); +- this.targetSelector.addGoal(3, new NearestAttackableTargetGoal<>(this, Endermite.class, true, false)); ++ this.targetSelector.addGoal(3, new NearestAttackableTargetGoal<>(this, Endermite.class, 10, true, false, (entityliving) -> entityliving.level().purpurConfig.endermanAggroEndermites && entityliving instanceof Endermite endermite && (!entityliving.level().purpurConfig.endermanAggroEndermitesOnlyIfPlayerSpawned || endermite.isPlayerSpawned()))); // Purpur + this.targetSelector.addGoal(4, new ResetUniversalAngerTargetGoal<>(this, false)); + } + +@@ -242,7 +271,7 @@ public class EnderMan extends Monster implements NeutralMob { + // Paper end - EndermanAttackPlayerEvent + ItemStack itemstack = (ItemStack) player.getInventory().armor.get(3); + +- if (itemstack.is(Blocks.CARVED_PUMPKIN.asItem())) { ++ if (this.level().purpurConfig.endermanDisableStareAggro || itemstack.is(Blocks.CARVED_PUMPKIN.asItem()) || (this.level().purpurConfig.endermanIgnorePlayerDragonHead && itemstack.is(net.minecraft.world.item.Items.DRAGON_HEAD))) { // Purpur + return false; + } else { + Vec3 vec3d = player.getViewVector(1.0F).normalize(); +@@ -274,12 +303,12 @@ public class EnderMan extends Monster implements NeutralMob { + + @Override + public boolean isSensitiveToWater() { +- return true; ++ return this.level().purpurConfig.endermanTakeDamageFromWater; // Purpur + } + + @Override + protected void customServerAiStep() { +- if (this.level().isDay() && this.tickCount >= this.targetChangeTime + 600) { ++ if ((getRider() == null || !this.isControllable()) && this.level().isDay() && this.tickCount >= this.targetChangeTime + 600) { // Purpur - no random teleporting + float f = this.getLightLevelDependentMagicValue(); + + if (f > 0.5F && this.level().canSeeSky(this.blockPosition()) && this.random.nextFloat() * 30.0F < (f - 0.4F) * 2.0F && this.tryEscape(com.destroystokyo.paper.event.entity.EndermanEscapeEvent.Reason.RUNAWAY)) { // Paper - EndermanEscapeEvent +@@ -394,6 +423,8 @@ public class EnderMan extends Monster implements NeutralMob { + public boolean hurt(DamageSource source, float amount) { + if (this.isInvulnerableTo(source)) { + return false; ++ } else if (getRider() != null && this.isControllable()) { return super.hurt(source, amount); // Purpur - no teleporting on damage ++ } else if (org.purpurmc.purpur.PurpurConfig.endermanShortHeight && source.is(net.minecraft.world.damagesource.DamageTypes.IN_WALL)) { return false; // Purpur - no suffocation damage if short height + } else { + boolean flag = source.getDirectEntity() instanceof ThrownPotion; + boolean flag1; +@@ -408,6 +439,7 @@ public class EnderMan extends Monster implements NeutralMob { + } else { + flag1 = flag && this.hurtWithCleanWater(source, (ThrownPotion) source.getDirectEntity(), amount); + ++ if (!flag1 && this.level().purpurConfig.endermanIgnoreProjectiles) return super.hurt(source, amount); // Purpur + if (this.tryEscape(com.destroystokyo.paper.event.entity.EndermanEscapeEvent.Reason.INDIRECT)) { // Paper - EndermanEscapeEvent + for (int i = 0; i < 64; ++i) { + if (this.teleport()) { +@@ -452,7 +484,7 @@ public class EnderMan extends Monster implements NeutralMob { + + @Override + public boolean requiresCustomPersistence() { +- return super.requiresCustomPersistence() || this.getCarriedBlock() != null; ++ return super.requiresCustomPersistence() || (!this.level().purpurConfig.endermanDespawnEvenWithBlock && this.getCarriedBlock() != null); // Purpur + } + + private static class EndermanFreezeWhenLookedAt extends Goal { +@@ -499,7 +531,16 @@ public class EnderMan extends Monster implements NeutralMob { + + @Override + public boolean canUse() { +- return this.enderman.getCarriedBlock() == null ? false : (!this.enderman.level().getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING) ? false : this.enderman.getRandom().nextInt(reducedTickDelay(2000)) == 0); ++ if (!enderman.level().purpurConfig.endermanAllowGriefing) return false; // Purpur ++ // Purpur start ++ if (this.enderman.getCarriedBlock() == null) { ++ return false; ++ } ++ if (!this.enderman.level().getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING) && !this.enderman.level().purpurConfig.endermanBypassMobGriefing) { ++ return false; ++ } ++ return this.enderman.getRandom().nextInt(reducedTickDelay(2000)) == 0; ++ // Purpur end + } + + @Override +@@ -544,7 +585,16 @@ public class EnderMan extends Monster implements NeutralMob { + + @Override + public boolean canUse() { +- return this.enderman.getCarriedBlock() != null ? false : (!this.enderman.level().getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING) ? false : this.enderman.getRandom().nextInt(reducedTickDelay(20)) == 0); ++ if (!enderman.level().purpurConfig.endermanAllowGriefing) return false; // Purpur ++ // Purpur start ++ if (this.enderman.getCarriedBlock() != null) { ++ return false; ++ } ++ if (!this.enderman.level().getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING) && !this.enderman.level().purpurConfig.endermanBypassMobGriefing) { ++ return false; ++ } ++ return this.enderman.getRandom().nextInt(reducedTickDelay(20)) == 0; ++ // Purpur end + } + + @Override +diff --git a/src/main/java/net/minecraft/world/entity/monster/Endermite.java b/src/main/java/net/minecraft/world/entity/monster/Endermite.java +index 9c78905762d9a484878fa9cf03a2ca3850e7e613..14d6796a124a85b8cbf5f3b719d89d99e0cf8db5 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/Endermite.java ++++ b/src/main/java/net/minecraft/world/entity/monster/Endermite.java +@@ -32,20 +32,63 @@ public class Endermite extends Monster { + + private static final int MAX_LIFE = 2400; + public int life; ++ private boolean isPlayerSpawned; // Purpur + + public Endermite(EntityType type, Level world) { + super(type, world); + this.xpReward = 3; + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level().purpurConfig.endermiteRidable; ++ } ++ ++ @Override ++ public boolean dismountsUnderwater() { ++ return level().purpurConfig.useDismountsUnderwaterTag ? super.dismountsUnderwater() : !level().purpurConfig.endermiteRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level().purpurConfig.endermiteControllable; ++ } ++ // Purpur end ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level().purpurConfig.endermiteMaxHealth); ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level().purpurConfig.endermiteTakeDamageFromWater; ++ } ++ ++ public boolean isPlayerSpawned() { ++ return this.isPlayerSpawned; ++ } ++ ++ public void setPlayerSpawned(boolean playerSpawned) { ++ this.isPlayerSpawned = playerSpawned; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level().purpurConfig.endermiteAlwaysDropExp; ++ } ++ + @Override + protected void registerGoals() { + this.goalSelector.addGoal(1, new FloatGoal(this)); ++ this.goalSelector.addGoal(1, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.goalSelector.addGoal(1, new ClimbOnTopOfPowderSnowGoal(this, this.level())); + this.goalSelector.addGoal(2, new MeleeAttackGoal(this, 1.0D, false)); + this.goalSelector.addGoal(3, new WaterAvoidingRandomStrollGoal(this, 1.0D)); + this.goalSelector.addGoal(7, new LookAtPlayerGoal(this, Player.class, 8.0F)); + this.goalSelector.addGoal(8, new RandomLookAroundGoal(this)); ++ this.targetSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.targetSelector.addGoal(1, (new HurtByTargetGoal(this, new Class[0])).setAlertOthers()); + this.targetSelector.addGoal(2, new NearestAttackableTargetGoal<>(this, Player.class, true)); + } +@@ -83,12 +126,14 @@ public class Endermite extends Monster { + public void readAdditionalSaveData(CompoundTag nbt) { + super.readAdditionalSaveData(nbt); + this.life = nbt.getInt("Lifetime"); ++ this.isPlayerSpawned = nbt.getBoolean("PlayerSpawned"); // Purpur + } + + @Override + public void addAdditionalSaveData(CompoundTag nbt) { + super.addAdditionalSaveData(nbt); + nbt.putInt("Lifetime", this.life); ++ nbt.putBoolean("PlayerSpawned", this.isPlayerSpawned); // Purpur + } + + @Override +diff --git a/src/main/java/net/minecraft/world/entity/monster/Evoker.java b/src/main/java/net/minecraft/world/entity/monster/Evoker.java +index 38e866571c35ebc4843a8d4fa39691902a5fcc91..f92f93c780f4c176d6c02c4b98ffe3a4ef3993f6 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/Evoker.java ++++ b/src/main/java/net/minecraft/world/entity/monster/Evoker.java +@@ -52,10 +52,43 @@ public class Evoker extends SpellcasterIllager { + this.xpReward = 10; + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level().purpurConfig.evokerRidable; ++ } ++ ++ @Override ++ public boolean dismountsUnderwater() { ++ return level().purpurConfig.useDismountsUnderwaterTag ? super.dismountsUnderwater() : !level().purpurConfig.evokerRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level().purpurConfig.evokerControllable; ++ } ++ // Purpur end ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level().purpurConfig.evokerMaxHealth); ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level().purpurConfig.evokerTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level().purpurConfig.evokerAlwaysDropExp; ++ } ++ + @Override + protected void registerGoals() { + super.registerGoals(); + this.goalSelector.addGoal(0, new FloatGoal(this)); ++ this.goalSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.goalSelector.addGoal(1, new Evoker.EvokerCastingSpellGoal()); + this.goalSelector.addGoal(2, new AvoidEntityGoal<>(this, Player.class, 8.0F, 0.6D, 1.0D)); + this.goalSelector.addGoal(4, new Evoker.EvokerSummonSpellGoal()); +@@ -64,6 +97,7 @@ public class Evoker extends SpellcasterIllager { + this.goalSelector.addGoal(8, new RandomStrollGoal(this, 0.6D)); + this.goalSelector.addGoal(9, new LookAtPlayerGoal(this, Player.class, 3.0F, 1.0F)); + this.goalSelector.addGoal(10, new LookAtPlayerGoal(this, Mob.class, 8.0F)); ++ this.targetSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.targetSelector.addGoal(1, (new HurtByTargetGoal(this, new Class[]{Raider.class})).setAlertOthers()); + this.targetSelector.addGoal(2, (new NearestAttackableTargetGoal<>(this, Player.class, true)).setUnseenMemoryTicks(300)); + this.targetSelector.addGoal(3, (new NearestAttackableTargetGoal<>(this, AbstractVillager.class, false)).setUnseenMemoryTicks(300)); +@@ -340,7 +374,7 @@ public class Evoker extends SpellcasterIllager { + return false; + } else if (Evoker.this.tickCount < this.nextAttackTickCount) { + return false; +- } else if (!Evoker.this.level().getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING)) { ++ } else if (!Evoker.this.level().purpurConfig.evokerBypassMobGriefing && !Evoker.this.level().getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING)) { // Purpur + return false; + } else { + List list = Evoker.this.level().getNearbyEntities(Sheep.class, this.wololoTargeting, Evoker.this, Evoker.this.getBoundingBox().inflate(16.0D, 4.0D, 16.0D)); +diff --git a/src/main/java/net/minecraft/world/entity/monster/Ghast.java b/src/main/java/net/minecraft/world/entity/monster/Ghast.java +index 373a4f036157017b0d95e8f1849780582235a549..a25c82be45e3db5143f6bf617fedc2fa85bd89ca 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/Ghast.java ++++ b/src/main/java/net/minecraft/world/entity/monster/Ghast.java +@@ -43,11 +43,47 @@ public class Ghast extends FlyingMob implements Enemy { + this.moveControl = new Ghast.GhastMoveControl(this); + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level().purpurConfig.ghastRidable; ++ } ++ ++ @Override ++ public boolean dismountsUnderwater() { ++ return level().purpurConfig.useDismountsUnderwaterTag ? super.dismountsUnderwater() : !level().purpurConfig.ghastRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level().purpurConfig.ghastControllable; ++ } ++ ++ @Override ++ public double getMaxY() { ++ return level().purpurConfig.ghastMaxY; ++ } ++ ++ @Override ++ public void travel(Vec3 vec3) { ++ super.travel(vec3); ++ if (getRider() != null && this.isControllable() && !onGround) { ++ float speed = (float) getAttributeValue(Attributes.FLYING_SPEED); ++ setSpeed(speed); ++ Vec3 mot = getDeltaMovement(); ++ move(net.minecraft.world.entity.MoverType.SELF, mot.multiply(speed, 1.0, speed)); ++ setDeltaMovement(mot.scale(0.9D)); ++ } ++ } ++ // Purpur end ++ + @Override + protected void registerGoals() { ++ this.goalSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.goalSelector.addGoal(5, new Ghast.RandomFloatAroundGoal(this)); + this.goalSelector.addGoal(7, new Ghast.GhastLookGoal(this)); + this.goalSelector.addGoal(7, new Ghast.GhastShootFireballGoal(this)); ++ this.targetSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.targetSelector.addGoal(1, new NearestAttackableTargetGoal<>(this, Player.class, 10, true, false, (entityliving) -> { + return Math.abs(entityliving.getY() - this.getY()) <= 4.0D; + })); +@@ -95,6 +131,21 @@ public class Ghast extends FlyingMob implements Enemy { + } + } + ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level().purpurConfig.ghastMaxHealth); ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level().purpurConfig.ghastTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level().purpurConfig.ghastAlwaysDropExp; ++ } ++ + @Override + protected void defineSynchedData(SynchedEntityData.Builder builder) { + super.defineSynchedData(builder); +@@ -102,7 +153,7 @@ public class Ghast extends FlyingMob implements Enemy { + } + + public static AttributeSupplier.Builder createAttributes() { +- return Mob.createMobAttributes().add(Attributes.MAX_HEALTH, 10.0D).add(Attributes.FOLLOW_RANGE, 100.0D); ++ return Mob.createMobAttributes().add(Attributes.MAX_HEALTH, 10.0D).add(Attributes.FOLLOW_RANGE, 100.0D).add(Attributes.FLYING_SPEED, 0.6D); // Purpur + } + + @Override +@@ -154,7 +205,7 @@ public class Ghast extends FlyingMob implements Enemy { + + } + +- private static class GhastMoveControl extends MoveControl { ++ private static class GhastMoveControl extends org.purpurmc.purpur.controller.FlyingMoveControllerWASD { // Purpur + + private final Ghast ghast; + private int floatDuration; +@@ -165,7 +216,7 @@ public class Ghast extends FlyingMob implements Enemy { + } + + @Override +- public void tick() { ++ public void vanillaTick() { // Purpur + if (this.operation == MoveControl.Operation.MOVE_TO) { + if (this.floatDuration-- <= 0) { + this.floatDuration += this.ghast.getRandom().nextInt(5) + 2; +diff --git a/src/main/java/net/minecraft/world/entity/monster/Giant.java b/src/main/java/net/minecraft/world/entity/monster/Giant.java +index 118521ae54254b0a73bb7cba7b2871c9c26f89fc..5c2881d0be519c52cbba74d7b7ca3ea9b4536463 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/Giant.java ++++ b/src/main/java/net/minecraft/world/entity/monster/Giant.java +@@ -1,23 +1,128 @@ + package net.minecraft.world.entity.monster; + + import net.minecraft.core.BlockPos; ++import net.minecraft.nbt.CompoundTag; ++import net.minecraft.world.Difficulty; ++import net.minecraft.world.DifficultyInstance; + import net.minecraft.world.entity.EntityType; ++import net.minecraft.world.entity.EquipmentSlot; ++import net.minecraft.world.entity.MobSpawnType; ++import net.minecraft.world.entity.SpawnGroupData; + import net.minecraft.world.entity.ai.attributes.AttributeSupplier; + import net.minecraft.world.entity.ai.attributes.Attributes; ++import net.minecraft.world.entity.ai.goal.FloatGoal; ++import net.minecraft.world.entity.ai.goal.LookAtPlayerGoal; ++import net.minecraft.world.entity.ai.goal.MeleeAttackGoal; ++import net.minecraft.world.entity.ai.goal.MoveTowardsRestrictionGoal; ++import net.minecraft.world.entity.ai.goal.RandomLookAroundGoal; ++import net.minecraft.world.entity.ai.goal.WaterAvoidingRandomStrollGoal; ++import net.minecraft.world.entity.ai.goal.target.HurtByTargetGoal; ++import net.minecraft.world.entity.ai.goal.target.NearestAttackableTargetGoal; ++import net.minecraft.world.entity.animal.IronGolem; ++import net.minecraft.world.entity.animal.Turtle; ++import net.minecraft.world.entity.npc.Villager; ++import net.minecraft.world.entity.player.Player; ++import net.minecraft.world.item.ItemStack; ++import net.minecraft.world.item.Items; + import net.minecraft.world.level.Level; + import net.minecraft.world.level.LevelReader; ++import net.minecraft.world.level.ServerLevelAccessor; ++ ++import javax.annotation.Nullable; + + public class Giant extends Monster { + public Giant(EntityType type, Level world) { + super(type, world); + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level().purpurConfig.giantRidable; ++ } ++ ++ @Override ++ public boolean dismountsUnderwater() { ++ return level().purpurConfig.useDismountsUnderwaterTag ? super.dismountsUnderwater() : !level().purpurConfig.giantRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level().purpurConfig.giantControllable; ++ } ++ ++ @Override ++ protected void registerGoals() { ++ if (level().purpurConfig.giantHaveAI) { ++ this.goalSelector.addGoal(0, new FloatGoal(this)); ++ this.goalSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); ++ this.goalSelector.addGoal(7, new WaterAvoidingRandomStrollGoal(this, 1.0D)); ++ this.goalSelector.addGoal(8, new LookAtPlayerGoal(this, Player.class, 16.0F)); ++ this.goalSelector.addGoal(8, new RandomLookAroundGoal(this)); ++ this.goalSelector.addGoal(5, new MoveTowardsRestrictionGoal(this, 1.0D)); ++ if (level().purpurConfig.giantHaveHostileAI) { ++ this.goalSelector.addGoal(2, new MeleeAttackGoal(this, 1.0D, false)); ++ this.targetSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); ++ this.targetSelector.addGoal(1, new HurtByTargetGoal(this).setAlertOthers(ZombifiedPiglin.class)); ++ this.targetSelector.addGoal(2, new NearestAttackableTargetGoal<>(this, Player.class, true)); ++ this.targetSelector.addGoal(3, new NearestAttackableTargetGoal<>(this, Villager.class, false)); ++ this.targetSelector.addGoal(4, new NearestAttackableTargetGoal<>(this, IronGolem.class, true)); ++ this.targetSelector.addGoal(5, new NearestAttackableTargetGoal<>(this, Turtle.class, true)); ++ } ++ } ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level().purpurConfig.giantTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level().purpurConfig.giantAlwaysDropExp; ++ } ++ ++ @Override ++ protected void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level().purpurConfig.giantMaxHealth); ++ this.getAttribute(Attributes.MOVEMENT_SPEED).setBaseValue(this.level().purpurConfig.giantMovementSpeed); ++ this.getAttribute(Attributes.ATTACK_DAMAGE).setBaseValue(this.level().purpurConfig.giantAttackDamage); ++ } ++ ++ // Purpur end ++ + public static AttributeSupplier.Builder createAttributes() { + return Monster.createMonsterAttributes().add(Attributes.MAX_HEALTH, 100.0).add(Attributes.MOVEMENT_SPEED, 0.5).add(Attributes.ATTACK_DAMAGE, 50.0); + } + ++ @Override ++ public SpawnGroupData finalizeSpawn(ServerLevelAccessor world, DifficultyInstance difficulty, MobSpawnType spawnReason, @Nullable SpawnGroupData entityData) { ++ SpawnGroupData groupData = super.finalizeSpawn(world, difficulty, spawnReason, entityData); ++ if (groupData == null) { ++ populateDefaultEquipmentSlots(this.random, difficulty); ++ populateDefaultEquipmentEnchantments(this.random, difficulty); ++ } ++ return groupData; ++ } ++ ++ @Override ++ protected void populateDefaultEquipmentSlots(net.minecraft.util.RandomSource random, DifficultyInstance difficulty) { ++ super.populateDefaultEquipmentSlots(this.random, difficulty); ++ // TODO make configurable ++ if (random.nextFloat() < (level().getDifficulty() == Difficulty.HARD ? 0.1F : 0.05F)) { ++ this.setItemSlot(EquipmentSlot.MAINHAND, new ItemStack(Items.IRON_SWORD)); ++ } ++ } ++ ++ @Override ++ public float getJumpPower() { ++ // make giants jump as high as everything else relative to their size ++ // 1.0 makes bottom of feet about as high as their waist when they jump ++ return level().purpurConfig.giantJumpHeight; ++ } ++ + @Override + public float getWalkTargetValue(BlockPos pos, LevelReader world) { +- return world.getPathfindingCostFromLightLevels(pos); ++ return super.getWalkTargetValue(pos, world); // Purpur - fix light requirements for natural spawns + } + } +diff --git a/src/main/java/net/minecraft/world/entity/monster/Guardian.java b/src/main/java/net/minecraft/world/entity/monster/Guardian.java +index 6c2e2fd5826a5f8070502e20d1d140c3d70bd0d3..f9496126f75b4c1b89ec33617e577d83042e0290 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/Guardian.java ++++ b/src/main/java/net/minecraft/world/entity/monster/Guardian.java +@@ -66,15 +66,51 @@ public class Guardian extends Monster { + this.xpReward = 10; + this.setPathfindingMalus(PathType.WATER, 0.0F); + this.moveControl = new Guardian.GuardianMoveControl(this); ++ // Purpur start ++ this.lookControl = new org.purpurmc.purpur.controller.LookControllerWASD(this) { ++ @Override ++ public void setYawPitch(float yaw, float pitch) { ++ super.setYawPitch(yaw, pitch * 0.35F); ++ } ++ }; ++ // Purpur end + this.clientSideTailAnimation = this.random.nextFloat(); + this.clientSideTailAnimationO = this.clientSideTailAnimation; + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level().purpurConfig.guardianRidable; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level().purpurConfig.guardianControllable; ++ } ++ // Purpur end ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level().purpurConfig.guardianMaxHealth); ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level().purpurConfig.guardianTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level().purpurConfig.guardianAlwaysDropExp; ++ } ++ + @Override + protected void registerGoals() { + MoveTowardsRestrictionGoal pathfindergoalmovetowardsrestriction = new MoveTowardsRestrictionGoal(this, 1.0D); + + this.randomStrollGoal = new RandomStrollGoal(this, 1.0D, 80); ++ this.goalSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.goalSelector.addGoal(4, this.guardianAttackGoal = new Guardian.GuardianAttackGoal(this)); // CraftBukkit - assign field + this.goalSelector.addGoal(5, pathfindergoalmovetowardsrestriction); + this.goalSelector.addGoal(7, this.randomStrollGoal); +@@ -83,6 +119,7 @@ public class Guardian extends Monster { + this.goalSelector.addGoal(9, new RandomLookAroundGoal(this)); + this.randomStrollGoal.setFlags(EnumSet.of(Goal.Flag.MOVE, Goal.Flag.LOOK)); + pathfindergoalmovetowardsrestriction.setFlags(EnumSet.of(Goal.Flag.MOVE, Goal.Flag.LOOK)); ++ this.targetSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.targetSelector.addGoal(1, new NearestAttackableTargetGoal<>(this, LivingEntity.class, 10, true, false, new Guardian.GuardianAttackSelector(this))); + } + +@@ -333,7 +370,7 @@ public class Guardian extends Monster { + @Override + public void travel(Vec3 movementInput) { + if (this.isControlledByLocalInstance() && this.isInWater()) { +- this.moveRelative(0.1F, movementInput); ++ this.moveRelative(getRider() != null && this.isControllable() ? getSpeed() : 0.1F, movementInput); // Purpur + this.move(MoverType.SELF, this.getDeltaMovement()); + this.setDeltaMovement(this.getDeltaMovement().scale(0.9D)); + if (!this.isMoving() && this.getTarget() == null) { +@@ -345,7 +382,7 @@ public class Guardian extends Monster { + + } + +- private static class GuardianMoveControl extends MoveControl { ++ private static class GuardianMoveControl extends org.purpurmc.purpur.controller.WaterMoveControllerWASD { // Purpur + + private final Guardian guardian; + +@@ -354,8 +391,17 @@ public class Guardian extends Monster { + this.guardian = guardian; + } + ++ // Purpur start + @Override +- public void tick() { ++ public void purpurTick(Player rider) { ++ super.purpurTick(rider); ++ guardian.setDeltaMovement(guardian.getDeltaMovement().add(0.0D, 0.005D, 0.0D)); ++ guardian.setMoving(guardian.getForwardMot() > 0.0F); // control tail speed ++ } ++ // Purpur end ++ ++ @Override ++ public void vanillaTick() { // Purpur + if (this.operation == MoveControl.Operation.MOVE_TO && !this.guardian.getNavigation().isDone()) { + Vec3 vec3d = new Vec3(this.wantedX - this.guardian.getX(), this.wantedY - this.guardian.getY(), this.wantedZ - this.guardian.getZ()); + double d0 = vec3d.length(); +@@ -366,7 +412,7 @@ public class Guardian extends Monster { + + this.guardian.setYRot(this.rotlerp(this.guardian.getYRot(), f, 90.0F)); + this.guardian.yBodyRot = this.guardian.getYRot(); +- float f1 = (float) (this.speedModifier * this.guardian.getAttributeValue(Attributes.MOVEMENT_SPEED)); ++ float f1 = (float) (this.getSpeedModifier() * this.guardian.getAttributeValue(Attributes.MOVEMENT_SPEED)); // Purpur + float f2 = Mth.lerp(0.125F, this.guardian.getSpeed(), f1); + + this.guardian.setSpeed(f2); +diff --git a/src/main/java/net/minecraft/world/entity/monster/Husk.java b/src/main/java/net/minecraft/world/entity/monster/Husk.java +index c34c8483a026f61fe20935697d321d7ef5d8dfbc..95d3edc6c88d6ed0556c21c2623cdd5cfda35911 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/Husk.java ++++ b/src/main/java/net/minecraft/world/entity/monster/Husk.java +@@ -20,6 +20,59 @@ public class Husk extends Zombie { + + public Husk(EntityType type, Level world) { + super(type, world); ++ this.setShouldBurnInDay(false); // Purpur ++ } ++ ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level().purpurConfig.huskRidable; ++ } ++ ++ @Override ++ public boolean dismountsUnderwater() { ++ return level().purpurConfig.useDismountsUnderwaterTag ? super.dismountsUnderwater() : !level().purpurConfig.huskRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level().purpurConfig.huskControllable; ++ } ++ // Purpur end ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(net.minecraft.world.entity.ai.attributes.Attributes.MAX_HEALTH).setBaseValue(this.level().purpurConfig.huskMaxHealth); ++ } ++ ++ @Override ++ protected void randomizeReinforcementsChance() { ++ this.getAttribute(net.minecraft.world.entity.ai.attributes.Attributes.SPAWN_REINFORCEMENTS_CHANCE).setBaseValue(this.random.nextDouble() * this.level().purpurConfig.huskSpawnReinforcements); ++ } ++ ++ @Override ++ public boolean jockeyOnlyBaby() { ++ return level().purpurConfig.huskJockeyOnlyBaby; ++ } ++ ++ @Override ++ public double jockeyChance() { ++ return level().purpurConfig.huskJockeyChance; ++ } ++ ++ @Override ++ public boolean jockeyTryExistingChickens() { ++ return level().purpurConfig.huskJockeyTryExistingChickens; ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level().purpurConfig.huskTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level().purpurConfig.huskAlwaysDropExp; + } + + public static boolean checkHuskSpawnRules(EntityType type, ServerLevelAccessor world, MobSpawnType spawnReason, BlockPos pos, RandomSource random) { +@@ -28,7 +81,7 @@ public class Husk extends Zombie { + + @Override + public boolean isSunSensitive() { +- return false; ++ return this.shouldBurnInDay; // Purpur - moved to LivingEntity - keep methods for ABI compatibility + } + + @Override +diff --git a/src/main/java/net/minecraft/world/entity/monster/Illusioner.java b/src/main/java/net/minecraft/world/entity/monster/Illusioner.java +index a7964208c952cb4e34916ae6523850fc3921b07e..ae036a16e3677dfba451f4eb4505036d45e50cf3 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/Illusioner.java ++++ b/src/main/java/net/minecraft/world/entity/monster/Illusioner.java +@@ -56,10 +56,45 @@ public class Illusioner extends SpellcasterIllager implements RangedAttackMob { + + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level().purpurConfig.illusionerRidable; ++ } ++ ++ @Override ++ public boolean dismountsUnderwater() { ++ return level().purpurConfig.useDismountsUnderwaterTag ? super.dismountsUnderwater() : !level().purpurConfig.illusionerRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level().purpurConfig.illusionerControllable; ++ } ++ // Purpur end ++ ++ @Override ++ protected void initAttributes() { ++ this.getAttribute(Attributes.MOVEMENT_SPEED).setBaseValue(this.level().purpurConfig.illusionerMovementSpeed); ++ this.getAttribute(Attributes.FOLLOW_RANGE).setBaseValue(this.level().purpurConfig.illusionerFollowRange); ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level().purpurConfig.illusionerMaxHealth); ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level().purpurConfig.illusionerTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level().purpurConfig.illusionerAlwaysDropExp; ++ } ++ + @Override + protected void registerGoals() { + super.registerGoals(); + this.goalSelector.addGoal(0, new FloatGoal(this)); ++ this.goalSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.goalSelector.addGoal(1, new SpellcasterIllager.SpellcasterCastingSpellGoal()); + this.goalSelector.addGoal(4, new Illusioner.IllusionerMirrorSpellGoal()); + this.goalSelector.addGoal(5, new Illusioner.IllusionerBlindnessSpellGoal()); +@@ -67,6 +102,7 @@ public class Illusioner extends SpellcasterIllager implements RangedAttackMob { + this.goalSelector.addGoal(8, new RandomStrollGoal(this, 0.6D)); + this.goalSelector.addGoal(9, new LookAtPlayerGoal(this, Player.class, 3.0F, 1.0F)); + this.goalSelector.addGoal(10, new LookAtPlayerGoal(this, Mob.class, 8.0F)); ++ this.targetSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.targetSelector.addGoal(1, (new HurtByTargetGoal(this, new Class[]{Raider.class})).setAlertOthers()); + this.targetSelector.addGoal(2, (new NearestAttackableTargetGoal<>(this, Player.class, true)).setUnseenMemoryTicks(300)); + this.targetSelector.addGoal(3, (new NearestAttackableTargetGoal<>(this, AbstractVillager.class, false)).setUnseenMemoryTicks(300)); +diff --git a/src/main/java/net/minecraft/world/entity/monster/MagmaCube.java b/src/main/java/net/minecraft/world/entity/monster/MagmaCube.java +index 7be2393dc3cb79556d9767b09f43be0f81308a12..e7c79e8c72226285eb5a4763dcf8ddd27078539c 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/MagmaCube.java ++++ b/src/main/java/net/minecraft/world/entity/monster/MagmaCube.java +@@ -24,6 +24,58 @@ public class MagmaCube extends Slime { + super(type, world); + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level().purpurConfig.magmaCubeRidable; ++ } ++ ++ @Override ++ public boolean dismountsUnderwater() { ++ return level().purpurConfig.useDismountsUnderwaterTag ? super.dismountsUnderwater() : !level().purpurConfig.magmaCubeRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level().purpurConfig.magmaCubeControllable; ++ } ++ ++ @Override ++ public float getJumpPower() { ++ return 0.42F * this.getBlockJumpFactor(); // from EntityLiving ++ } ++ // Purpur end ++ ++ @Override ++ protected String getMaxHealthEquation() { ++ return level().purpurConfig.magmaCubeMaxHealth; ++ } ++ ++ @Override ++ protected String getAttackDamageEquation() { ++ return level().purpurConfig.magmaCubeAttackDamage; ++ } ++ ++ @Override ++ protected java.util.Map getMaxHealthCache() { ++ return level().purpurConfig.magmaCubeMaxHealthCache; ++ } ++ ++ @Override ++ protected java.util.Map getAttackDamageCache() { ++ return level().purpurConfig.magmaCubeAttackDamageCache; ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level().purpurConfig.magmaCubeTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level().purpurConfig.magmaCubeAlwaysDropExp; ++ } ++ + public static AttributeSupplier.Builder createAttributes() { + return Monster.createMonsterAttributes().add(Attributes.MOVEMENT_SPEED, 0.2F); + } +@@ -64,11 +116,12 @@ public class MagmaCube extends Slime { + } + + @Override +- protected void jumpFromGround() { ++ public void jumpFromGround() { // Purpur - protected -> public + Vec3 vec3 = this.getDeltaMovement(); + float f = (float)this.getSize() * 0.1F; + this.setDeltaMovement(vec3.x, (double)(this.getJumpPower() + f), vec3.z); + this.hasImpulse = true; ++ this.actualJump = false; // Purpur + } + + @Override +diff --git a/src/main/java/net/minecraft/world/entity/monster/Monster.java b/src/main/java/net/minecraft/world/entity/monster/Monster.java +index 759839e912c54598b257ad738481364940f88a18..e60e6b3e5ae5a468cfe649ed2222412f3bc8b268 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/Monster.java ++++ b/src/main/java/net/minecraft/world/entity/monster/Monster.java +@@ -88,6 +88,14 @@ public abstract class Monster extends PathfinderMob implements Enemy { + } + + public static boolean isDarkEnoughToSpawn(ServerLevelAccessor world, BlockPos pos, RandomSource random) { ++ // Purpur start ++ if (!world.getMinecraftWorld().purpurConfig.mobsSpawnOnPackedIce || !world.getMinecraftWorld().purpurConfig.mobsSpawnOnBlueIce) { ++ net.minecraft.world.level.block.state.BlockState spawnBlock = world.getBlockState(pos.below()); ++ if ((!world.getMinecraftWorld().purpurConfig.mobsSpawnOnPackedIce && spawnBlock.is(net.minecraft.world.level.block.Blocks.PACKED_ICE)) || (!world.getMinecraftWorld().purpurConfig.mobsSpawnOnBlueIce && spawnBlock.is(net.minecraft.world.level.block.Blocks.BLUE_ICE))) { ++ return false; ++ } ++ } ++ // Purpur end + if (world.getBrightness(LightLayer.SKY, pos) > random.nextInt(32)) { + return false; + } else { +diff --git a/src/main/java/net/minecraft/world/entity/monster/Phantom.java b/src/main/java/net/minecraft/world/entity/monster/Phantom.java +index 68f8945292753535a3b73acb9f48c1594f0789a4..26077bd6eeedbdae84613188cb0f336abb3563d2 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/Phantom.java ++++ b/src/main/java/net/minecraft/world/entity/monster/Phantom.java +@@ -48,6 +48,8 @@ public class Phantom extends FlyingMob implements Enemy { + Vec3 moveTargetPoint; + public BlockPos anchorPoint; + Phantom.AttackPhase attackPhase; ++ private static final net.minecraft.world.item.crafting.Ingredient TORCH = net.minecraft.world.item.crafting.Ingredient.of(net.minecraft.world.item.Items.TORCH, net.minecraft.world.item.Items.SOUL_TORCH); // Purpur ++ Vec3 crystalPosition; // Purpur + + public Phantom(EntityType type, Level world) { + super(type, world); +@@ -57,6 +59,92 @@ public class Phantom extends FlyingMob implements Enemy { + this.xpReward = 5; + this.moveControl = new Phantom.PhantomMoveControl(this); + this.lookControl = new Phantom.PhantomLookControl(this, this); ++ this.setShouldBurnInDay(true); // Purpur ++ } ++ ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level().purpurConfig.phantomRidable; ++ } ++ ++ @Override ++ public boolean dismountsUnderwater() { ++ return level().purpurConfig.useDismountsUnderwaterTag ? super.dismountsUnderwater() : !level().purpurConfig.phantomRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level().purpurConfig.phantomControllable; ++ } ++ ++ @Override ++ public double getMaxY() { ++ return level().purpurConfig.phantomMaxY; ++ } ++ ++ @Override ++ public void travel(Vec3 vec3) { ++ super.travel(vec3); ++ if (getRider() != null && this.isControllable() && !onGround) { ++ float speed = (float) getAttributeValue(Attributes.FLYING_SPEED); ++ setSpeed(speed); ++ Vec3 mot = getDeltaMovement(); ++ move(net.minecraft.world.entity.MoverType.SELF, mot.multiply(speed, speed, speed)); ++ setDeltaMovement(mot.scale(0.9D)); ++ } ++ } ++ ++ public static net.minecraft.world.entity.ai.attributes.AttributeSupplier.Builder createAttributes() { ++ return Monster.createMonsterAttributes().add(Attributes.FLYING_SPEED, 3.0D); ++ } ++ ++ @Override ++ public boolean onSpacebar() { ++ if (getRider() != null && getRider().getBukkitEntity().hasPermission("allow.special.phantom")) { ++ shoot(); ++ } ++ return false; ++ } ++ ++ public boolean shoot() { ++ org.bukkit.Location loc = ((org.bukkit.entity.LivingEntity) getBukkitEntity()).getEyeLocation(); ++ loc.setPitch(-loc.getPitch()); ++ org.bukkit.util.Vector target = loc.getDirection().normalize().multiply(100).add(loc.toVector()); ++ ++ org.purpurmc.purpur.entity.PhantomFlames flames = new org.purpurmc.purpur.entity.PhantomFlames(level(), this); ++ flames.canGrief = level().purpurConfig.phantomAllowGriefing; ++ flames.shoot(target.getX() - getX(), target.getY() - getY(), target.getZ() - getZ(), 1.0F, 5.0F); ++ level().addFreshEntity(flames); ++ return true; ++ } ++ ++ @Override ++ protected void dropFromLootTable(DamageSource damageSource, boolean causedByPlayer) { ++ boolean dropped = false; ++ if (lastHurtByPlayer == null && damageSource.getEntity() instanceof net.minecraft.world.entity.boss.enderdragon.EndCrystal) { ++ if (random.nextInt(5) < 1) { ++ dropped = spawnAtLocation(new net.minecraft.world.item.ItemStack(net.minecraft.world.item.Items.PHANTOM_MEMBRANE)) != null; ++ } ++ } ++ if (!dropped) { ++ super.dropFromLootTable(damageSource, causedByPlayer); ++ } ++ } ++ ++ public boolean isCirclingCrystal() { ++ return crystalPosition != null; ++ } ++ // Purpur end ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level().purpurConfig.phantomTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level().purpurConfig.phantomAlwaysDropExp; + } + + @Override +@@ -71,9 +159,17 @@ public class Phantom extends FlyingMob implements Enemy { + + @Override + protected void registerGoals() { +- this.goalSelector.addGoal(1, new Phantom.PhantomAttackStrategyGoal()); +- this.goalSelector.addGoal(2, new Phantom.PhantomSweepAttackGoal()); +- this.goalSelector.addGoal(3, new Phantom.PhantomCircleAroundAnchorGoal()); ++ // Purpur start ++ this.goalSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); ++ if (level().purpurConfig.phantomOrbitCrystalRadius > 0) { ++ this.goalSelector.addGoal(1, new FindCrystalGoal(this)); ++ this.goalSelector.addGoal(2, new OrbitCrystalGoal(this)); ++ } ++ this.goalSelector.addGoal(3, new Phantom.PhantomAttackStrategyGoal()); ++ this.goalSelector.addGoal(4, new Phantom.PhantomSweepAttackGoal()); ++ this.goalSelector.addGoal(5, new Phantom.PhantomCircleAroundAnchorGoal()); ++ this.targetSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); ++ // Purpur end + this.targetSelector.addGoal(1, new Phantom.PhantomAttackPlayerTargetGoal()); + } + +@@ -89,7 +185,10 @@ public class Phantom extends FlyingMob implements Enemy { + + private void updatePhantomSizeInfo() { + this.refreshDimensions(); +- this.getAttribute(Attributes.ATTACK_DAMAGE).setBaseValue((double) (6 + this.getPhantomSize())); ++ // Purpur start ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(getFromCache(() -> this.level().purpurConfig.phantomMaxHealth, () -> this.level().purpurConfig.phantomMaxHealthCache, () -> 20.0D)); ++ this.getAttribute(Attributes.ATTACK_DAMAGE).setBaseValue(getFromCache(() -> this.level().purpurConfig.phantomAttackDamage, () -> this.level().purpurConfig.phantomAttackDamageCache, () -> (double) 6 + this.getPhantomSize())); ++ // Purpur end + } + + public int getPhantomSize() { +@@ -114,6 +213,21 @@ public class Phantom extends FlyingMob implements Enemy { + return true; + } + ++ private double getFromCache(java.util.function.Supplier equation, java.util.function.Supplier> cache, java.util.function.Supplier defaultValue) { ++ int size = getPhantomSize(); ++ Double value = cache.get().get(size); ++ if (value == null) { ++ try { ++ value = ((Number) scriptEngine.eval("let size = " + size + "; " + equation.get())).doubleValue(); ++ } catch (javax.script.ScriptException e) { ++ e.printStackTrace(); ++ value = defaultValue.get(); ++ } ++ cache.get().put(size, value); ++ } ++ return value; ++ } ++ + @Override + public void tick() { + super.tick(); +@@ -134,14 +248,12 @@ public class Phantom extends FlyingMob implements Enemy { + this.level().addParticle(ParticleTypes.MYCELIUM, this.getX() - (double) f3, this.getY() + (double) f5, this.getZ() - (double) f4, 0.0D, 0.0D, 0.0D); + } + ++ if (level().purpurConfig.phantomFlamesOnSwoop && attackPhase == AttackPhase.SWOOP) shoot(); // Purpur + } + + @Override + public void aiStep() { +- if (this.isAlive() && this.shouldBurnInDay && this.isSunBurnTick()) { // Paper - shouldBurnInDay API +- this.igniteForSeconds(8); +- } +- ++ // Purpur - moved down to shouldBurnInDay() + super.aiStep(); + } + +@@ -153,7 +265,11 @@ public class Phantom extends FlyingMob implements Enemy { + @Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor world, DifficultyInstance difficulty, MobSpawnType spawnReason, @Nullable SpawnGroupData entityData) { + this.anchorPoint = this.blockPosition().above(5); +- this.setPhantomSize(0); ++ // Purpur start ++ int min = world.getLevel().purpurConfig.phantomMinSize; ++ int max = world.getLevel().purpurConfig.phantomMaxSize; ++ this.setPhantomSize(min == max ? min : world.getRandom().nextInt(max + 1 - min) + min); ++ // Purpur end + return super.finalizeSpawn(world, difficulty, spawnReason, entityData); + } + +@@ -169,7 +285,7 @@ public class Phantom extends FlyingMob implements Enemy { + if (nbt.hasUUID("Paper.SpawningEntity")) { + this.spawningEntity = nbt.getUUID("Paper.SpawningEntity"); + } +- if (nbt.contains("Paper.ShouldBurnInDay")) { ++ if (false && nbt.contains("Paper.ShouldBurnInDay")) { // Purpur - implemented in LivingEntity + this.shouldBurnInDay = nbt.getBoolean("Paper.ShouldBurnInDay"); + } + // Paper end +@@ -186,7 +302,7 @@ public class Phantom extends FlyingMob implements Enemy { + if (this.spawningEntity != null) { + nbt.putUUID("Paper.SpawningEntity", this.spawningEntity); + } +- nbt.putBoolean("Paper.ShouldBurnInDay", shouldBurnInDay); ++ // nbt.putBoolean("Paper.ShouldBurnInDay", shouldBurnInDay); // Purpur - implemented in LivingEntity + // Paper end + } + +@@ -242,8 +358,15 @@ public class Phantom extends FlyingMob implements Enemy { + return this.spawningEntity; + } + public void setSpawningEntity(java.util.UUID entity) { this.spawningEntity = entity; } +- private boolean shouldBurnInDay = true; +- public boolean shouldBurnInDay() { return shouldBurnInDay; } ++ ++ // private boolean shouldBurnInDay = true; // Purpur - moved to LivingEntity - keep methods for ABI compatibility ++ // Purpur start ++ public boolean shouldBurnInDay() { ++ boolean burnFromDaylight = this.shouldBurnInDay && this.level().purpurConfig.phantomBurnInDaylight; ++ boolean burnFromLightSource = this.level().purpurConfig.phantomBurnInLight > 0 && this.level().getMaxLocalRawBrightness(blockPosition()) >= this.level().purpurConfig.phantomBurnInLight; ++ return burnFromDaylight || burnFromLightSource; ++ } ++ // Purpur End + public void setShouldBurnInDay(boolean shouldBurnInDay) { this.shouldBurnInDay = shouldBurnInDay; } + // Paper end + +@@ -254,7 +377,125 @@ public class Phantom extends FlyingMob implements Enemy { + private AttackPhase() {} + } + +- private class PhantomMoveControl extends MoveControl { ++ // Purpur start ++ class FindCrystalGoal extends Goal { ++ private final Phantom phantom; ++ private net.minecraft.world.entity.boss.enderdragon.EndCrystal crystal; ++ private Comparator comparator; ++ ++ FindCrystalGoal(Phantom phantom) { ++ this.phantom = phantom; ++ this.comparator = Comparator.comparingDouble(phantom::distanceToSqr); ++ this.setFlags(EnumSet.of(Flag.LOOK)); ++ } ++ ++ @Override ++ public boolean canUse() { ++ double range = maxTargetRange(); ++ List crystals = level().getEntitiesOfClass(net.minecraft.world.entity.boss.enderdragon.EndCrystal.class, phantom.getBoundingBox().inflate(range)); ++ if (crystals.isEmpty()) { ++ return false; ++ } ++ crystals.sort(comparator); ++ crystal = crystals.get(0); ++ if (phantom.distanceToSqr(crystal) > range * range) { ++ crystal = null; ++ return false; ++ } ++ return true; ++ } ++ ++ @Override ++ public boolean canContinueToUse() { ++ if (crystal == null || !crystal.isAlive()) { ++ return false; ++ } ++ double range = maxTargetRange(); ++ return phantom.distanceToSqr(crystal) <= (range * range) * 2; ++ } ++ ++ @Override ++ public void start() { ++ phantom.crystalPosition = new Vec3(crystal.getX(), crystal.getY() + (phantom.random.nextInt(10) + 10), crystal.getZ()); ++ } ++ ++ @Override ++ public void stop() { ++ crystal = null; ++ phantom.crystalPosition = null; ++ super.stop(); ++ } ++ ++ private double maxTargetRange() { ++ return phantom.level().purpurConfig.phantomOrbitCrystalRadius; ++ } ++ } ++ ++ class OrbitCrystalGoal extends Goal { ++ private final Phantom phantom; ++ private float offset; ++ private float radius; ++ private float verticalChange; ++ private float direction; ++ ++ OrbitCrystalGoal(Phantom phantom) { ++ this.phantom = phantom; ++ this.setFlags(EnumSet.of(Flag.MOVE)); ++ } ++ ++ @Override ++ public boolean canUse() { ++ return phantom.isCirclingCrystal(); ++ } ++ ++ @Override ++ public void start() { ++ this.radius = 5.0F + phantom.random.nextFloat() * 10.0F; ++ this.verticalChange = -4.0F + phantom.random.nextFloat() * 9.0F; ++ this.direction = phantom.random.nextBoolean() ? 1.0F : -1.0F; ++ updateOffset(); ++ } ++ ++ @Override ++ public void tick() { ++ if (phantom.random.nextInt(350) == 0) { ++ this.verticalChange = -4.0F + phantom.random.nextFloat() * 9.0F; ++ } ++ if (phantom.random.nextInt(250) == 0) { ++ ++this.radius; ++ if (this.radius > 15.0F) { ++ this.radius = 5.0F; ++ this.direction = -this.direction; ++ } ++ } ++ if (phantom.random.nextInt(450) == 0) { ++ this.offset = phantom.random.nextFloat() * 2.0F * 3.1415927F; ++ updateOffset(); ++ } ++ if (phantom.moveTargetPoint.distanceToSqr(phantom.getX(), phantom.getY(), phantom.getZ()) < 4.0D) { ++ updateOffset(); ++ } ++ if (phantom.moveTargetPoint.y < phantom.getY() && !phantom.level().isEmptyBlock(new BlockPos(phantom).below(1))) { ++ this.verticalChange = Math.max(1.0F, this.verticalChange); ++ updateOffset(); ++ } ++ if (phantom.moveTargetPoint.y > phantom.getY() && !phantom.level().isEmptyBlock(new BlockPos(phantom).above(1))) { ++ this.verticalChange = Math.min(-1.0F, this.verticalChange); ++ updateOffset(); ++ } ++ } ++ ++ private void updateOffset() { ++ this.offset += this.direction * 15.0F * 0.017453292F; ++ phantom.moveTargetPoint = phantom.crystalPosition.add( ++ this.radius * Mth.cos(this.offset), ++ -4.0F + this.verticalChange, ++ this.radius * Mth.sin(this.offset)); ++ } ++ } ++ // Purpur end ++ ++ private class PhantomMoveControl extends org.purpurmc.purpur.controller.FlyingMoveControllerWASD { // Purpur + + private float speed = 0.1F; + +@@ -262,8 +503,19 @@ public class Phantom extends FlyingMob implements Enemy { + super(entity); + } + ++ // Purpur start ++ public void purpurTick(Player rider) { ++ if (!Phantom.this.onGround) { ++ // phantom is always in motion when flying ++ // TODO - FIX THIS ++ // rider.setForward(1.0F); ++ } ++ super.purpurTick(rider); ++ } ++ // Purpur end ++ + @Override +- public void tick() { ++ public void vanillaTick() { // Purpur + if (Phantom.this.horizontalCollision) { + Phantom.this.setYRot(Phantom.this.getYRot() + 180.0F); + this.speed = 0.1F; +@@ -309,14 +561,20 @@ public class Phantom extends FlyingMob implements Enemy { + } + } + +- private class PhantomLookControl extends LookControl { ++ private class PhantomLookControl extends org.purpurmc.purpur.controller.LookControllerWASD { // Purpur + + public PhantomLookControl(final Phantom entity, final Mob phantom) { + super(phantom); + } + ++ // Purpur start ++ public void purpurTick(Player rider) { ++ setYawPitch(rider.getYRot(), -rider.xRotO * 0.75F); ++ } ++ // Purpur end ++ + @Override +- public void tick() {} ++ public void vanillaTick() {} // Purpur + } + + private class PhantomBodyRotationControl extends BodyRotationControl { +@@ -403,6 +661,12 @@ public class Phantom extends FlyingMob implements Enemy { + return false; + } else if (!entityliving.isAlive()) { + return false; ++ // Purpur start ++ } else if (level().purpurConfig.phantomBurnInLight > 0 && level().getLightEmission(new BlockPos(Phantom.this)) >= level().purpurConfig.phantomBurnInLight) { ++ return false; ++ } else if (level().purpurConfig.phantomIgnorePlayersWithTorch && (TORCH.test(entityliving.getItemInHand(net.minecraft.world.InteractionHand.MAIN_HAND)) || TORCH.test(entityliving.getItemInHand(net.minecraft.world.InteractionHand.OFF_HAND)))) { ++ return false; ++ // Purpur end + } else { + if (entityliving instanceof Player) { + Player entityhuman = (Player) entityliving; +@@ -548,6 +812,7 @@ public class Phantom extends FlyingMob implements Enemy { + this.nextScanTick = reducedTickDelay(60); + List list = Phantom.this.level().getNearbyPlayers(this.attackTargeting, Phantom.this, Phantom.this.getBoundingBox().inflate(16.0D, 64.0D, 16.0D)); + ++ if (level().purpurConfig.phantomIgnorePlayersWithTorch) list.removeIf(human -> TORCH.test(human.getItemInHand(net.minecraft.world.InteractionHand.MAIN_HAND)) || TORCH.test(human.getItemInHand(net.minecraft.world.InteractionHand.OFF_HAND)));// Purpur + if (!list.isEmpty()) { + list.sort(Comparator.comparing((Entity e) -> { return e.getY(); }).reversed()); // CraftBukkit - decompile error + Iterator iterator = list.iterator(); +diff --git a/src/main/java/net/minecraft/world/entity/monster/Pillager.java b/src/main/java/net/minecraft/world/entity/monster/Pillager.java +index ac411202c0029052a962b51b015da191b124de5f..ae3eb87af8d3853be82c2002507141e43ab644de 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/Pillager.java ++++ b/src/main/java/net/minecraft/world/entity/monster/Pillager.java +@@ -58,15 +58,49 @@ public class Pillager extends AbstractIllager implements CrossbowAttackMob, Inve + super(type, world); + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level().purpurConfig.pillagerRidable; ++ } ++ ++ @Override ++ public boolean dismountsUnderwater() { ++ return level().purpurConfig.useDismountsUnderwaterTag ? super.dismountsUnderwater() : !level().purpurConfig.pillagerRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level().purpurConfig.pillagerControllable; ++ } ++ // Purpur end ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level().purpurConfig.pillagerMaxHealth); ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level().purpurConfig.pillagerTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level().purpurConfig.pillagerAlwaysDropExp; ++ } ++ + @Override + protected void registerGoals() { + super.registerGoals(); + this.goalSelector.addGoal(0, new FloatGoal(this)); ++ this.goalSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.goalSelector.addGoal(2, new Raider.HoldGroundAttackGoal(this, 10.0F)); // Paper - decomp fix + this.goalSelector.addGoal(3, new RangedCrossbowAttackGoal<>(this, 1.0D, 8.0F)); + this.goalSelector.addGoal(8, new RandomStrollGoal(this, 0.6D)); + this.goalSelector.addGoal(9, new LookAtPlayerGoal(this, Player.class, 15.0F, 1.0F)); + this.goalSelector.addGoal(10, new LookAtPlayerGoal(this, Mob.class, 15.0F)); ++ this.targetSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.targetSelector.addGoal(1, (new HurtByTargetGoal(this, new Class[]{Raider.class})).setAlertOthers()); + this.targetSelector.addGoal(2, new NearestAttackableTargetGoal<>(this, Player.class, true)); + this.targetSelector.addGoal(3, new NearestAttackableTargetGoal<>(this, AbstractVillager.class, false)); +diff --git a/src/main/java/net/minecraft/world/entity/monster/Ravager.java b/src/main/java/net/minecraft/world/entity/monster/Ravager.java +index 2d7b7c949faaaaae94c0043132a4a822f55df104..9551bd7c9bed37cf17910e7f71b82ed20fb2d759 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/Ravager.java ++++ b/src/main/java/net/minecraft/world/entity/monster/Ravager.java +@@ -68,14 +68,54 @@ public class Ravager extends Raider { + this.setPathfindingMalus(PathType.LEAVES, 0.0F); + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level().purpurConfig.ravagerRidable; ++ } ++ ++ @Override ++ public boolean dismountsUnderwater() { ++ return level().purpurConfig.useDismountsUnderwaterTag ? super.dismountsUnderwater() : !level().purpurConfig.ravagerRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level().purpurConfig.ravagerControllable; ++ } ++ ++ @Override ++ public void onMount(Player rider) { ++ super.onMount(rider); ++ getNavigation().stop(); ++ } ++ // Purpur end ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level().purpurConfig.ravagerMaxHealth); ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level().purpurConfig.ravagerTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level().purpurConfig.ravagerAlwaysDropExp; ++ } ++ + @Override + protected void registerGoals() { + super.registerGoals(); + this.goalSelector.addGoal(0, new FloatGoal(this)); ++ this.goalSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.goalSelector.addGoal(4, new MeleeAttackGoal(this, 1.0D, true)); + this.goalSelector.addGoal(5, new WaterAvoidingRandomStrollGoal(this, 0.4D)); + this.goalSelector.addGoal(6, new LookAtPlayerGoal(this, Player.class, 6.0F)); + this.goalSelector.addGoal(10, new LookAtPlayerGoal(this, Mob.class, 8.0F)); ++ this.targetSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.targetSelector.addGoal(2, (new HurtByTargetGoal(this, new Class[]{Raider.class})).setAlertOthers()); + this.targetSelector.addGoal(3, new NearestAttackableTargetGoal<>(this, Player.class, true)); + this.targetSelector.addGoal(4, new NearestAttackableTargetGoal<>(this, AbstractVillager.class, true, (entityliving) -> { +@@ -128,7 +168,7 @@ public class Ravager extends Raider { + @Override + public void aiStep() { + super.aiStep(); +- if (this.isAlive()) { ++ if (this.isAlive() && (getRider() == null || !this.isControllable())) { // Purpur + if (this.isImmobile()) { + this.getAttribute(Attributes.MOVEMENT_SPEED).setBaseValue(0.0D); + } else { +@@ -138,7 +178,7 @@ public class Ravager extends Raider { + this.getAttribute(Attributes.MOVEMENT_SPEED).setBaseValue(Mth.lerp(0.1D, d1, d0)); + } + +- if (this.horizontalCollision && this.level().getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING)) { ++ if (this.horizontalCollision && (this.level().purpurConfig.ravagerBypassMobGriefing || this.level().getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING))) { // Purpur + boolean flag = false; + AABB axisalignedbb = this.getBoundingBox().inflate(0.2D); + Iterator iterator = BlockPos.betweenClosed(Mth.floor(axisalignedbb.minX), Mth.floor(axisalignedbb.minY), Mth.floor(axisalignedbb.minZ), Mth.floor(axisalignedbb.maxX), Mth.floor(axisalignedbb.maxY), Mth.floor(axisalignedbb.maxZ)).iterator(); +@@ -148,7 +188,7 @@ public class Ravager extends Raider { + BlockState iblockdata = this.level().getBlockState(blockposition); + Block block = iblockdata.getBlock(); + +- if (block instanceof LeavesBlock) { ++ if (this.level().purpurConfig.ravagerGriefableBlocks.contains(block)) { // Purpur + // CraftBukkit start + if (!CraftEventFactory.callEntityChangeBlockEvent(this, blockposition, iblockdata.getFluidState().createLegacyBlock())) { // Paper - fix wrong block state + continue; +diff --git a/src/main/java/net/minecraft/world/entity/monster/Shulker.java b/src/main/java/net/minecraft/world/entity/monster/Shulker.java +index 5215fa54666979ef4da074ddfdb082e7274f2957..1afa6dc4f2a6437cd4cc3e49694e79641fcc13ad 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/Shulker.java ++++ b/src/main/java/net/minecraft/world/entity/monster/Shulker.java +@@ -23,6 +23,8 @@ import net.minecraft.tags.DamageTypeTags; + import net.minecraft.util.Mth; + import net.minecraft.world.Difficulty; + import net.minecraft.world.DifficultyInstance; ++import net.minecraft.world.InteractionHand; ++import net.minecraft.world.InteractionResult; + import net.minecraft.world.damagesource.DamageSource; + import net.minecraft.world.entity.Entity; + import net.minecraft.world.entity.EntitySelector; +@@ -48,6 +50,8 @@ import net.minecraft.world.entity.player.Player; + import net.minecraft.world.entity.projectile.AbstractArrow; + import net.minecraft.world.entity.projectile.ShulkerBullet; + import net.minecraft.world.item.DyeColor; ++import net.minecraft.world.item.DyeItem; ++import net.minecraft.world.item.ItemStack; + import net.minecraft.world.level.Level; + import net.minecraft.world.level.ServerLevelAccessor; + import net.minecraft.world.level.block.Blocks; +@@ -97,12 +101,59 @@ public class Shulker extends AbstractGolem implements VariantHolder= f) { ++ if ((!this.level().purpurConfig.shulkerSpawnFromBulletRequireOpenLid || !this.isClosed()) && this.teleportSomewhere()) { ++ // Purpur start ++ float chance = this.level().purpurConfig.shulkerSpawnFromBulletBaseChance; ++ if (!this.level().purpurConfig.shulkerSpawnFromBulletNearbyEquation.isBlank()) { ++ int nearby = this.level().getEntities((EntityTypeTest) EntityType.SHULKER, axisalignedbb.inflate(this.level().purpurConfig.shulkerSpawnFromBulletNearbyRange), Entity::isAlive).size(); ++ try { ++ chance -= ((Number) scriptEngine.eval("let nearby = " + nearby + "; " + this.level().purpurConfig.shulkerSpawnFromBulletNearbyEquation)).floatValue(); ++ } catch (javax.script.ScriptException e) { ++ e.printStackTrace(); ++ chance -= (nearby - 1) / 5.0F; ++ } ++ } ++ if (this.level().random.nextFloat() <= chance) { + Shulker entityshulker = (Shulker) EntityType.SHULKER.create(this.level()); ++ // Purpur end + + if (entityshulker != null) { + entityshulker.setVariant(this.getVariant()); +@@ -592,7 +652,7 @@ public class Shulker extends AbstractGolem implements VariantHolder getVariant() { +- return Optional.ofNullable(this.getColor()); ++ return Optional.ofNullable(this.level().purpurConfig.shulkerSpawnFromBulletRandomColor ? DyeColor.random(this.level().random) : this.getColor()); // Purpur + } + + @Nullable +@@ -602,7 +662,7 @@ public class Shulker extends AbstractGolem implements VariantHolder(this, Player.class, true)); + } +@@ -167,7 +201,7 @@ public class Silverfish extends Monster { + continue; + } + // CraftBukkit end +- if (world.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING)) { ++ if (world.purpurConfig.silverfishBypassMobGriefing || world.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING)) { // Purpur + world.destroyBlock(blockposition1, true, this.silverfish); + } else { + world.setBlock(blockposition1, ((InfestedBlock) block).hostStateByInfested(world.getBlockState(blockposition1)), 3); +@@ -205,7 +239,7 @@ public class Silverfish extends Monster { + } else { + RandomSource randomsource = this.mob.getRandom(); + +- if (this.mob.level().getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING) && randomsource.nextInt(reducedTickDelay(10)) == 0) { ++ if ((this.mob.level().purpurConfig.silverfishBypassMobGriefing || this.mob.level().getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING)) && randomsource.nextInt(reducedTickDelay(10)) == 0) { // Purpur + this.selectedDirection = Direction.getRandom(randomsource); + BlockPos blockposition = BlockPos.containing(this.mob.getX(), this.mob.getY() + 0.5D, this.mob.getZ()).relative(this.selectedDirection); + BlockState iblockdata = this.mob.level().getBlockState(blockposition); +diff --git a/src/main/java/net/minecraft/world/entity/monster/Skeleton.java b/src/main/java/net/minecraft/world/entity/monster/Skeleton.java +index 5642bddc8268d70e5bb5446b65be1d8ce34feb9b..9eb6ed001bfc578311300977dda6f3f156d07190 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/Skeleton.java ++++ b/src/main/java/net/minecraft/world/entity/monster/Skeleton.java +@@ -14,6 +14,16 @@ import net.minecraft.world.item.Items; + import net.minecraft.world.level.ItemLike; + import net.minecraft.world.level.Level; + ++// Purpur start ++import net.minecraft.world.item.ItemStack; ++import net.minecraft.world.level.block.Blocks; ++import org.bukkit.craftbukkit.event.CraftEventFactory; ++import net.minecraft.world.InteractionHand; ++import net.minecraft.world.InteractionResult; ++import net.minecraft.server.level.ServerLevel; ++import net.minecraft.core.particles.ParticleTypes; ++// Purpur end ++ + public class Skeleton extends AbstractSkeleton { + + private static final int TOTAL_CONVERSION_TIME = 300; +@@ -26,6 +36,40 @@ public class Skeleton extends AbstractSkeleton { + super(type, world); + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level().purpurConfig.skeletonRidable; ++ } ++ ++ @Override ++ public boolean dismountsUnderwater() { ++ return level().purpurConfig.useDismountsUnderwaterTag ? super.dismountsUnderwater() : !level().purpurConfig.skeletonRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level().purpurConfig.skeletonControllable; ++ } ++ // Purpur end ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(net.minecraft.world.entity.ai.attributes.Attributes.MAX_HEALTH).setBaseValue(this.level().purpurConfig.skeletonMaxHealth); ++ } ++ ++ // Purpur start ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level().purpurConfig.skeletonTakeDamageFromWater; ++ } ++ // Purpur end ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level().purpurConfig.skeletonAlwaysDropExp; ++ } ++ + @Override + protected void defineSynchedData(SynchedEntityData.Builder builder) { + super.defineSynchedData(builder); +@@ -140,4 +184,67 @@ public class Skeleton extends AbstractSkeleton { + } + + } ++ ++ // Purpur start ++ private int witherRosesFed = 0; ++ ++ @Override ++ public InteractionResult mobInteract(Player player, InteractionHand hand) { ++ ItemStack stack = player.getItemInHand(hand); ++ ++ if (level().purpurConfig.skeletonFeedWitherRoses > 0 && this.getType() != EntityType.WITHER_SKELETON && stack.getItem() == Blocks.WITHER_ROSE.asItem()) { ++ return this.feedWitherRose(player, stack); ++ } ++ ++ return super.mobInteract(player, hand); ++ } ++ ++ private InteractionResult feedWitherRose(Player player, ItemStack stack) { ++ if (++witherRosesFed < level().purpurConfig.skeletonFeedWitherRoses) { ++ if (!player.getAbilities().instabuild) { ++ stack.shrink(1); ++ } ++ return InteractionResult.CONSUME; ++ } ++ ++ WitherSkeleton skeleton = EntityType.WITHER_SKELETON.create(level()); ++ if (skeleton == null) { ++ return InteractionResult.PASS; ++ } ++ ++ skeleton.moveTo(this.getX(), this.getY(), this.getZ(), this.getYRot(), this.getXRot()); ++ skeleton.setHealth(this.getHealth()); ++ skeleton.setAggressive(this.isAggressive()); ++ skeleton.copyPosition(this); ++ skeleton.setYBodyRot(this.yBodyRot); ++ skeleton.setYHeadRot(this.getYHeadRot()); ++ skeleton.yRotO = this.yRotO; ++ skeleton.xRotO = this.xRotO; ++ ++ if (this.hasCustomName()) { ++ skeleton.setCustomName(this.getCustomName()); ++ } ++ ++ if (CraftEventFactory.callEntityTransformEvent(this, skeleton, org.bukkit.event.entity.EntityTransformEvent.TransformReason.INFECTION).isCancelled()) { ++ return InteractionResult.PASS; ++ } ++ ++ if (!new com.destroystokyo.paper.event.entity.EntityTransformedEvent(this.getBukkitEntity(), skeleton.getBukkitEntity(), com.destroystokyo.paper.event.entity.EntityTransformedEvent.TransformedReason.INFECTED).callEvent()) { ++ return InteractionResult.PASS; ++ } ++ ++ this.level().addFreshEntity(skeleton); ++ this.remove(RemovalReason.DISCARDED, org.bukkit.event.entity.EntityRemoveEvent.Cause.DISCARD); ++ if (!player.getAbilities().instabuild) { ++ stack.shrink(1); ++ } ++ ++ for (int i = 0; i < 15; ++i) { ++ ((ServerLevel) level()).sendParticles(((ServerLevel) level()).players(), null, ParticleTypes.HAPPY_VILLAGER, ++ getX() + random.nextFloat(), getY() + (random.nextFloat() * 2), getZ() + random.nextFloat(), 1, ++ random.nextGaussian() * 0.05D, random.nextGaussian() * 0.05D, random.nextGaussian() * 0.05D, 0, true); ++ } ++ return InteractionResult.SUCCESS; ++ } ++ // Purpur end + } +diff --git a/src/main/java/net/minecraft/world/entity/monster/Slime.java b/src/main/java/net/minecraft/world/entity/monster/Slime.java +index f223e78eb1204bbf5f2de38a7ce5b663800f7dc4..ccf7fea215d3096e76db294daa5874fec00147ca 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/Slime.java ++++ b/src/main/java/net/minecraft/world/entity/monster/Slime.java +@@ -61,6 +61,7 @@ public class Slime extends Mob implements Enemy { + public float squish; + public float oSquish; + private boolean wasOnGround; ++ protected boolean actualJump; // Purpur + + public Slime(EntityType type, Level world) { + super(type, world); +@@ -68,12 +69,89 @@ public class Slime extends Mob implements Enemy { + this.moveControl = new Slime.SlimeMoveControl(this); + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level().purpurConfig.slimeRidable; ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level().purpurConfig.slimeTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level().purpurConfig.slimeAlwaysDropExp; ++ } ++ ++ @Override ++ public boolean dismountsUnderwater() { ++ return level().purpurConfig.useDismountsUnderwaterTag ? super.dismountsUnderwater() : !level().purpurConfig.slimeRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level().purpurConfig.slimeControllable; ++ } ++ ++ @Override ++ public float getJumpPower() { ++ float height = super.getJumpPower(); ++ return getRider() != null && this.isControllable() && actualJump ? height * 1.5F : height; ++ } ++ ++ @Override ++ public boolean onSpacebar() { ++ if (onGround && getRider() != null && this.isControllable()) { ++ actualJump = true; ++ if (getRider().getForwardMot() == 0 || getRider().getStrafeMot() == 0) { ++ jumpFromGround(); // jump() here if not moving ++ } ++ } ++ return true; // do not jump() in wasd controller, let vanilla controller handle ++ } ++ ++ protected String getMaxHealthEquation() { ++ return level().purpurConfig.slimeMaxHealth; ++ } ++ ++ protected String getAttackDamageEquation() { ++ return level().purpurConfig.slimeAttackDamage; ++ } ++ ++ protected java.util.Map getMaxHealthCache() { ++ return level().purpurConfig.slimeMaxHealthCache; ++ } ++ ++ protected java.util.Map getAttackDamageCache() { ++ return level().purpurConfig.slimeAttackDamageCache; ++ } ++ ++ protected double getFromCache(java.util.function.Supplier equation, java.util.function.Supplier> cache, java.util.function.Supplier defaultValue) { ++ int size = getSize(); ++ Double value = cache.get().get(size); ++ if (value == null) { ++ try { ++ value = ((Number) scriptEngine.eval("let size = " + size + "; " + equation.get())).doubleValue(); ++ } catch (javax.script.ScriptException e) { ++ e.printStackTrace(); ++ value = defaultValue.get(); ++ } ++ cache.get().put(size, value); ++ } ++ return value; ++ } ++ // Purpur end ++ + @Override + protected void registerGoals() { ++ this.goalSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.goalSelector.addGoal(1, new Slime.SlimeFloatGoal(this)); + this.goalSelector.addGoal(2, new Slime.SlimeAttackGoal(this)); + this.goalSelector.addGoal(3, new Slime.SlimeRandomDirectionGoal(this)); + this.goalSelector.addGoal(5, new Slime.SlimeKeepOnJumpingGoal(this)); ++ this.targetSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.targetSelector.addGoal(1, new NearestAttackableTargetGoal<>(this, Player.class, 10, true, false, (entityliving) -> { + return Math.abs(entityliving.getY() - this.getY()) <= 4.0D; + })); +@@ -98,9 +176,9 @@ public class Slime extends Mob implements Enemy { + this.entityData.set(Slime.ID_SIZE, j); + this.reapplyPosition(); + this.refreshDimensions(); +- this.getAttribute(Attributes.MAX_HEALTH).setBaseValue((double) (j * j)); ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(getFromCache(this::getMaxHealthEquation, this::getMaxHealthCache, () -> (double) size * size)); // Purpur + this.getAttribute(Attributes.MOVEMENT_SPEED).setBaseValue((double) (0.2F + 0.1F * (float) j)); +- this.getAttribute(Attributes.ATTACK_DAMAGE).setBaseValue((double) j); ++ this.getAttribute(Attributes.ATTACK_DAMAGE).setBaseValue(getFromCache(this::getAttackDamageEquation, this::getAttackDamageCache, () -> (double) j)); // Purpur + if (heal) { + this.setHealth(this.getMaxHealth()); + } +@@ -382,11 +460,12 @@ public class Slime extends Mob implements Enemy { + } + + @Override +- protected void jumpFromGround() { ++ public void jumpFromGround() { // Purpur - protected -> public + Vec3 vec3d = this.getDeltaMovement(); + + this.setDeltaMovement(vec3d.x, (double) this.getJumpPower(), vec3d.z); + this.hasImpulse = true; ++ this.actualJump = false; // Purpur + } + + @Nullable +@@ -420,7 +499,7 @@ public class Slime extends Mob implements Enemy { + return super.getDefaultDimensions(pose).scale((float) this.getSize()); + } + +- private static class SlimeMoveControl extends MoveControl { ++ private static class SlimeMoveControl extends org.purpurmc.purpur.controller.MoveControllerWASD { // Purpur + + private float yRot; + private int jumpDelay; +@@ -439,21 +518,33 @@ public class Slime extends Mob implements Enemy { + } + + public void setWantedMovement(double speed) { +- this.speedModifier = speed; ++ this.setSpeedModifier(speed); // Purpur + this.operation = MoveControl.Operation.MOVE_TO; + } + + @Override + public void tick() { ++ // Purpur start ++ if (slime.getRider() != null && slime.isControllable()) { ++ purpurTick(slime.getRider()); ++ if (slime.getForwardMot() != 0 || slime.getStrafeMot() != 0) { ++ if (jumpDelay > 10) { ++ jumpDelay = 6; ++ } ++ } else { ++ jumpDelay = 20; ++ } ++ } else { ++ // Purpur end + this.mob.setYRot(this.rotlerp(this.mob.getYRot(), this.yRot, 90.0F)); + this.mob.yHeadRot = this.mob.getYRot(); + this.mob.yBodyRot = this.mob.getYRot(); +- if (this.operation != MoveControl.Operation.MOVE_TO) { ++ } if ((slime.getRider() == null || !slime.isControllable()) && this.operation != MoveControl.Operation.MOVE_TO) { // Purpur + this.mob.setZza(0.0F); + } else { + this.operation = MoveControl.Operation.WAIT; + if (this.mob.onGround()) { +- this.mob.setSpeed((float) (this.speedModifier * this.mob.getAttributeValue(Attributes.MOVEMENT_SPEED))); ++ this.mob.setSpeed((float) (this.getSpeedModifier() * this.mob.getAttributeValue(Attributes.MOVEMENT_SPEED) * (slime.getRider() != null && slime.isControllable() && (slime.getRider().getForwardMot() != 0 || slime.getRider().getStrafeMot() != 0) ? 2.0D : 1.0D))); // Purpur + if (this.jumpDelay-- <= 0) { + this.jumpDelay = this.slime.getJumpDelay(); + if (this.isAggressive) { +@@ -470,7 +561,7 @@ public class Slime extends Mob implements Enemy { + this.mob.setSpeed(0.0F); + } + } else { +- this.mob.setSpeed((float) (this.speedModifier * this.mob.getAttributeValue(Attributes.MOVEMENT_SPEED))); ++ this.mob.setSpeed((float) (this.getSpeedModifier() * this.mob.getAttributeValue(Attributes.MOVEMENT_SPEED) * (slime.getRider() != null && slime.isControllable() && (slime.getRider().getForwardMot() != 0 || slime.getRider().getStrafeMot() != 0) ? 2.0D : 1.0D))); // Purpur + } + + } +diff --git a/src/main/java/net/minecraft/world/entity/monster/Spider.java b/src/main/java/net/minecraft/world/entity/monster/Spider.java +index fa0316e9d2a4cf213982994dc8bf310299cca984..159740069aba59bffff444d933af32aaf752ba48 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/Spider.java ++++ b/src/main/java/net/minecraft/world/entity/monster/Spider.java +@@ -51,9 +51,42 @@ public class Spider extends Monster { + super(type, world); + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level().purpurConfig.spiderRidable; ++ } ++ ++ @Override ++ public boolean dismountsUnderwater() { ++ return level().purpurConfig.useDismountsUnderwaterTag ? super.dismountsUnderwater() : !level().purpurConfig.spiderRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level().purpurConfig.spiderControllable; ++ } ++ // Purpur end ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level().purpurConfig.spiderMaxHealth); ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level().purpurConfig.spiderTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level().purpurConfig.spiderAlwaysDropExp; ++ } ++ + @Override + protected void registerGoals() { + this.goalSelector.addGoal(1, new FloatGoal(this)); ++ this.goalSelector.addGoal(1, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.goalSelector.addGoal(2, new AvoidEntityGoal<>(this, Armadillo.class, 6.0F, 1.0D, 1.2D, (entityliving) -> { + return !((Armadillo) entityliving).isScared(); + })); +@@ -62,6 +95,7 @@ public class Spider extends Monster { + this.goalSelector.addGoal(5, new WaterAvoidingRandomStrollGoal(this, 0.8D)); + this.goalSelector.addGoal(6, new LookAtPlayerGoal(this, Player.class, 8.0F)); + this.goalSelector.addGoal(6, new RandomLookAroundGoal(this)); ++ this.targetSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.targetSelector.addGoal(1, new HurtByTargetGoal(this, new Class[0])); + this.targetSelector.addGoal(2, new Spider.SpiderTargetGoal<>(this, Player.class)); + this.targetSelector.addGoal(3, new Spider.SpiderTargetGoal<>(this, IronGolem.class)); +diff --git a/src/main/java/net/minecraft/world/entity/monster/Stray.java b/src/main/java/net/minecraft/world/entity/monster/Stray.java +index 207a649d737adff440bd3f7cba15b0dbca338a35..18c0cf991c2e8418d7fdd4c8dbd7487a301e890d 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/Stray.java ++++ b/src/main/java/net/minecraft/world/entity/monster/Stray.java +@@ -21,6 +21,38 @@ public class Stray extends AbstractSkeleton { + super(type, world); + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level().purpurConfig.strayRidable; ++ } ++ ++ @Override ++ public boolean dismountsUnderwater() { ++ return level().purpurConfig.useDismountsUnderwaterTag ? super.dismountsUnderwater() : !level().purpurConfig.strayRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level().purpurConfig.strayControllable; ++ } ++ // Purpur end ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(net.minecraft.world.entity.ai.attributes.Attributes.MAX_HEALTH).setBaseValue(this.level().purpurConfig.strayMaxHealth); ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level().purpurConfig.strayTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level().purpurConfig.strayAlwaysDropExp; ++ } ++ + public static boolean checkStraySpawnRules(EntityType type, ServerLevelAccessor world, MobSpawnType spawnReason, BlockPos pos, RandomSource random) { + BlockPos blockPos = pos; + +diff --git a/src/main/java/net/minecraft/world/entity/monster/Strider.java b/src/main/java/net/minecraft/world/entity/monster/Strider.java +index fe85900a610afd0b237d8b5a164181c03afbdfc7..5ea5bf9c0e11b0e1f9fe50093899c6e35ee6cf4f 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/Strider.java ++++ b/src/main/java/net/minecraft/world/entity/monster/Strider.java +@@ -91,12 +91,44 @@ public class Strider extends Animal implements ItemSteerable, Saddleable { + super(type, world); + this.steering = new ItemBasedSteering(this.entityData, Strider.DATA_BOOST_TIME, Strider.DATA_SADDLE_ID); + this.blocksBuilding = true; +- this.setPathfindingMalus(PathType.WATER, -1.0F); ++ if (isSensitiveToWater()) this.setPathfindingMalus(PathType.WATER, -1.0F); // Purpur + this.setPathfindingMalus(PathType.LAVA, 0.0F); + this.setPathfindingMalus(PathType.DANGER_FIRE, 0.0F); + this.setPathfindingMalus(PathType.DAMAGE_FIRE, 0.0F); + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level().purpurConfig.striderRidable; ++ } ++ ++ @Override ++ public boolean dismountsUnderwater() { ++ return level().purpurConfig.useDismountsUnderwaterTag ? super.dismountsUnderwater() : !level().purpurConfig.striderRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level().purpurConfig.striderControllable; ++ } ++ // Purpur end ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level().purpurConfig.striderMaxHealth); ++ } ++ ++ @Override ++ public int getPurpurBreedTime() { ++ return this.level().purpurConfig.striderBreedingTicks; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level().purpurConfig.striderAlwaysDropExp; ++ } ++ + public static boolean checkStriderSpawnRules(EntityType type, LevelAccessor world, MobSpawnType spawnReason, BlockPos pos, RandomSource random) { + BlockPos.MutableBlockPos blockposition_mutableblockposition = pos.mutable(); + +@@ -158,6 +190,7 @@ public class Strider extends Animal implements ItemSteerable, Saddleable { + @Override + protected void registerGoals() { + this.goalSelector.addGoal(1, new PanicGoal(this, 1.65D)); ++ this.goalSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.goalSelector.addGoal(2, new BreedGoal(this, 1.0D)); + this.temptGoal = new TemptGoal(this, 1.4D, (itemstack) -> { + return itemstack.is(ItemTags.STRIDER_TEMPT_ITEMS); +@@ -414,7 +447,7 @@ public class Strider extends Animal implements ItemSteerable, Saddleable { + + @Override + public boolean isSensitiveToWater() { +- return true; ++ return this.level().purpurConfig.striderTakeDamageFromWater; // Purpur + } + + @Override +@@ -456,6 +489,19 @@ public class Strider extends Animal implements ItemSteerable, Saddleable { + public InteractionResult mobInteract(Player player, InteractionHand hand) { + boolean flag = this.isFood(player.getItemInHand(hand)); + ++ // Purpur start ++ if (level().purpurConfig.striderGiveSaddleBack && player.isSecondaryUseActive() && !flag && isSaddled() && !isVehicle()) { ++ this.steering.setSaddle(false); ++ if (!player.getAbilities().instabuild) { ++ ItemStack saddle = new ItemStack(Items.SADDLE); ++ if (!player.getInventory().add(saddle)) { ++ player.drop(saddle, false); ++ } ++ } ++ return InteractionResult.SUCCESS; ++ } ++ // Purpur end ++ + if (!flag && this.isSaddled() && !this.isVehicle() && !player.isSecondaryUseActive()) { + if (!this.level().isClientSide) { + player.startRiding(this); +@@ -468,7 +514,7 @@ public class Strider extends Animal implements ItemSteerable, Saddleable { + if (!enuminteractionresult.consumesAction()) { + ItemStack itemstack = player.getItemInHand(hand); + +- return itemstack.is(Items.SADDLE) ? itemstack.interactLivingEntity(player, this, hand) : InteractionResult.PASS; ++ return itemstack.is(Items.SADDLE) ? itemstack.interactLivingEntity(player, this, hand) : tryRide(player, hand); // Purpur + } else { + if (flag && !this.isSilent()) { + this.level().playSound((Player) null, this.getX(), this.getY(), this.getZ(), SoundEvents.STRIDER_EAT, this.getSoundSource(), 1.0F, 1.0F + (this.random.nextFloat() - this.random.nextFloat()) * 0.2F); +diff --git a/src/main/java/net/minecraft/world/entity/monster/Vex.java b/src/main/java/net/minecraft/world/entity/monster/Vex.java +index fd3b37dde54623ba38186efb2a64d364c86b81d2..691f319719280f873140df7d93c821417e32e8f7 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/Vex.java ++++ b/src/main/java/net/minecraft/world/entity/monster/Vex.java +@@ -60,6 +60,65 @@ public class Vex extends Monster implements TraceableEntity { + this.xpReward = 3; + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level().purpurConfig.vexRidable; ++ } ++ ++ @Override ++ public boolean dismountsUnderwater() { ++ return level().purpurConfig.useDismountsUnderwaterTag ? super.dismountsUnderwater() : !level().purpurConfig.vexRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level().purpurConfig.vexControllable; ++ } ++ ++ @Override ++ public double getMaxY() { ++ return level().purpurConfig.vexMaxY; ++ } ++ ++ @Override ++ public void travel(Vec3 vec3) { ++ super.travel(vec3); ++ if (getRider() != null && this.isControllable()) { ++ float speed; ++ if (onGround) { ++ speed = (float) getAttributeValue(Attributes.MOVEMENT_SPEED) * 0.1F; ++ } else { ++ speed = (float) getAttributeValue(Attributes.FLYING_SPEED); ++ } ++ setSpeed(speed); ++ Vec3 mot = getDeltaMovement(); ++ move(MoverType.SELF, mot.multiply(speed, 1.0, speed)); ++ setDeltaMovement(mot.scale(0.9D)); ++ } ++ } ++ ++ @Override ++ public boolean causeFallDamage(float fallDistance, float damageMultiplier, DamageSource damageSource) { ++ return false; // no fall damage please ++ } ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level().purpurConfig.vexMaxHealth); ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level().purpurConfig.vexTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level().purpurConfig.vexAlwaysDropExp; ++ } ++ // Purpur end ++ + @Override + public boolean isFlapping() { + return this.tickCount % Vex.TICKS_PER_FLAP == 0; +@@ -73,7 +132,7 @@ public class Vex extends Monster implements TraceableEntity { + + @Override + public void tick() { +- this.noPhysics = true; ++ this.noPhysics = getRider() == null || !this.isControllable(); // Purpur + super.tick(); + this.noPhysics = false; + this.setNoGravity(true); +@@ -88,17 +147,19 @@ public class Vex extends Monster implements TraceableEntity { + protected void registerGoals() { + super.registerGoals(); + this.goalSelector.addGoal(0, new FloatGoal(this)); ++ this.goalSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.goalSelector.addGoal(4, new Vex.VexChargeAttackGoal()); + this.goalSelector.addGoal(8, new Vex.VexRandomMoveGoal()); + this.goalSelector.addGoal(9, new LookAtPlayerGoal(this, Player.class, 3.0F, 1.0F)); + this.goalSelector.addGoal(10, new LookAtPlayerGoal(this, Mob.class, 8.0F)); ++ this.targetSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.targetSelector.addGoal(1, (new HurtByTargetGoal(this, new Class[]{Raider.class})).setAlertOthers()); + this.targetSelector.addGoal(2, new Vex.VexCopyOwnerTargetGoal(this)); + this.targetSelector.addGoal(3, new NearestAttackableTargetGoal<>(this, Player.class, true)); + } + + public static AttributeSupplier.Builder createAttributes() { +- return Monster.createMonsterAttributes().add(Attributes.MAX_HEALTH, 14.0D).add(Attributes.ATTACK_DAMAGE, 4.0D); ++ return Monster.createMonsterAttributes().add(Attributes.MAX_HEALTH, 14.0D).add(Attributes.ATTACK_DAMAGE, 4.0D).add(Attributes.FLYING_SPEED, 0.6D); // Purpur; + } + + @Override +@@ -230,14 +291,14 @@ public class Vex extends Monster implements TraceableEntity { + this.setDropChance(EquipmentSlot.MAINHAND, 0.0F); + } + +- private class VexMoveControl extends MoveControl { ++ private class VexMoveControl extends org.purpurmc.purpur.controller.FlyingMoveControllerWASD { // Purpur + + public VexMoveControl(final Vex entityvex) { + super(entityvex); + } + + @Override +- public void tick() { ++ public void vanillaTick() { // Purpur + if (this.operation == MoveControl.Operation.MOVE_TO) { + Vec3 vec3d = new Vec3(this.wantedX - Vex.this.getX(), this.wantedY - Vex.this.getY(), this.wantedZ - Vex.this.getZ()); + double d0 = vec3d.length(); +@@ -246,7 +307,7 @@ public class Vex extends Monster implements TraceableEntity { + this.operation = MoveControl.Operation.WAIT; + Vex.this.setDeltaMovement(Vex.this.getDeltaMovement().scale(0.5D)); + } else { +- Vex.this.setDeltaMovement(Vex.this.getDeltaMovement().add(vec3d.scale(this.speedModifier * 0.05D / d0))); ++ Vex.this.setDeltaMovement(Vex.this.getDeltaMovement().add(vec3d.scale(this.getSpeedModifier() * 0.05D / d0))); // Purpur + if (Vex.this.getTarget() == null) { + Vec3 vec3d1 = Vex.this.getDeltaMovement(); + +diff --git a/src/main/java/net/minecraft/world/entity/monster/Vindicator.java b/src/main/java/net/minecraft/world/entity/monster/Vindicator.java +index b3da310d6fd1d533da805c38c2f449cf06d01492..e7703aa5467e7551bff06fab4c11d76237bda2e0 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/Vindicator.java ++++ b/src/main/java/net/minecraft/world/entity/monster/Vindicator.java +@@ -50,14 +50,48 @@ public class Vindicator extends AbstractIllager { + super(type, world); + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level().purpurConfig.vindicatorRidable; ++ } ++ ++ @Override ++ public boolean dismountsUnderwater() { ++ return level().purpurConfig.useDismountsUnderwaterTag ? super.dismountsUnderwater() : !level().purpurConfig.vindicatorRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level().purpurConfig.vindicatorControllable; ++ } ++ // Purpur end ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level().purpurConfig.vindicatorMaxHealth); ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level().purpurConfig.vindicatorTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level().purpurConfig.vindicatorAlwaysDropExp; ++ } ++ + @Override + protected void registerGoals() { + super.registerGoals(); + this.goalSelector.addGoal(0, new FloatGoal(this)); ++ this.goalSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.goalSelector.addGoal(1, new Vindicator.VindicatorBreakDoorGoal(this)); + this.goalSelector.addGoal(2, new AbstractIllager.RaiderOpenDoorGoal(this)); + this.goalSelector.addGoal(3, new Raider.HoldGroundAttackGoal(this, 10.0F)); + this.goalSelector.addGoal(4, new MeleeAttackGoal(this, 1.0, false)); ++ this.targetSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.targetSelector.addGoal(1, new HurtByTargetGoal(this, Raider.class).setAlertOthers()); + this.targetSelector.addGoal(2, new NearestAttackableTargetGoal<>(this, Player.class, true)); + this.targetSelector.addGoal(3, new NearestAttackableTargetGoal<>(this, AbstractVillager.class, true)); +@@ -124,6 +158,12 @@ public class Vindicator extends AbstractIllager { + RandomSource randomSource = world.getRandom(); + this.populateDefaultEquipmentSlots(randomSource, difficulty); + this.populateDefaultEquipmentEnchantments(randomSource, difficulty); ++ // Purpur start ++ Level level = world.getMinecraftWorld(); ++ if (level().purpurConfig.vindicatorJohnnySpawnChance > 0D && random.nextDouble() <= level().purpurConfig.vindicatorJohnnySpawnChance) { ++ setCustomName(Component.translatable("Johnny")); ++ } ++ // Purpur end + return spawnGroupData; + } + +diff --git a/src/main/java/net/minecraft/world/entity/monster/Witch.java b/src/main/java/net/minecraft/world/entity/monster/Witch.java +index 5803c1d36b769f0186baa0665976749765b4cb61..b4aab57b9aaab2ed1322ca41d4bf3c60f155902c 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/Witch.java ++++ b/src/main/java/net/minecraft/world/entity/monster/Witch.java +@@ -55,6 +55,38 @@ public class Witch extends Raider implements RangedAttackMob { + super(type, world); + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level().purpurConfig.witchRidable; ++ } ++ ++ @Override ++ public boolean dismountsUnderwater() { ++ return level().purpurConfig.useDismountsUnderwaterTag ? super.dismountsUnderwater() : !level().purpurConfig.witchRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level().purpurConfig.witchControllable; ++ } ++ // Purpur end ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level().purpurConfig.witchMaxHealth); ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level().purpurConfig.witchTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level().purpurConfig.witchAlwaysDropExp; ++ } ++ + @Override + protected void registerGoals() { + super.registerGoals(); +@@ -63,10 +95,12 @@ public class Witch extends Raider implements RangedAttackMob { + }); + this.attackPlayersGoal = new NearestAttackableWitchTargetGoal<>(this, Player.class, 10, true, false, (Predicate) null); + this.goalSelector.addGoal(1, new FloatGoal(this)); ++ this.goalSelector.addGoal(1, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.goalSelector.addGoal(2, new RangedAttackGoal(this, 1.0D, 60, 10.0F)); + this.goalSelector.addGoal(2, new WaterAvoidingRandomStrollGoal(this, 1.0D)); + this.goalSelector.addGoal(3, new LookAtPlayerGoal(this, Player.class, 8.0F)); + this.goalSelector.addGoal(3, new RandomLookAroundGoal(this)); ++ this.targetSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.targetSelector.addGoal(1, new HurtByTargetGoal(this, new Class[]{Raider.class})); + this.targetSelector.addGoal(2, this.healRaidersGoal); + this.targetSelector.addGoal(3, this.attackPlayersGoal); +diff --git a/src/main/java/net/minecraft/world/entity/monster/WitherSkeleton.java b/src/main/java/net/minecraft/world/entity/monster/WitherSkeleton.java +index 3f1191795e58f31b7e2fe34ef2774df13b9a789f..8c62d39c54acf274200667ae30c517cd4416b22f 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/WitherSkeleton.java ++++ b/src/main/java/net/minecraft/world/entity/monster/WitherSkeleton.java +@@ -32,6 +32,38 @@ public class WitherSkeleton extends AbstractSkeleton { + this.setPathfindingMalus(PathType.LAVA, 8.0F); + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level().purpurConfig.witherSkeletonRidable; ++ } ++ ++ @Override ++ public boolean dismountsUnderwater() { ++ return level().purpurConfig.useDismountsUnderwaterTag ? super.dismountsUnderwater() : !level().purpurConfig.witherSkeletonRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level().purpurConfig.witherSkeletonControllable; ++ } ++ // Purpur end ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level().purpurConfig.witherSkeletonMaxHealth); ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level().purpurConfig.witherSkeletonTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level().purpurConfig.witherSkeletonAlwaysDropExp; ++ } ++ + @Override + protected void registerGoals() { + this.targetSelector.addGoal(3, new NearestAttackableTargetGoal<>(this, AbstractPiglin.class, true)); +diff --git a/src/main/java/net/minecraft/world/entity/monster/Zoglin.java b/src/main/java/net/minecraft/world/entity/monster/Zoglin.java +index cfdb2b793f11544ec5e2d1e726134089994b2b0f..f9bb83f35a41e5b076ae438af613a97b8e60c0c3 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/Zoglin.java ++++ b/src/main/java/net/minecraft/world/entity/monster/Zoglin.java +@@ -80,6 +80,38 @@ public class Zoglin extends Monster implements Enemy, HoglinBase { + this.xpReward = 5; + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level().purpurConfig.zoglinRidable; ++ } ++ ++ @Override ++ public boolean dismountsUnderwater() { ++ return level().purpurConfig.useDismountsUnderwaterTag ? super.dismountsUnderwater() : !level().purpurConfig.zoglinRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level().purpurConfig.zoglinControllable; ++ } ++ // Purpur end ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level().purpurConfig.zoglinMaxHealth); ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level().purpurConfig.zoglinTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level().purpurConfig.zoglinAlwaysDropExp; ++ } ++ + @Override + protected Brain.Provider brainProvider() { + return Brain.provider(MEMORY_TYPES, SENSOR_TYPES); +@@ -234,9 +266,10 @@ public class Zoglin extends Monster implements Enemy, HoglinBase { + + @Override + protected void customServerAiStep() { +- this.level().getProfiler().push("zoglinBrain"); ++ //this.level().getProfiler().push("zoglinBrain"); // Purpur ++ if (getRider() == null || !this.isControllable()) // Purpur - only use brain if no rider + this.getBrain().tick((ServerLevel)this.level(), this); +- this.level().getProfiler().pop(); ++ //this.level().getProfiler().pop(); // Purpur + this.updateActivity(); + } + +diff --git a/src/main/java/net/minecraft/world/entity/monster/Zombie.java b/src/main/java/net/minecraft/world/entity/monster/Zombie.java +index e42dfc62bb179be1ab01b0096c05c6549d38abbc..d48d22157a89f98c1bbabc70b0bb31187038176d 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/Zombie.java ++++ b/src/main/java/net/minecraft/world/entity/monster/Zombie.java +@@ -93,22 +93,69 @@ public class Zombie extends Monster { + private int inWaterTime; + public int conversionTime; + private int lastTick = MinecraftServer.currentTick; // CraftBukkit - add field +- private boolean shouldBurnInDay = true; // Paper - Add more Zombie API ++ // private boolean shouldBurnInDay = true; // Paper - Add more Zombie API // Purpur - implemented in LivingEntity + + public Zombie(EntityType type, Level world) { + super(type, world); + this.breakDoorGoal = new BreakDoorGoal(this, com.google.common.base.Predicates.in(world.paperConfig().entities.behavior.doorBreakingDifficulty.getOrDefault(type, world.paperConfig().entities.behavior.doorBreakingDifficulty.get(EntityType.ZOMBIE)))); // Paper - Configurable door breaking difficulty ++ this.setShouldBurnInDay(true); // Purpur + } + + public Zombie(Level world) { + this(EntityType.ZOMBIE, world); + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level().purpurConfig.zombieRidable; ++ } ++ ++ @Override ++ public boolean dismountsUnderwater() { ++ return level().purpurConfig.useDismountsUnderwaterTag ? super.dismountsUnderwater() : !level().purpurConfig.zombieRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level().purpurConfig.zombieControllable; ++ } ++ // Purpur end ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level().purpurConfig.zombieMaxHealth); ++ } ++ ++ public boolean jockeyOnlyBaby() { ++ return level().purpurConfig.zombieJockeyOnlyBaby; ++ } ++ ++ public double jockeyChance() { ++ return level().purpurConfig.zombieJockeyChance; ++ } ++ ++ public boolean jockeyTryExistingChickens() { ++ return level().purpurConfig.zombieJockeyTryExistingChickens; ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level().purpurConfig.zombieTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level().purpurConfig.zombieAlwaysDropExp; ++ } ++ + @Override + protected void registerGoals() { ++ this.goalSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + if (this.level().paperConfig().entities.behavior.zombiesTargetTurtleEggs) this.goalSelector.addGoal(4, new Zombie.ZombieAttackTurtleEggGoal(this, 1.0D, 3)); // Paper - Add zombie targets turtle egg config + this.goalSelector.addGoal(8, new LookAtPlayerGoal(this, Player.class, 8.0F)); + this.goalSelector.addGoal(8, new RandomLookAroundGoal(this)); ++ this.targetSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.addBehaviourGoals(); + } + +@@ -118,7 +165,19 @@ public class Zombie extends Monster { + this.goalSelector.addGoal(7, new WaterAvoidingRandomStrollGoal(this, 1.0D)); + this.targetSelector.addGoal(1, (new HurtByTargetGoal(this, new Class[0])).setAlertOthers(ZombifiedPiglin.class)); + this.targetSelector.addGoal(2, new NearestAttackableTargetGoal<>(this, Player.class, true)); +- if ( this.level().spigotConfig.zombieAggressiveTowardsVillager ) this.targetSelector.addGoal(3, new NearestAttackableTargetGoal<>(this, AbstractVillager.class, false)); // Spigot ++ // Purpur start ++ if ( this.level().spigotConfig.zombieAggressiveTowardsVillager ) this.targetSelector.addGoal(3, new NearestAttackableTargetGoal<>(this, AbstractVillager.class, false) { // Spigot ++ @Override ++ public boolean canUse() { ++ return (level().purpurConfig.zombieAggressiveTowardsVillagerWhenLagging || !level().getServer().server.isLagging()) && super.canUse(); ++ } ++ ++ @Override ++ public boolean canContinueToUse() { ++ return (level().purpurConfig.zombieAggressiveTowardsVillagerWhenLagging || !level().getServer().server.isLagging()) && super.canContinueToUse(); ++ } ++ }); ++ // Purpur end + this.targetSelector.addGoal(3, new NearestAttackableTargetGoal<>(this, IronGolem.class, true)); + this.targetSelector.addGoal(5, new NearestAttackableTargetGoal<>(this, Turtle.class, 10, true, false, Turtle.BABY_ON_LAND_SELECTOR)); + } +@@ -240,30 +299,7 @@ public class Zombie extends Monster { + + @Override + public void aiStep() { +- if (this.isAlive()) { +- boolean flag = this.isSunSensitive() && this.isSunBurnTick(); +- +- if (flag) { +- ItemStack itemstack = this.getItemBySlot(EquipmentSlot.HEAD); +- +- if (!itemstack.isEmpty()) { +- if (itemstack.isDamageableItem()) { +- itemstack.setDamageValue(itemstack.getDamageValue() + this.random.nextInt(2)); +- if (itemstack.getDamageValue() >= itemstack.getMaxDamage()) { +- this.broadcastBreakEvent(EquipmentSlot.HEAD); +- this.setItemSlot(EquipmentSlot.HEAD, ItemStack.EMPTY); +- } +- } +- +- flag = false; +- } +- +- if (flag) { +- this.igniteForSeconds(8); +- } +- } +- } +- ++ // Purpur - implemented in LivingEntity + super.aiStep(); + } + +@@ -301,6 +337,7 @@ public class Zombie extends Monster { + + } + ++ public boolean shouldBurnInDay() { return isSunSensitive(); } // Purpur - for ABI compatibility + public boolean isSunSensitive() { + return this.shouldBurnInDay; // Paper - Add more Zombie API + } +@@ -424,7 +461,7 @@ public class Zombie extends Monster { + nbt.putBoolean("CanBreakDoors", this.canBreakDoors()); + nbt.putInt("InWaterTime", this.isInWater() ? this.inWaterTime : -1); + nbt.putInt("DrownedConversionTime", this.isUnderWaterConverting() ? this.conversionTime : -1); +- nbt.putBoolean("Paper.ShouldBurnInDay", this.shouldBurnInDay); // Paper - Add more Zombie API ++ // nbt.putBoolean("Paper.ShouldBurnInDay", this.shouldBurnInDay); // Paper - Add more Zombie API // Purpur - implemented in LivingEntity + } + + @Override +@@ -438,7 +475,7 @@ public class Zombie extends Monster { + } + // Paper start - Add more Zombie API + if (nbt.contains("Paper.ShouldBurnInDay")) { +- this.shouldBurnInDay = nbt.getBoolean("Paper.ShouldBurnInDay"); ++ // this.shouldBurnInDay = nbt.getBoolean("Paper.ShouldBurnInDay"); // Purpur - implemented in LivingEntity + } + // Paper end - Add more Zombie API + +@@ -509,19 +546,20 @@ public class Zombie extends Monster { + } + + if (object instanceof Zombie.ZombieGroupData entityzombie_groupdatazombie) { +- if (entityzombie_groupdatazombie.isBaby) { +- this.setBaby(true); ++ // Purpur start ++ if (!jockeyOnlyBaby() || entityzombie_groupdatazombie.isBaby) { ++ this.setBaby(entityzombie_groupdatazombie.isBaby); + if (entityzombie_groupdatazombie.canSpawnJockey) { +- if ((double) randomsource.nextFloat() < 0.05D) { +- List list = world.getEntitiesOfClass(Chicken.class, this.getBoundingBox().inflate(5.0D, 3.0D, 5.0D), EntitySelector.ENTITY_NOT_BEING_RIDDEN); ++ if ((double) randomsource.nextFloat() < jockeyChance()) { ++ List list = jockeyTryExistingChickens() ? world.getEntitiesOfClass(Chicken.class, this.getBoundingBox().inflate(5.0D, 3.0D, 5.0D), EntitySelector.ENTITY_NOT_BEING_RIDDEN) : java.util.Collections.emptyList(); ++ // Purpur end + + if (!list.isEmpty()) { + Chicken entitychicken = (Chicken) list.get(0); + + entitychicken.setChickenJockey(true); + this.startRiding(entitychicken); +- } +- } else if ((double) randomsource.nextFloat() < 0.05D) { ++ } else { // Purpur + Chicken entitychicken1 = (Chicken) EntityType.CHICKEN.create(this.level()); + + if (entitychicken1 != null) { +@@ -531,6 +569,7 @@ public class Zombie extends Monster { + this.startRiding(entitychicken1); + world.addFreshEntity(entitychicken1, CreatureSpawnEvent.SpawnReason.MOUNT); // CraftBukkit + } ++ } // Purpur + } + } + } +@@ -541,11 +580,7 @@ public class Zombie extends Monster { + } + + if (this.getItemBySlot(EquipmentSlot.HEAD).isEmpty()) { +- LocalDate localdate = LocalDate.now(); +- int i = localdate.get(ChronoField.DAY_OF_MONTH); +- int j = localdate.get(ChronoField.MONTH_OF_YEAR); +- +- if (j == 10 && i == 31 && randomsource.nextFloat() < 0.25F) { ++ if (net.minecraft.world.entity.ambient.Bat.isHalloweenSeason(world.getMinecraftWorld()) && this.random.nextFloat() < this.level().purpurConfig.chanceHeadHalloweenOnEntity) { // Purpur + this.setItemSlot(EquipmentSlot.HEAD, new ItemStack(randomsource.nextFloat() < 0.1F ? Blocks.JACK_O_LANTERN : Blocks.CARVED_PUMPKIN)); + this.armorDropChances[EquipmentSlot.HEAD.getIndex()] = 0.0F; + } +@@ -577,7 +612,7 @@ public class Zombie extends Monster { + } + + protected void randomizeReinforcementsChance() { +- this.getAttribute(Attributes.SPAWN_REINFORCEMENTS_CHANCE).setBaseValue(this.random.nextDouble() * 0.10000000149011612D); ++ this.getAttribute(Attributes.SPAWN_REINFORCEMENTS_CHANCE).setBaseValue(this.random.nextDouble() * this.level().purpurConfig.zombieSpawnReinforcements); // Purpur + } + + @Override +diff --git a/src/main/java/net/minecraft/world/entity/monster/ZombieVillager.java b/src/main/java/net/minecraft/world/entity/monster/ZombieVillager.java +index f38acc96f71298e40ce9433e7759fd223ca55e48..091095d1690bdd4d0870910b19e5e4ee3a3f9e7c 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/ZombieVillager.java ++++ b/src/main/java/net/minecraft/world/entity/monster/ZombieVillager.java +@@ -80,6 +80,58 @@ public class ZombieVillager extends Zombie implements VillagerDataHolder { + }); + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level().purpurConfig.zombieVillagerRidable; ++ } ++ ++ @Override ++ public boolean dismountsUnderwater() { ++ return level().purpurConfig.useDismountsUnderwaterTag ? super.dismountsUnderwater() : !level().purpurConfig.zombieVillagerRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level().purpurConfig.zombieVillagerControllable; ++ } ++ // Purpur end ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(net.minecraft.world.entity.ai.attributes.Attributes.MAX_HEALTH).setBaseValue(this.level().purpurConfig.zombieVillagerMaxHealth); ++ } ++ ++ @Override ++ protected void randomizeReinforcementsChance() { ++ this.getAttribute(net.minecraft.world.entity.ai.attributes.Attributes.SPAWN_REINFORCEMENTS_CHANCE).setBaseValue(this.random.nextDouble() * this.level().purpurConfig.zombieVillagerSpawnReinforcements); ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level().purpurConfig.zombieVillagerTakeDamageFromWater; ++ } ++ ++ @Override ++ public boolean jockeyOnlyBaby() { ++ return level().purpurConfig.zombieVillagerJockeyOnlyBaby; ++ } ++ ++ @Override ++ public double jockeyChance() { ++ return level().purpurConfig.zombieVillagerJockeyChance; ++ } ++ ++ @Override ++ public boolean jockeyTryExistingChickens() { ++ return level().purpurConfig.zombieVillagerJockeyTryExistingChickens; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level().purpurConfig.zombieVillagerAlwaysDropExp; ++ } ++ + @Override + protected void defineSynchedData(SynchedEntityData.Builder builder) { + super.defineSynchedData(builder); +@@ -172,10 +224,10 @@ public class ZombieVillager extends Zombie implements VillagerDataHolder { + ItemStack itemstack = player.getItemInHand(hand); + + if (itemstack.is(Items.GOLDEN_APPLE)) { +- if (this.hasEffect(MobEffects.WEAKNESS)) { ++ if (this.hasEffect(MobEffects.WEAKNESS) && level().purpurConfig.zombieVillagerCureEnabled) { // Purpur + itemstack.consume(1, player); + if (!this.level().isClientSide) { +- this.startConverting(player.getUUID(), this.random.nextInt(2401) + 3600); ++ this.startConverting(player.getUUID(), this.random.nextInt(level().purpurConfig.zombieVillagerCuringTimeMax - level().purpurConfig.zombieVillagerCuringTimeMin + 1) + level().purpurConfig.zombieVillagerCuringTimeMin); // Purpur + } + + return InteractionResult.SUCCESS; +diff --git a/src/main/java/net/minecraft/world/entity/monster/ZombifiedPiglin.java b/src/main/java/net/minecraft/world/entity/monster/ZombifiedPiglin.java +index a6def4133f06c41be287e9942643e80a7b8e8218..5bae3215ee0bf222c3bd77b3131f3d01ac6c9c41 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/ZombifiedPiglin.java ++++ b/src/main/java/net/minecraft/world/entity/monster/ZombifiedPiglin.java +@@ -62,6 +62,53 @@ public class ZombifiedPiglin extends Zombie implements NeutralMob { + this.setPathfindingMalus(PathType.LAVA, 8.0F); + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level().purpurConfig.zombifiedPiglinRidable; ++ } ++ ++ @Override ++ public boolean dismountsUnderwater() { ++ return level().purpurConfig.useDismountsUnderwaterTag ? super.dismountsUnderwater() : !level().purpurConfig.zombifiedPiglinRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level().purpurConfig.zombifiedPiglinControllable; ++ } ++ // Purpur end ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level().purpurConfig.zombifiedPiglinMaxHealth); ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level().purpurConfig.zombifiedPiglinTakeDamageFromWater; ++ } ++ ++ @Override ++ public boolean jockeyOnlyBaby() { ++ return level().purpurConfig.zombifiedPiglinJockeyOnlyBaby; ++ } ++ ++ @Override ++ public double jockeyChance() { ++ return level().purpurConfig.zombifiedPiglinJockeyChance; ++ } ++ ++ @Override ++ public boolean jockeyTryExistingChickens() { ++ return level().purpurConfig.zombifiedPiglinJockeyTryExistingChickens; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level().purpurConfig.zombifiedPiglinAlwaysDropExp; ++ } ++ + @Override + public void setPersistentAngerTarget(@Nullable UUID angryAt) { + this.persistentAngerTarget = angryAt; +@@ -109,7 +156,7 @@ public class ZombifiedPiglin extends Zombie implements NeutralMob { + this.maybeAlertOthers(); + } + +- if (this.isAngry()) { ++ if (this.isAngry() && this.level().purpurConfig.zombifiedPiglinCountAsPlayerKillWhenAngry) { // Purpur + this.lastHurtByPlayerTime = this.tickCount; + } + +@@ -164,7 +211,7 @@ public class ZombifiedPiglin extends Zombie implements NeutralMob { + this.ticksUntilNextAlert = ZombifiedPiglin.ALERT_INTERVAL.sample(this.random); + } + +- if (entityliving instanceof Player) { ++ if (entityliving instanceof Player && this.level().purpurConfig.zombifiedPiglinCountAsPlayerKillWhenAngry) { // Purpur + this.setLastHurtByPlayer((Player) entityliving); + } + +@@ -244,7 +291,7 @@ public class ZombifiedPiglin extends Zombie implements NeutralMob { + + @Override + protected void randomizeReinforcementsChance() { +- this.getAttribute(Attributes.SPAWN_REINFORCEMENTS_CHANCE).setBaseValue(0.0D); ++ this.getAttribute(Attributes.SPAWN_REINFORCEMENTS_CHANCE).setBaseValue(this.random.nextDouble() * this.level().purpurConfig.zombifiedPiglinSpawnReinforcements); // Purpur + } + + @Nullable +diff --git a/src/main/java/net/minecraft/world/entity/monster/breeze/Breeze.java b/src/main/java/net/minecraft/world/entity/monster/breeze/Breeze.java +index 796ce24185ab9e80864116f9523c4289fcaad243..82391c84789c27353212d3142c036cc5aedb98f9 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/breeze/Breeze.java ++++ b/src/main/java/net/minecraft/world/entity/monster/breeze/Breeze.java +@@ -226,11 +226,11 @@ public class Breeze extends Monster { + + @Override + protected void customServerAiStep() { +- this.level().getProfiler().push("breezeBrain"); ++ //this.level().getProfiler().push("breezeBrain"); // Purpur + this.getBrain().tick((ServerLevel)this.level(), this); +- this.level().getProfiler().popPush("breezeActivityUpdate"); ++ //this.level().getProfiler().popPush("breezeActivityUpdate"); // Purpur + BreezeAi.updateActivity(this); +- this.level().getProfiler().pop(); ++ //this.level().getProfiler().pop(); // Purpur + super.customServerAiStep(); + } + +diff --git a/src/main/java/net/minecraft/world/entity/monster/hoglin/Hoglin.java b/src/main/java/net/minecraft/world/entity/monster/hoglin/Hoglin.java +index c583d883118ded5e1884c757427dc5e73c10dd27..757d2b7bcb83f5bdcddf85a00e90288f3b82a2d6 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/hoglin/Hoglin.java ++++ b/src/main/java/net/minecraft/world/entity/monster/hoglin/Hoglin.java +@@ -90,6 +90,43 @@ public class Hoglin extends Animal implements Enemy, HoglinBase { + this.xpReward = 5; + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level().purpurConfig.hoglinRidable; ++ } ++ ++ @Override ++ public boolean dismountsUnderwater() { ++ return level().purpurConfig.useDismountsUnderwaterTag ? super.dismountsUnderwater() : !level().purpurConfig.hoglinRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level().purpurConfig.hoglinControllable; ++ } ++ // Purpur end ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level().purpurConfig.hoglinMaxHealth); ++ } ++ ++ @Override ++ public int getPurpurBreedTime() { ++ return this.level().purpurConfig.hoglinBreedingTicks; ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level().purpurConfig.hoglinTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level().purpurConfig.hoglinAlwaysDropExp; ++ } ++ + @Override + public boolean canBeLeashed(Player player) { + return !this.isLeashed(); +@@ -155,9 +192,10 @@ public class Hoglin extends Animal implements Enemy, HoglinBase { + + @Override + protected void customServerAiStep() { +- this.level().getProfiler().push("hoglinBrain"); ++ //this.level().getProfiler().push("hoglinBrain"); // Purpur ++ //if ((getRider() == null || !this.isControllable()) && this.behaviorTick++ % this.activatedPriority == 0) // Pufferfish // Purpur - only use brain if no rider // Purpur - TODO: Pufferfish + this.getBrain().tick((ServerLevel)this.level(), this); +- this.level().getProfiler().pop(); ++ //this.level().getProfiler().pop(); // Purpur + HoglinAi.updateActivity(this); + if (this.isConverting()) { + this.timeInOverworld++; +diff --git a/src/main/java/net/minecraft/world/entity/monster/piglin/Piglin.java b/src/main/java/net/minecraft/world/entity/monster/piglin/Piglin.java +index a8ab486c7e11ec137da48174af6f1030dfd48056..1b5977aa14d9a7254e7692bb152cc2808d52107a 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/piglin/Piglin.java ++++ b/src/main/java/net/minecraft/world/entity/monster/piglin/Piglin.java +@@ -94,6 +94,38 @@ public class Piglin extends AbstractPiglin implements CrossbowAttackMob, Invento + this.xpReward = 5; + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level().purpurConfig.piglinRidable; ++ } ++ ++ @Override ++ public boolean dismountsUnderwater() { ++ return level().purpurConfig.useDismountsUnderwaterTag ? super.dismountsUnderwater() : !level().purpurConfig.piglinRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level().purpurConfig.piglinControllable; ++ } ++ // Purpur end ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level().purpurConfig.piglinMaxHealth); ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level().purpurConfig.piglinTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level().purpurConfig.piglinAlwaysDropExp; ++ } ++ + @Override + public void addAdditionalSaveData(CompoundTag nbt) { + super.addAdditionalSaveData(nbt); +@@ -296,9 +328,10 @@ public class Piglin extends AbstractPiglin implements CrossbowAttackMob, Invento + + @Override + protected void customServerAiStep() { +- this.level().getProfiler().push("piglinBrain"); ++ //this.level().getProfiler().push("piglinBrain"); // Purpur ++ //if ((getRider() == null || !this.isControllable()) && this.behaviorTick++ % this.activatedPriority == 0) // Pufferfish // Purpur - only use brain if no rider // Purpur - TODO: Pufferfish + this.getBrain().tick((ServerLevel) this.level(), this); +- this.level().getProfiler().pop(); ++ //this.level().getProfiler().pop(); // Purpur + PiglinAi.updateActivity(this); + super.customServerAiStep(); + } +@@ -389,7 +422,7 @@ public class Piglin extends AbstractPiglin implements CrossbowAttackMob, Invento + + @Override + public boolean wantsToPickUp(ItemStack stack) { +- return this.level().getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING) && this.canPickUpLoot() && PiglinAi.wantsToPickup(this, stack); ++ return (this.level().purpurConfig.piglinBypassMobGriefing || this.level().getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING)) && this.canPickUpLoot() && PiglinAi.wantsToPickup(this, stack); + } + + protected boolean canReplaceCurrentItem(ItemStack stack) { +diff --git a/src/main/java/net/minecraft/world/entity/monster/piglin/PiglinAi.java b/src/main/java/net/minecraft/world/entity/monster/piglin/PiglinAi.java +index e25af9af8f87e6762716749c367658bf6bda9e34..b7d5c0b0e3741fcf04c4bac21a82fc41e2eeed5d 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/piglin/PiglinAi.java ++++ b/src/main/java/net/minecraft/world/entity/monster/piglin/PiglinAi.java +@@ -606,11 +606,18 @@ public class PiglinAi { + ItemStack itemstack = (ItemStack) iterator.next(); + + item = itemstack.getItem(); +- } while (!(item instanceof ArmorItem) || !((ArmorItem) item).getMaterial().is(ArmorMaterials.GOLD)); ++ } while (!(item instanceof ArmorItem) || !((ArmorItem) item).getMaterial().is(ArmorMaterials.GOLD) && (!entity.level().purpurConfig.piglinIgnoresArmorWithGoldTrim || !isWearingGoldTrim(item))); // Purpur + + return true; + } + ++ // Purpur start ++ private static boolean isWearingGoldTrim(Item itemstack) { ++ net.minecraft.world.item.armortrim.ArmorTrim armorTrim = itemstack.components().get(net.minecraft.core.component.DataComponents.TRIM); ++ return armorTrim != null && armorTrim.material().is(net.minecraft.world.item.armortrim.TrimMaterials.GOLD); ++ } ++ // Purpur end ++ + private static void stopWalking(Piglin piglin) { + piglin.getBrain().eraseMemory(MemoryModuleType.WALK_TARGET); + piglin.getNavigation().stop(); +diff --git a/src/main/java/net/minecraft/world/entity/monster/piglin/PiglinBrute.java b/src/main/java/net/minecraft/world/entity/monster/piglin/PiglinBrute.java +index fcadd7f28ccb81bbb36e97d8b8d8a8ba3f3d6a16..407a0f27719d3944b3a005c664d80246ea1c7cf4 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/piglin/PiglinBrute.java ++++ b/src/main/java/net/minecraft/world/entity/monster/piglin/PiglinBrute.java +@@ -62,6 +62,38 @@ public class PiglinBrute extends AbstractPiglin { + this.xpReward = 20; + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level().purpurConfig.piglinBruteRidable; ++ } ++ ++ @Override ++ public boolean dismountsUnderwater() { ++ return level().purpurConfig.useDismountsUnderwaterTag ? super.dismountsUnderwater() : !level().purpurConfig.piglinBruteRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level().purpurConfig.piglinBruteControllable; ++ } ++ // Purpur end ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level().purpurConfig.piglinBruteMaxHealth); ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level().purpurConfig.piglinBruteTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level().purpurConfig.piglinBruteAlwaysDropExp; ++ } ++ + public static AttributeSupplier.Builder createAttributes() { + return Monster.createMonsterAttributes().add(Attributes.MAX_HEALTH, 50.0).add(Attributes.MOVEMENT_SPEED, 0.35F).add(Attributes.ATTACK_DAMAGE, 7.0); + } +@@ -106,9 +138,10 @@ public class PiglinBrute extends AbstractPiglin { + + @Override + protected void customServerAiStep() { +- this.level().getProfiler().push("piglinBruteBrain"); ++ //this.level().getProfiler().push("piglinBruteBrain"); // Purpur ++ if (getRider() == null || this.isControllable()) // Purpur - only use brain if no rider + this.getBrain().tick((ServerLevel)this.level(), this); +- this.level().getProfiler().pop(); ++ //this.level().getProfiler().pop(); // Purpur + PiglinBruteAi.updateActivity(this); + PiglinBruteAi.maybePlayActivitySound(this); + super.customServerAiStep(); +diff --git a/src/main/java/net/minecraft/world/entity/monster/warden/Warden.java b/src/main/java/net/minecraft/world/entity/monster/warden/Warden.java +index ddd60be52dce5773c80934be5aa5705db239e3dd..0bb577ec9ba0d23a741ccf067ac35f6be68312ca 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/warden/Warden.java ++++ b/src/main/java/net/minecraft/world/entity/monster/warden/Warden.java +@@ -123,8 +123,32 @@ public class Warden extends Monster implements VibrationSystem { + this.setPathfindingMalus(PathType.LAVA, 8.0F); + this.setPathfindingMalus(PathType.DAMAGE_FIRE, 0.0F); + this.setPathfindingMalus(PathType.DANGER_FIRE, 0.0F); ++ this.moveControl = new org.purpurmc.purpur.controller.MoveControllerWASD(this, 0.5F); // Purpur + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level().purpurConfig.wardenRidable; ++ } ++ ++ @Override ++ public boolean dismountsUnderwater() { ++ return level().purpurConfig.useDismountsUnderwaterTag ? super.dismountsUnderwater() : !level().purpurConfig.wardenRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level().purpurConfig.wardenControllable; ++ } ++ ++ @Override ++ protected void registerGoals() { ++ this.goalSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur ++ this.targetSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur ++ } ++ // Purpur end ++ + @Override + public Packet getAddEntityPacket() { + return new ClientboundAddEntityPacket(this, this.hasPose(Pose.EMERGING) ? 1 : 0); +@@ -275,9 +299,10 @@ public class Warden extends Monster implements VibrationSystem { + protected void customServerAiStep() { + ServerLevel worldserver = (ServerLevel) this.level(); + +- worldserver.getProfiler().push("wardenBrain"); ++ //worldserver.getProfiler().push("wardenBrain"); // Purpur ++ //if (this.behaviorTick++ % this.activatedPriority == 0) // Pufferfish // Purpur - TODO: Move to Ridables patch + this.getBrain().tick(worldserver, this); +- this.level().getProfiler().pop(); ++ //this.level().getProfiler().pop(); // Purpur + super.customServerAiStep(); + if ((this.tickCount + this.getId()) % 120 == 0) { + Warden.applyDarknessAround(worldserver, this.position(), this, 20); +@@ -392,17 +417,14 @@ public class Warden extends Monster implements VibrationSystem { + + @Contract("null->false") + public boolean canTargetEntity(@Nullable Entity entity) { +- boolean flag; +- ++ if (getRider() != null && isControllable()) return false; // Purpur + if (entity instanceof LivingEntity entityliving) { + if (this.level() == entity.level() && EntitySelector.NO_CREATIVE_OR_SPECTATOR.test(entity) && !this.isAlliedTo(entity) && entityliving.getType() != EntityType.ARMOR_STAND && entityliving.getType() != EntityType.WARDEN && !entityliving.isInvulnerable() && !entityliving.isDeadOrDying() && this.level().getWorldBorder().isWithinBounds(entityliving.getBoundingBox())) { +- flag = true; +- return flag; ++ return true; // Purpur - wtf + } + } + +- flag = false; +- return flag; ++ return false; // Purpur - wtf + } + + public static void applyDarknessAround(ServerLevel world, Vec3 pos, @Nullable Entity entity, int range) { +diff --git a/src/main/java/net/minecraft/world/entity/npc/AbstractVillager.java b/src/main/java/net/minecraft/world/entity/npc/AbstractVillager.java +index d323cf157f2a910916baa9ce3f7e5bc81648c47d..6cbbca1db5362fa2dd5c5704c7fbaa612d3cbab1 100644 +--- a/src/main/java/net/minecraft/world/entity/npc/AbstractVillager.java ++++ b/src/main/java/net/minecraft/world/entity/npc/AbstractVillager.java +@@ -48,6 +48,7 @@ import org.bukkit.event.entity.VillagerAcquireTradeEvent; + // CraftBukkit end + + public abstract class AbstractVillager extends AgeableMob implements InventoryCarrier, Npc, Merchant { ++ static final net.minecraft.world.item.crafting.Ingredient TEMPT_ITEMS = net.minecraft.world.item.crafting.Ingredient.of(net.minecraft.world.level.block.Blocks.EMERALD_BLOCK.asItem()); // Purpur + + // CraftBukkit start + private CraftMerchant craftMerchant; +diff --git a/src/main/java/net/minecraft/world/entity/npc/CatSpawner.java b/src/main/java/net/minecraft/world/entity/npc/CatSpawner.java +index e0e5046c84941a8d17e18c177f3daea9cb631940..d503d7a5837dbeb98e58dbe8f7e5de45f6d88990 100644 +--- a/src/main/java/net/minecraft/world/entity/npc/CatSpawner.java ++++ b/src/main/java/net/minecraft/world/entity/npc/CatSpawner.java +@@ -27,7 +27,7 @@ public class CatSpawner implements CustomSpawner { + if (this.nextTick > 0) { + return 0; + } else { +- this.nextTick = 1200; ++ this.nextTick = world.purpurConfig.catSpawnDelay; // Purpur + Player player = world.getRandomPlayer(); + if (player == null) { + return 0; +@@ -61,8 +61,12 @@ public class CatSpawner implements CustomSpawner { + + private int spawnInVillage(ServerLevel world, BlockPos pos) { + int i = 48; +- if (world.getPoiManager().getCountInRange(entry -> entry.is(PoiTypes.HOME), pos, 48, PoiManager.Occupancy.IS_OCCUPIED) > 4L) { +- List list = world.getEntitiesOfClass(Cat.class, new AABB(pos).inflate(48.0, 8.0, 48.0)); ++ // Purpur start ++ int range = world.purpurConfig.catSpawnVillageScanRange; ++ if (range <= 0) return 0; ++ if (world.getPoiManager().getCountInRange(entry -> entry.is(PoiTypes.HOME), pos, range, PoiManager.Occupancy.IS_OCCUPIED) > 4L) { ++ List list = world.getEntitiesOfClass(Cat.class, new AABB(pos).inflate(range, 8.0, range)); ++ // Purpur end + if (list.size() < 5) { + return this.spawnCat(pos, world); + } +@@ -73,7 +77,11 @@ public class CatSpawner implements CustomSpawner { + + private int spawnInHut(ServerLevel world, BlockPos pos) { + int i = 16; +- List list = world.getEntitiesOfClass(Cat.class, new AABB(pos).inflate(16.0, 8.0, 16.0)); ++ // Purpur start ++ int range = world.purpurConfig.catSpawnSwampHutScanRange; ++ if (range <= 0) return 0; ++ List list = world.getEntitiesOfClass(Cat.class, new AABB(pos).inflate(range, 8.0, range)); ++ // Purpur end + return list.size() < 1 ? this.spawnCat(pos, world) : 0; + } + +diff --git a/src/main/java/net/minecraft/world/entity/npc/Villager.java b/src/main/java/net/minecraft/world/entity/npc/Villager.java +index a7930f9875aa4aca997caaead46ecdc21e5e11d7..4be218129188c1be8736940170a861adc10fdb7d 100644 +--- a/src/main/java/net/minecraft/world/entity/npc/Villager.java ++++ b/src/main/java/net/minecraft/world/entity/npc/Villager.java +@@ -142,6 +142,8 @@ public class Villager extends AbstractVillager implements ReputationEventHandler + }, MemoryModuleType.MEETING_POINT, (entityvillager, holder) -> { + return holder.is(PoiTypes.MEETING); + }); ++ private boolean isLobotomized = false; public boolean isLobotomized() { return this.isLobotomized; } // Purpur ++ private int notLobotomizedCount = 0; // Purpur + + public Villager(EntityType entityType, Level world) { + this(entityType, world, VillagerType.PLAINS); +@@ -154,6 +156,91 @@ public class Villager extends AbstractVillager implements ReputationEventHandler + this.getNavigation().setCanFloat(true); + this.setCanPickUpLoot(true); + this.setVillagerData(this.getVillagerData().setType(type).setProfession(VillagerProfession.NONE)); ++ if (level().purpurConfig.villagerFollowEmeraldBlock) this.goalSelector.addGoal(3, new net.minecraft.world.entity.ai.goal.TemptGoal(this, 1.0D, TEMPT_ITEMS, false)); ++ } ++ ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level().purpurConfig.villagerRidable; ++ } ++ ++ @Override ++ public boolean dismountsUnderwater() { ++ return level().purpurConfig.useDismountsUnderwaterTag ? super.dismountsUnderwater() : !level().purpurConfig.villagerRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level().purpurConfig.villagerControllable; ++ } ++ ++ @Override ++ protected void registerGoals() { ++ this.goalSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); ++ } ++ // Purpur end ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level().purpurConfig.villagerMaxHealth); ++ } ++ ++ @Override ++ public boolean canBeLeashed(Player player) { ++ return level().purpurConfig.villagerCanBeLeashed && !this.isLeashed(); ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level().purpurConfig.villagerTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level().purpurConfig.villagerAlwaysDropExp; ++ } ++ ++ private boolean checkLobotomized() { ++ int interval = this.level().purpurConfig.villagerLobotomizeCheckInterval; ++ boolean shouldCheckForTradeLocked = this.level().purpurConfig.villagerLobotomizeWaitUntilTradeLocked; ++ if (this.notLobotomizedCount > 3) { ++ // check half as often if not lobotomized for the last 3+ consecutive checks ++ interval *= 2; ++ } ++ if (this.level().getGameTime() % interval == 0) { ++ // offset Y for short blocks like dirt_path/farmland ++ this.isLobotomized = !(shouldCheckForTradeLocked && this.getVillagerXp() == 0) && !canTravelFrom(BlockPos.containing(this.position().x, this.getBoundingBox().minY + 0.0625D, this.position().z)); ++ ++ if (this.isLobotomized) { ++ this.notLobotomizedCount = 0; ++ } else { ++ this.notLobotomizedCount++; ++ } ++ } ++ return this.isLobotomized; ++ } ++ ++ private boolean canTravelFrom(BlockPos pos) { ++ return canTravelTo(pos.east()) || canTravelTo(pos.west()) || canTravelTo(pos.north()) || canTravelTo(pos.south()); ++ } ++ ++ private boolean canTravelTo(BlockPos pos) { ++ net.minecraft.world.level.block.state.BlockState state = this.level().getBlockStateIfLoaded(pos); ++ if (state == null) { ++ // chunk not loaded ++ return false; ++ } ++ net.minecraft.world.level.block.Block bottom = state.getBlock(); ++ if (bottom instanceof net.minecraft.world.level.block.FenceBlock || ++ bottom instanceof net.minecraft.world.level.block.FenceGateBlock || ++ bottom instanceof net.minecraft.world.level.block.WallBlock) { ++ // bottom block is too tall to get over ++ return false; ++ } ++ net.minecraft.world.level.block.Block top = level().getBlockState(pos.above()).getBlock(); ++ // only if both blocks have no collision ++ return !bottom.hasCollision && !top.hasCollision; + } + + @Override +@@ -190,7 +277,7 @@ public class Villager extends AbstractVillager implements ReputationEventHandler + brain.addActivity(Activity.PLAY, VillagerGoalPackages.getPlayPackage(0.5F)); + } else { + brain.setSchedule(Schedule.VILLAGER_DEFAULT); +- brain.addActivityWithConditions(Activity.WORK, VillagerGoalPackages.getWorkPackage(villagerprofession, 0.5F), ImmutableSet.of(Pair.of(MemoryModuleType.JOB_SITE, MemoryStatus.VALUE_PRESENT))); ++ brain.addActivityWithConditions(Activity.WORK, VillagerGoalPackages.getWorkPackage(villagerprofession, 0.5F, this.level().purpurConfig.villagerClericsFarmWarts), ImmutableSet.of(Pair.of(MemoryModuleType.JOB_SITE, MemoryStatus.VALUE_PRESENT))); // Purpur + } + + brain.addActivity(Activity.CORE, VillagerGoalPackages.getCorePackage(villagerprofession, 0.5F)); +@@ -252,11 +339,25 @@ public class Villager extends AbstractVillager implements ReputationEventHandler + // Paper start + this.customServerAiStep(false); + } +- protected void customServerAiStep(final boolean inactive) { ++ protected void customServerAiStep(boolean inactive) { // Purpur - not final + // Paper end +- this.level().getProfiler().push("villagerBrain"); ++ //this.level().getProfiler().push("villagerBrain"); // Purpur ++ // Purpur start ++ if (this.level().purpurConfig.villagerLobotomizeEnabled) { ++ // treat as inactive if lobotomized ++ inactive = inactive || checkLobotomized(); ++ } else { ++ this.isLobotomized = false; ++ } ++ // Purpur end + if (!inactive) this.getBrain().tick((ServerLevel) this.level(), this); // Paper +- this.level().getProfiler().pop(); ++ else if (this.isLobotomized && shouldRestock()) restock(); // Purpur ++ /*// Purpur start // Purpur - TODO: Pufferfish ++ if (!inactive && (getRider() == null || !this.isControllable()) && this.behaviorTick++ % this.activatedPriority == 0) { // Purpur - only use brain if no rider ++ this.getBrain().tick((ServerLevel) this.level(), this); // Paper ++ } ++ // Purpur end*/ // Purpur - TODO: Pufferfish ++ //this.level().getProfiler().pop(); // Purpur + if (this.assignProfessionWhenSpawned) { + this.assignProfessionWhenSpawned = false; + } +@@ -312,7 +413,7 @@ public class Villager extends AbstractVillager implements ReputationEventHandler + if (!itemstack.is(Items.VILLAGER_SPAWN_EGG) && this.isAlive() && !this.isTrading() && !this.isSleeping()) { + if (this.isBaby()) { + this.setUnhappy(); +- return InteractionResult.sidedSuccess(this.level().isClientSide); ++ return tryRide(player, hand, InteractionResult.sidedSuccess(this.level().isClientSide)); // Purpur + } else { + boolean flag = this.getOffers().isEmpty(); + +@@ -325,9 +426,10 @@ public class Villager extends AbstractVillager implements ReputationEventHandler + } + + if (flag) { +- return InteractionResult.sidedSuccess(this.level().isClientSide); ++ return tryRide(player, hand, InteractionResult.sidedSuccess(this.level().isClientSide)); // Purpur + } else { +- if (!this.level().isClientSide && !this.offers.isEmpty()) { ++ if (level().purpurConfig.villagerRidable && itemstack.isEmpty()) return tryRide(player, hand); // Purpur ++ if (this.level().purpurConfig.villagerAllowTrading && !this.offers.isEmpty()) { + this.startTrading(player); + } + +@@ -496,7 +598,7 @@ public class Villager extends AbstractVillager implements ReputationEventHandler + while (iterator.hasNext()) { + MerchantOffer merchantrecipe = (MerchantOffer) iterator.next(); + +- merchantrecipe.updateDemand(); ++ merchantrecipe.updateDemand(this.level().purpurConfig.villagerMinimumDemand); // Purpur + } + + } +@@ -738,7 +840,7 @@ public class Villager extends AbstractVillager implements ReputationEventHandler + + @Override + public boolean canBreed() { +- return this.foodLevel + this.countFoodPointsInInventory() >= 12 && !this.isSleeping() && this.getAge() == 0; ++ return this.level().purpurConfig.villagerCanBreed && this.foodLevel + this.countFoodPointsInInventory() >= 12 && !this.isSleeping() && this.getAge() == 0; // Purpur + } + + private boolean hungry() { +@@ -931,6 +1033,11 @@ public class Villager extends AbstractVillager implements ReputationEventHandler + + public boolean hasFarmSeeds() { + return this.getInventory().hasAnyMatching((itemstack) -> { ++ // Purpur start ++ if (this.level().purpurConfig.villagerClericsFarmWarts && this.getVillagerData().getProfession() == VillagerProfession.CLERIC) { ++ return itemstack.is(Items.NETHER_WART); ++ } ++ // Purpur end + return itemstack.is(ItemTags.VILLAGER_PLANTABLE_SEEDS); + }); + } +@@ -988,6 +1095,7 @@ public class Villager extends AbstractVillager implements ReputationEventHandler + } + + public void spawnGolemIfNeeded(ServerLevel world, long time, int requiredCount) { ++ if (world.purpurConfig.villagerSpawnIronGolemRadius > 0 && world.getEntitiesOfClass(net.minecraft.world.entity.animal.IronGolem.class, getBoundingBox().inflate(world.purpurConfig.villagerSpawnIronGolemRadius)).size() > world.purpurConfig.villagerSpawnIronGolemLimit) return; // Purpur + if (this.wantsToSpawnGolem(time)) { + AABB axisalignedbb = this.getBoundingBox().inflate(10.0D, 10.0D, 10.0D); + List list = world.getEntitiesOfClass(Villager.class, axisalignedbb); +@@ -1052,6 +1160,12 @@ public class Villager extends AbstractVillager implements ReputationEventHandler + + @Override + public void startSleeping(BlockPos pos) { ++ // Purpur start ++ if (level().purpurConfig.bedExplodeOnVillagerSleep && this.level().getBlockState(pos).getBlock() instanceof net.minecraft.world.level.block.BedBlock) { ++ this.level().explode(null, (double) pos.getX() + 0.5D, (double) pos.getY() + 0.5D, (double) pos.getZ() + 0.5D, (float) this.level().purpurConfig.bedExplosionPower, this.level().purpurConfig.bedExplosionFire, this.level().purpurConfig.bedExplosionEffect); ++ return; ++ } ++ // Purpur end + super.startSleeping(pos); + this.brain.setMemory(MemoryModuleType.LAST_SLEPT, this.level().getGameTime()); // CraftBukkit - decompile error + this.brain.eraseMemory(MemoryModuleType.WALK_TARGET); +diff --git a/src/main/java/net/minecraft/world/entity/npc/VillagerProfession.java b/src/main/java/net/minecraft/world/entity/npc/VillagerProfession.java +index 1316f4475802e17039800cc6128e1b065328beb7..d02e2d1aceac651e06a3a3698b7ac64dd30d4b12 100644 +--- a/src/main/java/net/minecraft/world/entity/npc/VillagerProfession.java ++++ b/src/main/java/net/minecraft/world/entity/npc/VillagerProfession.java +@@ -31,7 +31,7 @@ public record VillagerProfession( + public static final VillagerProfession ARMORER = register("armorer", PoiTypes.ARMORER, SoundEvents.VILLAGER_WORK_ARMORER); + public static final VillagerProfession BUTCHER = register("butcher", PoiTypes.BUTCHER, SoundEvents.VILLAGER_WORK_BUTCHER); + public static final VillagerProfession CARTOGRAPHER = register("cartographer", PoiTypes.CARTOGRAPHER, SoundEvents.VILLAGER_WORK_CARTOGRAPHER); +- public static final VillagerProfession CLERIC = register("cleric", PoiTypes.CLERIC, SoundEvents.VILLAGER_WORK_CLERIC); ++ public static final VillagerProfession CLERIC = register("cleric", PoiTypes.CLERIC, ImmutableSet.of(Items.NETHER_WART), ImmutableSet.of(Blocks.SOUL_SAND), SoundEvents.VILLAGER_WORK_CLERIC); // Purpur + public static final VillagerProfession FARMER = register( + "farmer", + PoiTypes.FARMER, +diff --git a/src/main/java/net/minecraft/world/entity/npc/WanderingTrader.java b/src/main/java/net/minecraft/world/entity/npc/WanderingTrader.java +index 0854e9b7ee2e6b23b6c1ee6a324a5a253c9d4679..c1e573758539a151452b12466339ccf8b39c7d38 100644 +--- a/src/main/java/net/minecraft/world/entity/npc/WanderingTrader.java ++++ b/src/main/java/net/minecraft/world/entity/npc/WanderingTrader.java +@@ -71,6 +71,43 @@ public class WanderingTrader extends net.minecraft.world.entity.npc.AbstractVill + //this.setDespawnDelay(48000); // CraftBukkit - set default from MobSpawnerTrader // Paper - move back to MobSpawnerTrader - Vanilla behavior is that only traders spawned by it have this value set. + } + ++ // Purpur - start ++ @Override ++ public boolean isRidable() { ++ return level().purpurConfig.wanderingTraderRidable; ++ } ++ ++ @Override ++ public boolean dismountsUnderwater() { ++ return level().purpurConfig.useDismountsUnderwaterTag ? super.dismountsUnderwater() : !level().purpurConfig.wanderingTraderRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level().purpurConfig.wanderingTraderControllable; ++ } ++ // Purpur end ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(net.minecraft.world.entity.ai.attributes.Attributes.MAX_HEALTH).setBaseValue(this.level().purpurConfig.wanderingTraderMaxHealth); ++ } ++ ++ @Override ++ public boolean canBeLeashed(Player player) { ++ return level().purpurConfig.wanderingTraderCanBeLeashed && !this.isLeashed(); ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level().purpurConfig.wanderingTraderTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level().purpurConfig.wanderingTraderAlwaysDropExp; ++ } ++ + @Override + protected void registerGoals() { + this.goalSelector.addGoal(0, new FloatGoal(this)); +@@ -78,7 +115,7 @@ public class WanderingTrader extends net.minecraft.world.entity.npc.AbstractVill + return this.canDrinkPotion && this.level().isNight() && !entityvillagertrader.isInvisible(); // Paper - Add more WanderingTrader API + })); + this.goalSelector.addGoal(0, new UseItemGoal<>(this, new ItemStack(Items.MILK_BUCKET), SoundEvents.WANDERING_TRADER_REAPPEARED, (entityvillagertrader) -> { +- return this.canDrinkMilk && this.level().isDay() && entityvillagertrader.isInvisible(); // Paper - Add more WanderingTrader API ++ return level().purpurConfig.milkClearsBeneficialEffects && this.canDrinkMilk && this.level().isDay() && entityvillagertrader.isInvisible(); // Paper - Add more WanderingTrader API // Purpur + })); + this.goalSelector.addGoal(1, new TradeWithPlayerGoal(this)); + this.goalSelector.addGoal(1, new AvoidEntityGoal<>(this, Zombie.class, 8.0F, 0.5D, 0.5D)); +@@ -91,6 +128,7 @@ public class WanderingTrader extends net.minecraft.world.entity.npc.AbstractVill + this.goalSelector.addGoal(1, new PanicGoal(this, 0.5D)); + this.goalSelector.addGoal(1, new LookAtTradingPlayerGoal(this)); + this.goalSelector.addGoal(2, new WanderingTrader.WanderToPositionGoal(this, 2.0D, 0.35D)); ++ if (level().purpurConfig.wanderingTraderFollowEmeraldBlock) this.goalSelector.addGoal(3, new net.minecraft.world.entity.ai.goal.TemptGoal(this, 1.0D, TEMPT_ITEMS, false)); // Purpur + this.goalSelector.addGoal(4, new MoveTowardsRestrictionGoal(this, 0.35D)); + this.goalSelector.addGoal(8, new WaterAvoidingRandomStrollGoal(this, 0.35D)); + this.goalSelector.addGoal(9, new InteractGoal(this, Player.class, 3.0F, 1.0F)); +@@ -118,9 +156,10 @@ public class WanderingTrader extends net.minecraft.world.entity.npc.AbstractVill + } + + if (this.getOffers().isEmpty()) { +- return InteractionResult.sidedSuccess(this.level().isClientSide); ++ return tryRide(player, hand, InteractionResult.sidedSuccess(this.level().isClientSide)); // Purpur + } else { +- if (!this.level().isClientSide) { ++ if (level().purpurConfig.wanderingTraderRidable && itemstack.isEmpty()) return tryRide(player, hand); // Purpur ++ if (this.level().purpurConfig.wanderingTraderAllowTrading) { + this.setTradingPlayer(player); + this.openTradingScreen(player, this.getDisplayName(), 1); + } +diff --git a/src/main/java/net/minecraft/world/entity/npc/WanderingTraderSpawner.java b/src/main/java/net/minecraft/world/entity/npc/WanderingTraderSpawner.java +index c72b6ea5530e54fc373c701028e1c147cea34b59..96e9fce5f9084737d2fcf4deb83305733b480179 100644 +--- a/src/main/java/net/minecraft/world/entity/npc/WanderingTraderSpawner.java ++++ b/src/main/java/net/minecraft/world/entity/npc/WanderingTraderSpawner.java +@@ -160,7 +160,17 @@ public class WanderingTraderSpawner implements CustomSpawner { + int k = pos.getX() + this.random.nextInt(range * 2) - range; + int l = pos.getZ() + this.random.nextInt(range * 2) - range; + int i1 = world.getHeight(Heightmap.Types.WORLD_SURFACE, k, l); +- BlockPos blockposition2 = new BlockPos(k, i1, l); ++ // Purpur start - allow traders to spawn below nether roof ++ BlockPos.MutableBlockPos blockposition2 = new BlockPos.MutableBlockPos(k, i1, l); ++ if (world.dimensionType().hasCeiling()) { ++ do { ++ blockposition2.relative(net.minecraft.core.Direction.DOWN); ++ } while (!world.getBlockState(blockposition2).isAir()); ++ do { ++ blockposition2.relative(net.minecraft.core.Direction.DOWN); ++ } while (world.getBlockState(blockposition2).isAir() && blockposition2.getY() > 0); ++ } ++ // Purpur end + + if (spawnplacementtype.isSpawnPositionOk(world, blockposition2, EntityType.WANDERING_TRADER)) { + blockposition1 = blockposition2; +diff --git a/src/main/java/net/minecraft/world/entity/player/Player.java b/src/main/java/net/minecraft/world/entity/player/Player.java +index 093d1388ff90ad59110a37536b6639f939549068..845c4af5d5d38d54de4a1b20fe32bf5dd4776a29 100644 +--- a/src/main/java/net/minecraft/world/entity/player/Player.java ++++ b/src/main/java/net/minecraft/world/entity/player/Player.java +@@ -195,17 +195,40 @@ public abstract class Player extends LivingEntity { + public boolean ignoreFallDamageFromCurrentImpulse; + public boolean affectsSpawning = true; // Paper - Affects Spawning API + public net.kyori.adventure.util.TriState flyingFallDamage = net.kyori.adventure.util.TriState.NOT_SET; // Paper - flying fall damage ++ public int sixRowEnderchestSlotCount = -1; // Purpur ++ public int burpDelay = 0; // Purpur ++ public boolean canPortalInstant = false; // Purpur + + // CraftBukkit start + public boolean fauxSleeping; + public int oldLevel = -1; + ++ public void setAfk(boolean afk) { ++ } ++ ++ public boolean isAfk() { ++ return false; ++ } ++ + @Override + public CraftHumanEntity getBukkitEntity() { + return (CraftHumanEntity) super.getBukkitEntity(); + } + // CraftBukkit end + ++ // Purpur start ++ public abstract void resetLastActionTime(); ++ ++ @Override ++ public boolean processClick(InteractionHand hand) { ++ Entity vehicle = getRootVehicle(); ++ if (vehicle != null && vehicle.getRider() == this) { ++ return vehicle.onClick(hand); ++ } ++ return false; ++ } ++ // Purpur end ++ + public Player(Level world, BlockPos pos, float yaw, GameProfile gameProfile) { + super(EntityType.PLAYER, world); + this.lastItemInMainHand = ItemStack.EMPTY; +@@ -250,6 +273,12 @@ public abstract class Player extends LivingEntity { + + @Override + public void tick() { ++ // Purpur start ++ if (this.burpDelay > 0 && --this.burpDelay == 0) { ++ this.level().playSound(null, getX(), getY(), getZ(), SoundEvents.PLAYER_BURP, SoundSource.PLAYERS, 1.0F, this.level().random.nextFloat() * 0.1F + 0.9F); ++ } ++ // Purpur end ++ + this.noPhysics = this.isSpectator(); + if (this.isSpectator()) { + this.setOnGround(false); +@@ -360,6 +389,16 @@ public abstract class Player extends LivingEntity { + this.addEffect(new MobEffectInstance(MobEffects.WATER_BREATHING, 200, 0, false, false, true), org.bukkit.event.entity.EntityPotionEffectEvent.Cause.TURTLE_HELMET); // CraftBukkit + } + ++ // Purpur start ++ if (this.level().purpurConfig.playerNetheriteFireResistanceDuration > 0 && this.level().getGameTime() % 20 == 0) { ++ if (itemstack.is(Items.NETHERITE_HELMET) ++ && this.getItemBySlot(EquipmentSlot.CHEST).is(Items.NETHERITE_CHESTPLATE) ++ && this.getItemBySlot(EquipmentSlot.LEGS).is(Items.NETHERITE_LEGGINGS) ++ && this.getItemBySlot(EquipmentSlot.FEET).is(Items.NETHERITE_BOOTS)) { ++ this.addEffect(new MobEffectInstance(MobEffects.FIRE_RESISTANCE, this.level().purpurConfig.playerNetheriteFireResistanceDuration, this.level().purpurConfig.playerNetheriteFireResistanceAmplifier, this.level().purpurConfig.playerNetheriteFireResistanceAmbient, this.level().purpurConfig.playerNetheriteFireResistanceShowParticles, this.level().purpurConfig.playerNetheriteFireResistanceShowIcon), org.bukkit.event.entity.EntityPotionEffectEvent.Cause.NETHERITE_ARMOR); ++ } ++ } ++ // Purpur end + } + + protected ItemCooldowns createItemCooldowns() { +@@ -450,7 +489,7 @@ public abstract class Player extends LivingEntity { + + @Override + public int getPortalWaitTime() { +- return Math.max(1, this.level().getGameRules().getInt(this.abilities.invulnerable ? GameRules.RULE_PLAYERS_NETHER_PORTAL_CREATIVE_DELAY : GameRules.RULE_PLAYERS_NETHER_PORTAL_DEFAULT_DELAY)); ++ return Math.max(1, canPortalInstant ? 1 : this.level().getGameRules().getInt(this.abilities.invulnerable ? GameRules.RULE_PLAYERS_NETHER_PORTAL_CREATIVE_DELAY : GameRules.RULE_PLAYERS_NETHER_PORTAL_DEFAULT_DELAY)); + } + + @Override +@@ -593,7 +632,7 @@ public abstract class Player extends LivingEntity { + while (iterator.hasNext()) { + Entity entity = (Entity) iterator.next(); + +- if (entity.getType() == EntityType.EXPERIENCE_ORB) { ++ if (entity.getType() == EntityType.EXPERIENCE_ORB && entity.level().purpurConfig.playerExpPickupDelay >= 0) { // Purpur + list1.add(entity); + } else if (!entity.isRemoved()) { + this.touch(entity); +@@ -1301,7 +1340,7 @@ public abstract class Player extends LivingEntity { + + flag2 = flag2 && !this.level().paperConfig().entities.behavior.disablePlayerCrits; // Paper - Toggleable player crits + if (flag2) { +- f *= 1.5F; ++ f *= this.level().purpurConfig.playerCriticalDamageMultiplier; // Purpur + } + + f += f1; +@@ -1941,9 +1980,19 @@ public abstract class Player extends LivingEntity { + @Override + public int getExperienceReward() { + if (!this.level().getGameRules().getBoolean(GameRules.RULE_KEEPINVENTORY) && !this.isSpectator()) { +- int i = this.experienceLevel * 7; +- +- return i > 100 ? 100 : i; ++ // Purpur start ++ int toDrop; ++ try { ++ toDrop = Math.round(((Number) scriptEngine.eval("let expLevel = " + experienceLevel + "; " + ++ "let expTotal = " + totalExperience + "; " + ++ "let exp = " + experienceProgress + "; " + ++ level().purpurConfig.playerDeathExpDropEquation)).floatValue()); ++ } catch (javax.script.ScriptException e) { ++ e.printStackTrace(); ++ toDrop = experienceLevel * 7; ++ } ++ return Math.min(toDrop, level().purpurConfig.playerDeathExpDropMax); ++ // Purpur end + } else { + return 0; + } +@@ -2024,6 +2073,13 @@ public abstract class Player extends LivingEntity { + return slot != EquipmentSlot.BODY; + } + ++ // Purpur start ++ @Override ++ public boolean dismountsUnderwater() { ++ return !level().purpurConfig.playerRidableInWater; ++ } ++ // Purpur end ++ + public boolean setEntityOnShoulder(CompoundTag entityNbt) { + if (!this.isPassenger() && this.onGround() && !this.isInWater() && !this.isInPowderSnow) { + if (this.getShoulderEntityLeft().isEmpty()) { +@@ -2321,7 +2377,7 @@ public abstract class Player extends LivingEntity { + public ItemStack eat(Level world, ItemStack stack) { + this.getFoodData().eat(stack); + this.awardStat(Stats.ITEM_USED.get(stack.getItem())); +- world.playSound((Player) null, this.getX(), this.getY(), this.getZ(), SoundEvents.PLAYER_BURP, SoundSource.PLAYERS, 0.5F, world.random.nextFloat() * 0.1F + 0.9F); ++ // world.playSound((Player) null, this.getX(), this.getY(), this.getZ(), SoundEvents.PLAYER_BURP, SoundSource.PLAYERS, 0.5F, world.random.nextFloat() * 0.1F + 0.9F); // Purpur - moved to tick() + if (this instanceof ServerPlayer) { + CriteriaTriggers.CONSUME_ITEM.trigger((ServerPlayer) this, stack); + } +diff --git a/src/main/java/net/minecraft/world/entity/projectile/AbstractArrow.java b/src/main/java/net/minecraft/world/entity/projectile/AbstractArrow.java +index 31b8a8bf78d52b5f11b68e780ec09bf78e7bda84..06f7bc4d8d6679d6625a8d392777722fc97739ba 100644 +--- a/src/main/java/net/minecraft/world/entity/projectile/AbstractArrow.java ++++ b/src/main/java/net/minecraft/world/entity/projectile/AbstractArrow.java +@@ -77,6 +77,7 @@ public abstract class AbstractArrow extends Projectile { + @Nullable + private List piercedAndKilledEntities; + public ItemStack pickupItemStack; ++ public int lootingLevel; // Purpur + + // Spigot Start + @Override +@@ -332,7 +333,7 @@ public abstract class AbstractArrow extends Projectile { + Vec3 vec3d = this.getDeltaMovement(); + + this.setDeltaMovement(vec3d.multiply((double) (this.random.nextFloat() * 0.2F), (double) (this.random.nextFloat() * 0.2F), (double) (this.random.nextFloat() * 0.2F))); +- this.life = 0; ++ if (this.level().purpurConfig.arrowMovementResetsDespawnCounter) this.life = 0; // Purpur - do not reset despawn counter + } + + @Override +@@ -655,6 +656,12 @@ public abstract class AbstractArrow extends Projectile { + this.knockback = punch; + } + ++ // Purpur start ++ public void setLootingLevel(int looting) { ++ this.lootingLevel = looting; ++ } ++ // Purpur end ++ + public int getKnockback() { + return this.knockback; + } +diff --git a/src/main/java/net/minecraft/world/entity/projectile/LargeFireball.java b/src/main/java/net/minecraft/world/entity/projectile/LargeFireball.java +index 9d89872c5958f3e8d6c1ef4fd93f9b8b85296851..6a94c86acce5afbf1e9c8e7d664b3eb2d79ab5ab 100644 +--- a/src/main/java/net/minecraft/world/entity/projectile/LargeFireball.java ++++ b/src/main/java/net/minecraft/world/entity/projectile/LargeFireball.java +@@ -19,20 +19,20 @@ public class LargeFireball extends Fireball { + + public LargeFireball(EntityType type, Level world) { + super(type, world); +- this.isIncendiary = this.level().getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING); // CraftBukkit ++ this.isIncendiary = this.level().purpurConfig.fireballsBypassMobGriefing || this.level().getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING); // CraftBukkit // Purpur + } + + public LargeFireball(Level world, LivingEntity owner, double velocityX, double velocityY, double velocityZ, int explosionPower) { + super(EntityType.FIREBALL, owner, velocityX, velocityY, velocityZ, world); + this.explosionPower = explosionPower; +- this.isIncendiary = this.level().getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING); // CraftBukkit ++ this.isIncendiary = this.level().purpurConfig.fireballsBypassMobGriefing || this.level().getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING); // CraftBukkit // Purpur + } + + @Override + protected void onHit(HitResult hitResult) { + super.onHit(hitResult); + if (!this.level().isClientSide) { +- boolean flag = this.level().getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING); ++ boolean flag = this.level().purpurConfig.fireballsBypassMobGriefing || this.level().getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING); // Purpur + + // CraftBukkit start - fire ExplosionPrimeEvent + ExplosionPrimeEvent event = new ExplosionPrimeEvent((org.bukkit.entity.Explosive) this.getBukkitEntity()); +diff --git a/src/main/java/net/minecraft/world/entity/projectile/LlamaSpit.java b/src/main/java/net/minecraft/world/entity/projectile/LlamaSpit.java +index ffd01d24cbfc90e2a8807757e61b2cf20a944354..a419820d5001079ed839e67c757bc8fa591a20b3 100644 +--- a/src/main/java/net/minecraft/world/entity/projectile/LlamaSpit.java ++++ b/src/main/java/net/minecraft/world/entity/projectile/LlamaSpit.java +@@ -30,6 +30,12 @@ public class LlamaSpit extends Projectile { + this.setPos(owner.getX() - (double) (owner.getBbWidth() + 1.0F) * 0.5D * (double) Mth.sin(owner.yBodyRot * 0.017453292F), owner.getEyeY() - 0.10000000149011612D, owner.getZ() + (double) (owner.getBbWidth() + 1.0F) * 0.5D * (double) Mth.cos(owner.yBodyRot * 0.017453292F)); + } + ++ // Purpur start ++ public void super_tick() { ++ super.tick(); ++ } ++ // Purpur end ++ + @Override + protected double getDefaultGravity() { + return 0.06D; +diff --git a/src/main/java/net/minecraft/world/entity/projectile/Projectile.java b/src/main/java/net/minecraft/world/entity/projectile/Projectile.java +index 74c596264d4da551437bd2a23e1c70022cfc73fc..e4d4ff0ef4a0f3283aa42fe2304816cd6d9475a8 100644 +--- a/src/main/java/net/minecraft/world/entity/projectile/Projectile.java ++++ b/src/main/java/net/minecraft/world/entity/projectile/Projectile.java +@@ -343,7 +343,7 @@ public abstract class Projectile extends Entity implements TraceableEntity { + public boolean mayInteract(Level world, BlockPos pos) { + Entity entity = this.getOwner(); + +- return entity instanceof Player ? entity.mayInteract(world, pos) : entity == null || world.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING); ++ return entity instanceof Player ? entity.mayInteract(world, pos) : entity == null || world.purpurConfig.projectilesBypassMobGriefing || world.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING); + } + + public boolean mayBreak(Level world) { +diff --git a/src/main/java/net/minecraft/world/entity/projectile/SmallFireball.java b/src/main/java/net/minecraft/world/entity/projectile/SmallFireball.java +index 3a11ad32d95088a5aca713a1a6a984cc22d4fa9a..c078ccad4aabe469a9611331b415a4cef241973e 100644 +--- a/src/main/java/net/minecraft/world/entity/projectile/SmallFireball.java ++++ b/src/main/java/net/minecraft/world/entity/projectile/SmallFireball.java +@@ -27,7 +27,7 @@ public class SmallFireball extends Fireball { + super(EntityType.SMALL_FIREBALL, owner, velocityX, velocityY, velocityZ, world); + // CraftBukkit start + if (this.getOwner() != null && this.getOwner() instanceof Mob) { +- this.isIncendiary = this.level().getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING); ++ this.isIncendiary = this.level().purpurConfig.fireballsBypassMobGriefing || this.level().getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING); // Purpur + } + // CraftBukkit end + } +diff --git a/src/main/java/net/minecraft/world/entity/projectile/Snowball.java b/src/main/java/net/minecraft/world/entity/projectile/Snowball.java +index 2b4d206c0d31ba38d7b2af654bd420e85145d441..1b9d0e28e518c501b4b93ae385ddd64aeade97d5 100644 +--- a/src/main/java/net/minecraft/world/entity/projectile/Snowball.java ++++ b/src/main/java/net/minecraft/world/entity/projectile/Snowball.java +@@ -58,11 +58,41 @@ public class Snowball extends ThrowableItemProjectile { + protected void onHitEntity(EntityHitResult entityHitResult) { + super.onHitEntity(entityHitResult); + Entity entity = entityHitResult.getEntity(); +- int i = entity instanceof Blaze ? 3 : 0; ++ int i = entity.level().purpurConfig.snowballDamage >= 0 ? entity.level().purpurConfig.snowballDamage : entity instanceof Blaze ? 3 : 0; // Purpur + + entity.hurt(this.damageSources().thrown(this, this.getOwner()), (float) i); + } + ++ // Purpur start - borrowed and modified code from ThrownPotion#onHitBlock and ThrownPotion#dowseFire ++ @Override ++ protected void onHitBlock(net.minecraft.world.phys.BlockHitResult blockHitResult) { ++ super.onHitBlock(blockHitResult); ++ ++ if (!this.level().isClientSide) { ++ net.minecraft.core.BlockPos blockposition = blockHitResult.getBlockPos(); ++ net.minecraft.core.BlockPos blockposition1 = blockposition.relative(blockHitResult.getDirection()); ++ ++ net.minecraft.world.level.block.state.BlockState iblockdata = this.level().getBlockState(blockposition); ++ ++ if (this.level().purpurConfig.snowballExtinguishesFire && this.level().getBlockState(blockposition1).is(net.minecraft.world.level.block.Blocks.FIRE)) { ++ if (org.bukkit.craftbukkit.event.CraftEventFactory.callEntityChangeBlockEvent(this, blockposition1, net.minecraft.world.level.block.Blocks.AIR.defaultBlockState())) { ++ this.level().removeBlock(blockposition1, false); ++ } ++ } else if (this.level().purpurConfig.snowballExtinguishesCandles && net.minecraft.world.level.block.AbstractCandleBlock.isLit(iblockdata)) { ++ if (org.bukkit.craftbukkit.event.CraftEventFactory.callEntityChangeBlockEvent(this, blockposition, iblockdata.setValue(net.minecraft.world.level.block.AbstractCandleBlock.LIT, false))) { ++ net.minecraft.world.level.block.AbstractCandleBlock.extinguish(null, iblockdata, this.level(), blockposition); ++ } ++ } else if (this.level().purpurConfig.snowballExtinguishesCampfires && net.minecraft.world.level.block.CampfireBlock.isLitCampfire(iblockdata)) { ++ if (org.bukkit.craftbukkit.event.CraftEventFactory.callEntityChangeBlockEvent(this, blockposition, iblockdata.setValue(net.minecraft.world.level.block.CampfireBlock.LIT, false))) { ++ this.level().levelEvent(null, 1009, blockposition, 0); ++ net.minecraft.world.level.block.CampfireBlock.dowse(this.getOwner(), this.level(), blockposition, iblockdata); ++ this.level().setBlockAndUpdate(blockposition, iblockdata.setValue(net.minecraft.world.level.block.CampfireBlock.LIT, false)); ++ } ++ } ++ } ++ } ++ // Purpur end ++ + @Override + protected void onHit(HitResult hitResult) { + super.onHit(hitResult); +diff --git a/src/main/java/net/minecraft/world/entity/projectile/ThrownEnderpearl.java b/src/main/java/net/minecraft/world/entity/projectile/ThrownEnderpearl.java +index 1fb1e729d6879568d8b4943071fa940325b2e5b0..d9761d8fe746e925a7a32dfc15eb8045c6150fe5 100644 +--- a/src/main/java/net/minecraft/world/entity/projectile/ThrownEnderpearl.java ++++ b/src/main/java/net/minecraft/world/entity/projectile/ThrownEnderpearl.java +@@ -71,10 +71,11 @@ public class ThrownEnderpearl extends ThrowableItemProjectile { + Bukkit.getPluginManager().callEvent(teleEvent); + + if (!teleEvent.isCancelled() && entityplayer.connection.isAcceptingMessages()) { +- if (this.random.nextFloat() < 0.05F && this.level().getGameRules().getBoolean(GameRules.RULE_DOMOBSPAWNING)) { ++ if (this.random.nextFloat() < this.level().purpurConfig.enderPearlEndermiteChance && this.level().getGameRules().getBoolean(GameRules.RULE_DOMOBSPAWNING)) { // Purpur + Endermite entityendermite = (Endermite) EntityType.ENDERMITE.create(this.level()); + + if (entityendermite != null) { ++ entityendermite.setPlayerSpawned(true); // Purpur + entityendermite.moveTo(entity.getX(), entity.getY(), entity.getZ(), entity.getYRot(), entity.getXRot()); + this.level().addFreshEntity(entityendermite, CreatureSpawnEvent.SpawnReason.ENDER_PEARL); + } +@@ -86,7 +87,7 @@ public class ThrownEnderpearl extends ThrowableItemProjectile { + + entityplayer.connection.teleport(teleEvent.getTo()); + entity.resetFallDistance(); +- entity.hurt(this.damageSources().fall().customEventDamager(this), 5.0F); // CraftBukkit // Paper - fix DamageSource API ++ entity.hurt(this.damageSources().fall().customEventDamager(this), this.level().purpurConfig.enderPearlDamage); // CraftBukkit // Paper - fix DamageSource API // Purpur + } + // CraftBukkit end + this.level().playSound((Player) null, this.getX(), this.getY(), this.getZ(), SoundEvents.PLAYER_TELEPORT, SoundSource.PLAYERS); +diff --git a/src/main/java/net/minecraft/world/entity/projectile/ThrownTrident.java b/src/main/java/net/minecraft/world/entity/projectile/ThrownTrident.java +index 3ff06cc6ad35567bcb1f29115db63c11a8e79dbb..f7dd785bdb0dbd0706b367b48235215ff1a0e08f 100644 +--- a/src/main/java/net/minecraft/world/entity/projectile/ThrownTrident.java ++++ b/src/main/java/net/minecraft/world/entity/projectile/ThrownTrident.java +@@ -67,7 +67,7 @@ public class ThrownTrident extends AbstractArrow { + Entity entity = this.getOwner(); + byte b0 = (Byte) this.entityData.get(ThrownTrident.ID_LOYALTY); + +- if (b0 > 0 && (this.dealtDamage || this.isNoPhysics()) && entity != null) { ++ if (b0 > 0 && (this.dealtDamage || this.isNoPhysics() || (level().purpurConfig.tridentLoyaltyVoidReturnHeight < 0.0D && getY() < level().purpurConfig.tridentLoyaltyVoidReturnHeight)) && entity != null) { // Purpur + if (!this.isAcceptibleReturnOwner()) { + if (!this.level().isClientSide && this.pickup == AbstractArrow.Pickup.ALLOWED) { + this.spawnAtLocation(this.getPickupItem(), 0.1F); +diff --git a/src/main/java/net/minecraft/world/entity/projectile/WitherSkull.java b/src/main/java/net/minecraft/world/entity/projectile/WitherSkull.java +index 55b4b5ad5f084c9a271a716d076672478d6486ba..a60d7f7baab005afc532ecec7aa22c53db4f51e0 100644 +--- a/src/main/java/net/minecraft/world/entity/projectile/WitherSkull.java ++++ b/src/main/java/net/minecraft/world/entity/projectile/WitherSkull.java +@@ -99,7 +99,7 @@ public class WitherSkull extends AbstractHurtingProjectile { + if (!this.level().isClientSide) { + // CraftBukkit start + // this.level().explode(this, this.getX(), this.getY(), this.getZ(), 1.0F, false, World.a.MOB); +- ExplosionPrimeEvent event = new ExplosionPrimeEvent(this.getBukkitEntity(), 1.0F, false); ++ ExplosionPrimeEvent event = new ExplosionPrimeEvent(this.getBukkitEntity(), this.level().purpurConfig.witherExplosionRadius, false); // Purpur + this.level().getCraftServer().getPluginManager().callEvent(event); + + if (!event.isCancelled()) { +@@ -111,6 +111,19 @@ public class WitherSkull extends AbstractHurtingProjectile { + + } + ++ // Purpur start ++ @Override ++ public boolean canHitEntity(Entity target) { ++ // do not hit rider ++ return target != this.getRider() && super.canHitEntity(target); ++ } ++ ++ @Override ++ public boolean canSaveToDisk() { ++ return false; ++ } ++ // Purpur end ++ + @Override + public boolean hurt(DamageSource source, float amount) { + return false; +diff --git a/src/main/java/net/minecraft/world/entity/raid/Raider.java b/src/main/java/net/minecraft/world/entity/raid/Raider.java +index 98e558338b5d9fb03869d2cc21b3e90eb45b95f6..4a8fa7e5844b5cd12ef6b113f988b715c7a3ef64 100644 +--- a/src/main/java/net/minecraft/world/entity/raid/Raider.java ++++ b/src/main/java/net/minecraft/world/entity/raid/Raider.java +@@ -341,7 +341,7 @@ public abstract class Raider extends PatrollingMonster { + + @Override + public boolean canUse() { +- if (!this.mob.level().getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING) || !this.mob.canPickUpLoot()) return false; // Paper - respect game and entity rules for picking up items ++ if ((!this.mob.level().purpurConfig.pillagerBypassMobGriefing && !this.mob.level().getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING)) || !this.mob.canPickUpLoot()) return false; // Paper - respect game and entity rules for picking up items // Purpur + Raid raid = this.mob.getCurrentRaid(); + + if (this.mob.hasActiveRaid() && !this.mob.getCurrentRaid().isOver() && this.mob.canBeLeader() && !ItemStack.matches(this.mob.getItemBySlot(EquipmentSlot.HEAD), Raid.getLeaderBannerInstance(this.mob.registryAccess().lookupOrThrow(Registries.BANNER_PATTERN)))) { +diff --git a/src/main/java/net/minecraft/world/entity/raid/Raids.java b/src/main/java/net/minecraft/world/entity/raid/Raids.java +index 8c60f71270d909c10e6617eb64b8fdb42deb73e9..eedce2a3d67d875d5174ee125e2679480d4d412c 100644 +--- a/src/main/java/net/minecraft/world/entity/raid/Raids.java ++++ b/src/main/java/net/minecraft/world/entity/raid/Raids.java +@@ -26,6 +26,7 @@ import net.minecraft.world.phys.Vec3; + public class Raids extends SavedData { + + private static final String RAID_FILE_ID = "raids"; ++ public final Map playerCooldowns = Maps.newHashMap(); + public final Map raidMap = Maps.newHashMap(); + private final ServerLevel level; + private int nextAvailableID; +@@ -51,6 +52,17 @@ public class Raids extends SavedData { + + public void tick() { + ++this.tick; ++ // Purpur start ++ if (level.purpurConfig.raidCooldownSeconds != 0 && this.tick % 20 == 0) { ++ com.google.common.collect.ImmutableMap.copyOf(playerCooldowns).forEach((uuid, i) -> { ++ if (i < 1) { ++ playerCooldowns.remove(uuid); ++ } else { ++ playerCooldowns.put(uuid, i - 1); ++ } ++ }); ++ } ++ // Purpur end + Iterator iterator = this.raidMap.values().iterator(); + + while (iterator.hasNext()) { +@@ -122,11 +134,13 @@ public class Raids extends SavedData { + */ + + if (!raid.isStarted() || (raid.isInProgress() && raid.getRaidOmenLevel() < raid.getMaxRaidOmenLevel())) { // CraftBukkit - fixed a bug with raid: players could add up Bad Omen level even when the raid had finished ++ if (level.purpurConfig.raidCooldownSeconds != 0 && playerCooldowns.containsKey(player.getUUID())) return null; // Purpur + // CraftBukkit start + if (!org.bukkit.craftbukkit.event.CraftEventFactory.callRaidTriggerEvent(raid, player)) { + player.removeEffect(net.minecraft.world.effect.MobEffects.RAID_OMEN); + return null; + } ++ if (level.purpurConfig.raidCooldownSeconds != 0) playerCooldowns.put(player.getUUID(), level.purpurConfig.raidCooldownSeconds); // Purpur + + if (!raid.isStarted() && !this.raidMap.containsKey(raid.getId())) { + this.raidMap.put(raid.getId(), raid); +diff --git a/src/main/java/net/minecraft/world/entity/vehicle/AbstractMinecart.java b/src/main/java/net/minecraft/world/entity/vehicle/AbstractMinecart.java +index 4d7454e5a64fc18e63793a221daa94617f17c666..e7a1ce585c9e552e6f9ce9acd26fdfe5c43e0b5d 100644 +--- a/src/main/java/net/minecraft/world/entity/vehicle/AbstractMinecart.java ++++ b/src/main/java/net/minecraft/world/entity/vehicle/AbstractMinecart.java +@@ -102,12 +102,14 @@ public abstract class AbstractMinecart extends VehicleEntity { + private double flyingY = 0.95; + private double flyingZ = 0.95; + public double maxSpeed = 0.4D; ++ public double storedMaxSpeed; // Purpur + // CraftBukkit end + + protected AbstractMinecart(EntityType type, Level world) { + super(type, world); + this.targetDeltaMovement = Vec3.ZERO; + this.blocksBuilding = true; ++ if (world != null) maxSpeed = storedMaxSpeed = world.purpurConfig.minecartMaxSpeed; // Purpur + } + + protected AbstractMinecart(EntityType type, Level world, double x, double y, double z) { +@@ -296,6 +298,12 @@ public abstract class AbstractMinecart extends VehicleEntity { + + @Override + public void tick() { ++ // Purpur start ++ if (storedMaxSpeed != level().purpurConfig.minecartMaxSpeed) { ++ maxSpeed = storedMaxSpeed = level().purpurConfig.minecartMaxSpeed; ++ } ++ // Purpur end ++ + // CraftBukkit start + double prevX = this.getX(); + double prevY = this.getY(); +@@ -448,16 +456,62 @@ public abstract class AbstractMinecart extends VehicleEntity { + + public void activateMinecart(int x, int y, int z, boolean powered) {} + ++ // Purpur start ++ private Double lastSpeed; ++ ++ public double getControllableSpeed() { ++ BlockState blockState = level().getBlockState(this.blockPosition()); ++ if (!blockState.isSolid()) { ++ blockState = level().getBlockState(this.blockPosition().relative(Direction.DOWN)); ++ } ++ Double speed = level().purpurConfig.minecartControllableBlockSpeeds.get(blockState.getBlock()); ++ if (!blockState.isSolid()) { ++ speed = lastSpeed; ++ } ++ if (speed == null) { ++ speed = level().purpurConfig.minecartControllableBaseSpeed; ++ } ++ return lastSpeed = speed; ++ } ++ // Purpur end ++ + protected void comeOffTrack() { + double d0 = this.getMaxSpeed(); + Vec3 vec3d = this.getDeltaMovement(); + + this.setDeltaMovement(Mth.clamp(vec3d.x, -d0, d0), vec3d.y, Mth.clamp(vec3d.z, -d0, d0)); ++ ++ // Purpur start ++ if (level().purpurConfig.minecartControllable && !isInWater() && !isInLava() && !passengers.isEmpty()) { ++ Entity passenger = passengers.get(0); ++ if (passenger instanceof Player) { ++ Player player = (Player) passenger; ++ if (player.jumping && this.onGround) { ++ setDeltaMovement(new Vec3(getDeltaMovement().x, level().purpurConfig.minecartControllableHopBoost, getDeltaMovement().z)); ++ } ++ if (player.zza != 0.0F) { ++ Vector velocity = player.getBukkitEntity().getEyeLocation().getDirection().normalize().multiply(getControllableSpeed()); ++ if (player.zza < 0.0) { ++ velocity.multiply(-0.5); ++ } ++ setDeltaMovement(new Vec3(velocity.getX(), getDeltaMovement().y, velocity.getZ())); ++ } ++ this.setYRot(passenger.getYRot() - 90); ++ maxUpStep = level().purpurConfig.minecartControllableStepHeight; ++ } else { ++ maxUpStep = 0.0F; ++ } ++ } else { ++ maxUpStep = 0.0F; ++ } ++ // Purpur end ++ + if (this.onGround()) { + // CraftBukkit start - replace magic numbers with our variables + this.setDeltaMovement(new Vec3(this.getDeltaMovement().x * this.derailedX, this.getDeltaMovement().y * this.derailedY, this.getDeltaMovement().z * this.derailedZ)); + // CraftBukkit end + } ++ else if (level().purpurConfig.minecartControllable) setDeltaMovement(new Vec3(getDeltaMovement().x * derailedX, getDeltaMovement().y, getDeltaMovement().z * derailedZ)); // Purpur + + this.move(MoverType.SELF, this.getDeltaMovement()); + if (!this.onGround()) { +@@ -619,7 +673,7 @@ public abstract class AbstractMinecart extends VehicleEntity { + if (d18 > 0.01D) { + double d20 = 0.06D; + +- this.setDeltaMovement(vec3d4.add(vec3d4.x / d18 * 0.06D, 0.0D, vec3d4.z / d18 * 0.06D)); ++ this.setDeltaMovement(vec3d4.add(vec3d4.x / d18 * this.level().purpurConfig.poweredRailBoostModifier, 0.0D, vec3d4.z / d18 * this.level().purpurConfig.poweredRailBoostModifier)); // Purpur + } else { + Vec3 vec3d5 = this.getDeltaMovement(); + double d21 = vec3d5.x; +diff --git a/src/main/java/net/minecraft/world/entity/vehicle/Boat.java b/src/main/java/net/minecraft/world/entity/vehicle/Boat.java +index b068cff9b5aa457d65b679529956e8210296d799..105e2b7d7cd7c64a9164e4114476e44f29433f49 100644 +--- a/src/main/java/net/minecraft/world/entity/vehicle/Boat.java ++++ b/src/main/java/net/minecraft/world/entity/vehicle/Boat.java +@@ -514,6 +514,7 @@ public class Boat extends VehicleEntity implements VariantHolder { + + if (f > 0.0F) { + this.landFriction = f; ++ if (level().purpurConfig.boatEjectPlayersOnLand) ejectPassengers(); // Purpur + return Boat.Status.ON_LAND; + } else { + return Boat.Status.IN_AIR; +@@ -962,7 +963,13 @@ public class Boat extends VehicleEntity implements VariantHolder { + + @Override + public ItemStack getPickResult() { +- return new ItemStack(this.getDropItem()); ++ // Purpur start ++ final ItemStack boat = new ItemStack(this.getDropItem()); ++ if (!this.level().purpurConfig.persistentDroppableEntityDisplayNames) { ++ boat.set(net.minecraft.core.component.DataComponents.CUSTOM_NAME, null); ++ } ++ return boat; ++ // Purpur end + } + + public static enum Type implements StringRepresentable { +diff --git a/src/main/java/net/minecraft/world/food/FoodData.java b/src/main/java/net/minecraft/world/food/FoodData.java +index b89860d451d92ddda64b7e4144542b7fc5fd86f0..dd72d6a79139ff33f26a32b71283ce0b8d084ecc 100644 +--- a/src/main/java/net/minecraft/world/food/FoodData.java ++++ b/src/main/java/net/minecraft/world/food/FoodData.java +@@ -38,7 +38,9 @@ public class FoodData { + } + + public void eat(int food, float saturationModifier) { ++ int oldValue = this.foodLevel; // Purpur + this.add(food, FoodConstants.saturationByModifier(food, saturationModifier)); ++ if (this.entityhuman.level().purpurConfig.playerBurpWhenFull && this.foodLevel == 20 && oldValue < 20) this.entityhuman.burpDelay = this.entityhuman.level().purpurConfig.playerBurpDelay; // Purpur + } + + public void eat(ItemStack stack) { +@@ -105,7 +107,7 @@ public class FoodData { + ++this.tickTimer; + if (this.tickTimer >= this.starvationRate) { // CraftBukkit - add regen rate manipulation + if (player.getHealth() > 10.0F || enumdifficulty == Difficulty.HARD || player.getHealth() > 1.0F && enumdifficulty == Difficulty.NORMAL) { +- player.hurt(player.damageSources().starve(), 1.0F); ++ player.hurt(player.damageSources().starve(), player.level().purpurConfig.hungerStarvationDamage); // Purpur + } + + this.tickTimer = 0; +diff --git a/src/main/java/net/minecraft/world/inventory/AbstractContainerMenu.java b/src/main/java/net/minecraft/world/inventory/AbstractContainerMenu.java +index 32910f677b0522ac8ec513fa0d00b714b52cfae4..f85eef14b91a0ada1f6f4b13ab3966f051ff92d3 100644 +--- a/src/main/java/net/minecraft/world/inventory/AbstractContainerMenu.java ++++ b/src/main/java/net/minecraft/world/inventory/AbstractContainerMenu.java +@@ -76,6 +76,7 @@ public abstract class AbstractContainerMenu { + @Nullable + private ContainerSynchronizer synchronizer; + private boolean suppressRemoteUpdates; ++ @javax.annotation.Nullable protected ItemStack activeQuickItem = null; // Purpur + + // CraftBukkit start + public boolean checkReachable = true; +diff --git a/src/main/java/net/minecraft/world/inventory/AbstractFurnaceMenu.java b/src/main/java/net/minecraft/world/inventory/AbstractFurnaceMenu.java +index 1af7e1548f0648890a1ef2fc0ff4e4c3a56c947c..decea1697c075e7549ccc7501c8e59357d198a60 100644 +--- a/src/main/java/net/minecraft/world/inventory/AbstractFurnaceMenu.java ++++ b/src/main/java/net/minecraft/world/inventory/AbstractFurnaceMenu.java +@@ -147,7 +147,13 @@ public abstract class AbstractFurnaceMenu extends RecipeBookMenu { + } else if (slot != 1 && slot != 0) { + if (this.canSmelt(itemstack1)) { + if (!this.moveItemStackTo(itemstack1, 0, 1, false)) { +- return ItemStack.EMPTY; ++ // Purpur start - fix #625 ++ if (this.isFuel(itemstack1)) { ++ if (!this.moveItemStackTo(itemstack1, 1, 2, false)) { ++ return ItemStack.EMPTY; ++ } ++ } ++ // Purpur end + } + } else if (this.isFuel(itemstack1)) { + if (!this.moveItemStackTo(itemstack1, 1, 2, false)) { +diff --git a/src/main/java/net/minecraft/world/inventory/AnvilMenu.java b/src/main/java/net/minecraft/world/inventory/AnvilMenu.java +index 2bd91b48eaa06f85a5b9b1ae052c70e966ae8e4c..2747f04e657154362af55eef1dd50455e02af371 100644 +--- a/src/main/java/net/minecraft/world/inventory/AnvilMenu.java ++++ b/src/main/java/net/minecraft/world/inventory/AnvilMenu.java +@@ -25,6 +25,13 @@ import org.slf4j.Logger; + import org.bukkit.craftbukkit.inventory.CraftInventoryView; + // CraftBukkit end + ++// Purpur start ++import net.minecraft.nbt.IntTag; ++import net.minecraft.network.protocol.game.ClientboundContainerSetDataPacket; ++import net.minecraft.network.protocol.game.ClientboundContainerSetSlotPacket; ++import net.minecraft.server.level.ServerPlayer; ++// Purpur end ++ + public class AnvilMenu extends ItemCombinerMenu { + + public static final int INPUT_SLOT = 0; +@@ -53,6 +60,8 @@ public class AnvilMenu extends ItemCombinerMenu { + public int maximumRepairCost = 40; + private CraftInventoryView bukkitEntity; + // CraftBukkit end ++ public boolean bypassCost = false; // Purpur ++ public boolean canDoUnsafeEnchants = false; // Purpur + + public AnvilMenu(int syncId, Inventory inventory) { + this(syncId, inventory, ContainerLevelAccess.NULL); +@@ -80,12 +89,15 @@ public class AnvilMenu extends ItemCombinerMenu { + + @Override + protected boolean mayPickup(Player player, boolean present) { +- return (player.hasInfiniteMaterials() || player.experienceLevel >= this.cost.get()) && this.cost.get() > AnvilMenu.DEFAULT_DENIED_COST && present; // CraftBukkit - allow cost 0 like a free item ++ return (player.hasInfiniteMaterials() || player.experienceLevel >= this.cost.get()) && (bypassCost || this.cost.get() > AnvilMenu.DEFAULT_DENIED_COST) && present; // CraftBukkit - allow cost 0 like a free item // Purpur + } + + @Override + protected void onTake(Player player, ItemStack stack) { ++ ItemStack itemstack = activeQuickItem == null ? stack : activeQuickItem; // Purpur ++ if (org.purpurmc.purpur.event.inventory.AnvilTakeResultEvent.getHandlerList().getRegisteredListeners().length > 0) new org.purpurmc.purpur.event.inventory.AnvilTakeResultEvent(player.getBukkitEntity(), getBukkitView(), org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(itemstack)).callEvent(); // Purpur + if (!player.getAbilities().instabuild) { ++ if (bypassCost) ((ServerPlayer) player).lastSentExp = -1; else // Purpur + player.giveExperienceLevels(-this.cost.get()); + } + +@@ -136,6 +148,12 @@ public class AnvilMenu extends ItemCombinerMenu { + + @Override + public void createResult() { ++ // Purpur start ++ bypassCost = false; ++ canDoUnsafeEnchants = false; ++ if (org.purpurmc.purpur.event.inventory.AnvilUpdateResultEvent.getHandlerList().getRegisteredListeners().length > 0) new org.purpurmc.purpur.event.inventory.AnvilUpdateResultEvent(getBukkitView()).callEvent(); ++ // Purpur end ++ + ItemStack itemstack = this.inputSlots.getItem(0); + + this.cost.set(1); +@@ -143,7 +161,7 @@ public class AnvilMenu extends ItemCombinerMenu { + long j = 0L; + byte b0 = 0; + +- if (!itemstack.isEmpty() && EnchantmentHelper.canStoreEnchantments(itemstack)) { ++ if (!itemstack.isEmpty() && canDoUnsafeEnchants || EnchantmentHelper.canStoreEnchantments(itemstack)) { // Purpur + ItemStack itemstack1 = itemstack.copy(); + ItemStack itemstack2 = this.inputSlots.getItem(1); + ItemEnchantments.Mutable itemenchantments_a = new ItemEnchantments.Mutable(EnchantmentHelper.getEnchantmentsForCrafting(itemstack1)); +@@ -210,7 +228,8 @@ public class AnvilMenu extends ItemCombinerMenu { + int i2 = entry.getIntValue(); + + i2 = l1 == i2 ? i2 + 1 : Math.max(i2, l1); +- boolean flag3 = enchantment.canEnchant(itemstack); ++ boolean flag3 = canDoUnsafeEnchants || (org.purpurmc.purpur.PurpurConfig.allowUnsafeEnchants && org.purpurmc.purpur.PurpurConfig.allowInapplicableEnchants) || enchantment.canEnchant(itemstack); // Purpur ++ boolean flag4 = true; // Purpur + + if (this.player.getAbilities().instabuild || itemstack.is(Items.ENCHANTED_BOOK)) { + flag3 = true; +@@ -222,16 +241,20 @@ public class AnvilMenu extends ItemCombinerMenu { + Holder holder1 = (Holder) iterator1.next(); + + if (!holder1.equals(holder) && !enchantment.isCompatibleWith((Enchantment) holder1.value())) { +- flag3 = false; ++ flag4 = canDoUnsafeEnchants || (org.purpurmc.purpur.PurpurConfig.allowUnsafeEnchants && org.purpurmc.purpur.PurpurConfig.allowIncompatibleEnchants); // Purpur flag3 -> flag4 ++ if (!flag4 && org.purpurmc.purpur.PurpurConfig.replaceIncompatibleEnchants) { ++ iterator1.remove(); ++ flag4 = true; ++ } + ++i; + } + } + +- if (!flag3) { ++ if (!flag3 || !flag4) { // Purpur + flag2 = true; + } else { + flag1 = true; +- if (i2 > enchantment.getMaxLevel()) { ++ if ((!org.purpurmc.purpur.PurpurConfig.allowUnsafeEnchants || !org.purpurmc.purpur.PurpurConfig.allowHigherEnchantsLevels) && i2 > enchantment.getMaxLevel()) { // Purpur + i2 = enchantment.getMaxLevel(); + } + +@@ -261,6 +284,54 @@ public class AnvilMenu extends ItemCombinerMenu { + if (!this.itemName.equals(itemstack.getHoverName().getString())) { + b0 = 1; + i += b0; ++ // Purpur start ++ if (this.player != null) { ++ org.bukkit.craftbukkit.entity.CraftHumanEntity player = this.player.getBukkitEntity(); ++ String name = this.itemName; ++ boolean removeItalics = false; ++ if (player.hasPermission("purpur.anvil.remove_italics")) { ++ if (name.startsWith("&r")) { ++ name = name.substring(2); ++ removeItalics = true; ++ } else if (name.startsWith("")) { ++ name = name.substring(3); ++ removeItalics = true; ++ } else if (name.startsWith("")) { ++ name = name.substring(7); ++ removeItalics = true; ++ } ++ } ++ if (this.player.level().purpurConfig.anvilAllowColors) { ++ if (player.hasPermission("purpur.anvil.color")) { ++ java.util.regex.Matcher matcher = java.util.regex.Pattern.compile("(?i)&([0-9a-fr])").matcher(name); ++ while (matcher.find()) { ++ String match = matcher.group(1); ++ name = name.replace("&" + match, "\u00a7" + match.toLowerCase(java.util.Locale.ROOT)); ++ } ++ //name = name.replaceAll("(?i)&([0-9a-fr])", "\u00a7$1"); ++ } ++ if (player.hasPermission("purpur.anvil.format")) { ++ java.util.regex.Matcher matcher = java.util.regex.Pattern.compile("(?i)&([k-or])").matcher(name); ++ while (matcher.find()) { ++ String match = matcher.group(1); ++ name = name.replace("&" + match, "\u00a7" + match.toLowerCase(java.util.Locale.ROOT)); ++ } ++ //name = name.replaceAll("(?i)&([l-or])", "\u00a7$1"); ++ } ++ } ++ net.kyori.adventure.text.Component component; ++ if (this.player.level().purpurConfig.anvilColorsUseMiniMessage && player.hasPermission("purpur.anvil.minimessage")) { ++ component = net.kyori.adventure.text.minimessage.MiniMessage.miniMessage().deserialize(org.bukkit.ChatColor.stripColor(name)); ++ } else { ++ component = net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection().deserialize(name); ++ } ++ if (removeItalics) { ++ component = component.decoration(net.kyori.adventure.text.format.TextDecoration.ITALIC, false); ++ } ++ itemstack1.set(DataComponents.CUSTOM_NAME, io.papermc.paper.adventure.PaperAdventure.asVanilla(component)); ++ } ++ else ++ // Purpur end + itemstack1.set(DataComponents.CUSTOM_NAME, Component.literal(this.itemName)); + } + } else if (itemstack.has(DataComponents.CUSTOM_NAME)) { +@@ -280,6 +351,12 @@ public class AnvilMenu extends ItemCombinerMenu { + this.cost.set(this.maximumRepairCost - 1); // CraftBukkit + } + ++ // Purpur start ++ if (bypassCost && cost.get() >= maximumRepairCost) { ++ cost.set(maximumRepairCost - 1); ++ } ++ // Purpur end ++ + if (this.cost.get() >= this.maximumRepairCost && !this.player.getAbilities().instabuild) { // CraftBukkit + itemstack1 = ItemStack.EMPTY; + } +@@ -301,6 +378,12 @@ public class AnvilMenu extends ItemCombinerMenu { + org.bukkit.craftbukkit.event.CraftEventFactory.callPrepareAnvilEvent(this.getBukkitView(), itemstack1); // CraftBukkit + this.sendAllDataToRemote(); // CraftBukkit - SPIGOT-6686: Always send completed inventory to stay in sync with client + this.broadcastChanges(); ++ // Purpur start ++ if ((canDoUnsafeEnchants || org.purpurmc.purpur.PurpurConfig.allowUnsafeEnchants) && itemstack1 != ItemStack.EMPTY) { ++ ((ServerPlayer) player).connection.send(new ClientboundContainerSetSlotPacket(containerId, incrementStateId(), 2, itemstack1)); ++ ((ServerPlayer) player).connection.send(new ClientboundContainerSetDataPacket(containerId, 0, cost.get())); ++ } ++ // Purpur end + } else { + org.bukkit.craftbukkit.event.CraftEventFactory.callPrepareAnvilEvent(this.getBukkitView(), ItemStack.EMPTY); // CraftBukkit + this.cost.set(AnvilMenu.DEFAULT_DENIED_COST); // CraftBukkit - use a variable for set a cost for denied item +@@ -308,7 +391,7 @@ public class AnvilMenu extends ItemCombinerMenu { + } + + public static int calculateIncreasedRepairCost(int cost) { +- return (int) Math.min((long) cost * 2L + 1L, 2147483647L); ++ return org.purpurmc.purpur.PurpurConfig.anvilCumulativeCost ? (int) Math.min((long) cost * 2L + 1L, 2147483647L) : 0; + } + + public boolean setItemName(String newItemName) { +diff --git a/src/main/java/net/minecraft/world/inventory/ChestMenu.java b/src/main/java/net/minecraft/world/inventory/ChestMenu.java +index 0dbfd23bbfc6ad203f048142f8c90ef741849fe1..9a80427d2bb470b6b1638e59aba57216676dcbd2 100644 +--- a/src/main/java/net/minecraft/world/inventory/ChestMenu.java ++++ b/src/main/java/net/minecraft/world/inventory/ChestMenu.java +@@ -67,10 +67,30 @@ public class ChestMenu extends AbstractContainerMenu { + return new ChestMenu(MenuType.GENERIC_9x6, syncId, playerInventory, 6); + } + ++ // Purpur start ++ public static ChestMenu oneRow(int syncId, Inventory playerInventory, Container inventory) { ++ return new ChestMenu(MenuType.GENERIC_9x1, syncId, playerInventory, inventory, 1); ++ } ++ ++ public static ChestMenu twoRows(int syncId, Inventory playerInventory, Container inventory) { ++ return new ChestMenu(MenuType.GENERIC_9x2, syncId, playerInventory, inventory, 2); ++ } ++ // Purpur end ++ + public static ChestMenu threeRows(int syncId, Inventory playerInventory, Container inventory) { + return new ChestMenu(MenuType.GENERIC_9x3, syncId, playerInventory, inventory, 3); + } + ++ // Purpur start ++ public static ChestMenu fourRows(int syncId, Inventory playerInventory, Container inventory) { ++ return new ChestMenu(MenuType.GENERIC_9x4, syncId, playerInventory, inventory, 4); ++ } ++ ++ public static ChestMenu fiveRows(int syncId, Inventory playerInventory, Container inventory) { ++ return new ChestMenu(MenuType.GENERIC_9x5, syncId, playerInventory, inventory, 5); ++ } ++ // Purpur end ++ + public static ChestMenu sixRows(int syncId, Inventory playerInventory, Container inventory) { + return new ChestMenu(MenuType.GENERIC_9x6, syncId, playerInventory, inventory, 6); + } +diff --git a/src/main/java/net/minecraft/world/inventory/EnchantmentMenu.java b/src/main/java/net/minecraft/world/inventory/EnchantmentMenu.java +index 5b3e33807e0e13480e3359c0cf067719e5749237..c3a644b0f8c7c5622acc9e1a496f95d432718806 100644 +--- a/src/main/java/net/minecraft/world/inventory/EnchantmentMenu.java ++++ b/src/main/java/net/minecraft/world/inventory/EnchantmentMenu.java +@@ -38,6 +38,12 @@ import org.bukkit.event.enchantment.PrepareItemEnchantEvent; + import org.bukkit.entity.Player; + // CraftBukkit end + ++// Purpur start ++import net.minecraft.world.level.block.entity.BlockEntity; ++import net.minecraft.world.level.block.entity.EnchantingTableBlockEntity; ++import org.bukkit.craftbukkit.entity.CraftHumanEntity; ++// Purpur end ++ + public class EnchantmentMenu extends AbstractContainerMenu { + + static final ResourceLocation EMPTY_SLOT_LAPIS_LAZULI = new ResourceLocation("item/empty_slot_lapis_lazuli"); +@@ -72,6 +78,22 @@ public class EnchantmentMenu extends AbstractContainerMenu { + return context.getLocation(); + } + // CraftBukkit end ++ ++ // Purpur start ++ @Override ++ public void onClose(CraftHumanEntity who) { ++ super.onClose(who); ++ ++ if (who.getHandle().level().purpurConfig.enchantmentTableLapisPersists) { ++ access.execute((level, pos) -> { ++ BlockEntity blockEntity = level.getBlockEntity(pos); ++ if (blockEntity instanceof EnchantingTableBlockEntity enchantmentTable) { ++ enchantmentTable.setLapis(this.getItem(1).getCount()); ++ } ++ }); ++ } ++ } ++ // Purpur end + }; + this.random = RandomSource.create(); + this.enchantmentSeed = DataSlot.standalone(); +@@ -97,6 +119,17 @@ public class EnchantmentMenu extends AbstractContainerMenu { + } + }); + ++ // Purpur start ++ access.execute((level, pos) -> { ++ if (level.purpurConfig.enchantmentTableLapisPersists) { ++ BlockEntity blockEntity = level.getBlockEntity(pos); ++ if (blockEntity instanceof EnchantingTableBlockEntity enchantmentTable) { ++ this.getSlot(1).set(new ItemStack(Items.LAPIS_LAZULI, enchantmentTable.getLapis())); ++ } ++ } ++ }); ++ // Purpur end ++ + int j; + + for (j = 0; j < 3; ++j) { +@@ -332,6 +365,7 @@ public class EnchantmentMenu extends AbstractContainerMenu { + public void removed(net.minecraft.world.entity.player.Player player) { + super.removed(player); + this.access.execute((world, blockposition) -> { ++ if (world.purpurConfig.enchantmentTableLapisPersists) this.getSlot(1).set(ItemStack.EMPTY); // Purpur + this.clearContainer(player, this.enchantSlots); + }); + } +diff --git a/src/main/java/net/minecraft/world/inventory/GrindstoneMenu.java b/src/main/java/net/minecraft/world/inventory/GrindstoneMenu.java +index db9444dda248260372d96ce239a590e88a4c1142..1d181de9c8fd45b4d9f0230f80d5752ff5c1a432 100644 +--- a/src/main/java/net/minecraft/world/inventory/GrindstoneMenu.java ++++ b/src/main/java/net/minecraft/world/inventory/GrindstoneMenu.java +@@ -95,9 +95,11 @@ public class GrindstoneMenu extends AbstractContainerMenu { + + @Override + public void onTake(net.minecraft.world.entity.player.Player player, ItemStack stack) { ++ ItemStack itemstack = activeQuickItem == null ? stack : activeQuickItem; // Purpur + context.execute((world, blockposition) -> { ++ org.purpurmc.purpur.event.inventory.GrindstoneTakeResultEvent grindstoneTakeResultEvent = new org.purpurmc.purpur.event.inventory.GrindstoneTakeResultEvent(player.getBukkitEntity(), getBukkitView(), org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(itemstack), this.getExperienceAmount(world)); grindstoneTakeResultEvent.callEvent(); // Purpur + if (world instanceof ServerLevel) { +- ExperienceOrb.award((ServerLevel) world, Vec3.atCenterOf(blockposition), this.getExperienceAmount(world), org.bukkit.entity.ExperienceOrb.SpawnReason.GRINDSTONE, player); // Paper ++ ExperienceOrb.award((ServerLevel) world, Vec3.atCenterOf(blockposition), grindstoneTakeResultEvent.getExperienceAmount(), org.bukkit.entity.ExperienceOrb.SpawnReason.GRINDSTONE, player); // Paper // Purpur + } + + world.levelEvent(1042, blockposition, 0); +@@ -130,7 +132,7 @@ public class GrindstoneMenu extends AbstractContainerMenu { + Enchantment enchantment = (Enchantment) ((Holder) entry.getKey()).value(); + int k = entry.getIntValue(); + +- if (!enchantment.isCurse()) { ++ if (!org.purpurmc.purpur.PurpurConfig.grindstoneIgnoredEnchants.contains(enchantment)) { // Purpur + j += enchantment.getMinCost(k); + } + } +@@ -229,7 +231,7 @@ public class GrindstoneMenu extends AbstractContainerMenu { + Entry> entry = (Entry) iterator.next(); + Enchantment enchantment = (Enchantment) ((Holder) entry.getKey()).value(); + +- if (!enchantment.isCurse() || itemenchantments_a.getLevel(enchantment) == 0) { ++ if (!org.purpurmc.purpur.PurpurConfig.grindstoneIgnoredEnchants.contains(enchantment) || itemenchantments_a.getLevel(enchantment) == 0) { // Purpur + itemenchantments_a.upgrade(enchantment, entry.getIntValue()); + } + } +@@ -237,10 +239,70 @@ public class GrindstoneMenu extends AbstractContainerMenu { + }); + } + ++ // Purpur start ++ private java.util.List> GRINDSTONE_REMOVE_ATTRIBUTES_REMOVAL_LIST = java.util.List.of( ++ // DataComponents.MAX_STACK_SIZE, ++ // DataComponents.DAMAGE, ++ // DataComponents.BLOCK_STATE, ++ DataComponents.CUSTOM_DATA, ++ // DataComponents.MAX_DAMAGE, ++ // DataComponents.UNBREAKABLE, ++ // DataComponents.CUSTOM_NAME, ++ // DataComponents.ITEM_NAME, ++ // DataComponents.LORE, ++ // DataComponents.RARITY, ++ // DataComponents.ENCHANTMENTS, ++ // DataComponents.CAN_PLACE_ON, ++ // DataComponents.CAN_BREAK, ++ DataComponents.ATTRIBUTE_MODIFIERS, ++ DataComponents.CUSTOM_MODEL_DATA, ++ // DataComponents.HIDE_ADDITIONAL_TOOLTIP, ++ // DataComponents.HIDE_TOOLTIP, ++ // DataComponents.REPAIR_COST, ++ // DataComponents.CREATIVE_SLOT_LOCK, ++ // DataComponents.ENCHANTMENT_GLINT_OVERRIDE, ++ // DataComponents.INTANGIBLE_PROJECTILE, ++ // DataComponents.FOOD, ++ // DataComponents.FIRE_RESISTANT, ++ // DataComponents.TOOL, ++ // DataComponents.STORED_ENCHANTMENTS, ++ DataComponents.DYED_COLOR, ++ // DataComponents.MAP_COLOR, ++ // DataComponents.MAP_ID, ++ // DataComponents.MAP_DECORATIONS, ++ // DataComponents.MAP_POST_PROCESSING, ++ // DataComponents.CHARGED_PROJECTILES, ++ // DataComponents.BUNDLE_CONTENTS, ++ // DataComponents.POTION_CONTENTS, ++ DataComponents.SUSPICIOUS_STEW_EFFECTS ++ // DataComponents.WRITABLE_BOOK_CONTENT, ++ // DataComponents.WRITTEN_BOOK_CONTENT, ++ // DataComponents.TRIM, ++ // DataComponents.DEBUG_STICK_STATE, ++ // DataComponents.ENTITY_DATA, ++ // DataComponents.BUCKET_ENTITY_DATA, ++ // DataComponents.BLOCK_ENTITY_DATA, ++ // DataComponents.INSTRUMENT, ++ // DataComponents.OMINOUS_BOTTLE_AMPLIFIER, ++ // DataComponents.RECIPES, ++ // DataComponents.LODESTONE_TRACKER, ++ // DataComponents.FIREWORK_EXPLOSION, ++ // DataComponents.FIREWORKS, ++ // DataComponents.PROFILE, ++ // DataComponents.NOTE_BLOCK_SOUND, ++ // DataComponents.BANNER_PATTERNS, ++ // DataComponents.BASE_COLOR, ++ // DataComponents.POT_DECORATIONS, ++ // DataComponents.CONTAINER, ++ // DataComponents.BEES, ++ // DataComponents.LOCK, ++ // DataComponents.CONTAINER_LOOT, ++ ); ++ // Purpur end + private ItemStack removeNonCursesFrom(ItemStack item) { + ItemEnchantments itemenchantments = EnchantmentHelper.updateEnchantments(item, (itemenchantments_a) -> { + itemenchantments_a.removeIf((holder) -> { +- return !((Enchantment) holder.value()).isCurse(); ++ return !org.purpurmc.purpur.PurpurConfig.grindstoneIgnoredEnchants.contains(holder.value()); + }); + }); + +@@ -255,6 +317,23 @@ public class GrindstoneMenu extends AbstractContainerMenu { + } + + item.set(DataComponents.REPAIR_COST, i); ++ ++ // Purpur start ++ net.minecraft.core.component.DataComponentPatch.Builder builder = net.minecraft.core.component.DataComponentPatch.builder(); ++ if (org.purpurmc.purpur.PurpurConfig.grindstoneRemoveAttributes) { ++ item.getComponents().forEach(typedDataComponent -> { ++ if (GRINDSTONE_REMOVE_ATTRIBUTES_REMOVAL_LIST.contains(typedDataComponent.type())) { ++ builder.remove(typedDataComponent.type()); ++ } ++ }); ++ } ++ if (org.purpurmc.purpur.PurpurConfig.grindstoneRemoveDisplay) { ++ builder.remove(DataComponents.CUSTOM_NAME); ++ builder.remove(DataComponents.LORE); ++ } ++ item.applyComponents(builder.build()); ++ // Purpur end ++ + return item; + } + +@@ -316,7 +395,9 @@ public class GrindstoneMenu extends AbstractContainerMenu { + return ItemStack.EMPTY; + } + ++ this.activeQuickItem = itemstack; // Purpur + slot1.onTake(player, itemstack1); ++ this.activeQuickItem = null; // Purpur + } + + return itemstack; +diff --git a/src/main/java/net/minecraft/world/inventory/InventoryMenu.java b/src/main/java/net/minecraft/world/inventory/InventoryMenu.java +index 9992599dbe4f4a430e822a44b03c00505abfbfaf..3fea9339420aa38b303ccf6c154aec246e617b5b 100644 +--- a/src/main/java/net/minecraft/world/inventory/InventoryMenu.java ++++ b/src/main/java/net/minecraft/world/inventory/InventoryMenu.java +@@ -97,7 +97,7 @@ public class InventoryMenu extends RecipeBookMenu { + public boolean mayPickup(Player playerEntity) { + ItemStack itemstack = this.getItem(); + +- return !itemstack.isEmpty() && !playerEntity.isCreative() && EnchantmentHelper.hasBindingCurse(itemstack) ? false : super.mayPickup(playerEntity); ++ return !itemstack.isEmpty() && !playerEntity.isCreative() && EnchantmentHelper.hasBindingCurse(itemstack) ? playerEntity.level().purpurConfig.playerRemoveBindingWithWeakness && playerEntity.hasEffect(net.minecraft.world.effect.MobEffects.WEAKNESS) : super.mayPickup(playerEntity); // Purpur + } + + @Override +diff --git a/src/main/java/net/minecraft/world/inventory/ItemCombinerMenu.java b/src/main/java/net/minecraft/world/inventory/ItemCombinerMenu.java +index 7de5e47f9a54263734eeef855a2dc07ef64d30ea..7215af6cc91f48b040c23c54536d4aac8d293497 100644 +--- a/src/main/java/net/minecraft/world/inventory/ItemCombinerMenu.java ++++ b/src/main/java/net/minecraft/world/inventory/ItemCombinerMenu.java +@@ -178,7 +178,9 @@ public abstract class ItemCombinerMenu extends AbstractContainerMenu { + return ItemStack.EMPTY; + } + ++ this.activeQuickItem = itemstack; // Purpur + slot1.onTake(player, itemstack1); ++ this.activeQuickItem = null; // Purpur + } + + return itemstack; +diff --git a/src/main/java/net/minecraft/world/inventory/PlayerEnderChestContainer.java b/src/main/java/net/minecraft/world/inventory/PlayerEnderChestContainer.java +index a15d5ff872dbd77f3c3145e0328f3d02e431ff8c..1dcf36d502990d32fc4cd3ea69c3ea334baed69a 100644 +--- a/src/main/java/net/minecraft/world/inventory/PlayerEnderChestContainer.java ++++ b/src/main/java/net/minecraft/world/inventory/PlayerEnderChestContainer.java +@@ -31,11 +31,18 @@ public class PlayerEnderChestContainer extends SimpleContainer { + } + + public PlayerEnderChestContainer(Player owner) { +- super(27); ++ super(org.purpurmc.purpur.PurpurConfig.enderChestSixRows ? 54 : 27); // Purpur + this.owner = owner; + // CraftBukkit end + } + ++ // Purpur start ++ @Override ++ public int getContainerSize() { ++ return owner.sixRowEnderchestSlotCount < 0 ? super.getContainerSize() : owner.sixRowEnderchestSlotCount; ++ } ++ // Purpur end ++ + public void setActiveChest(EnderChestBlockEntity blockEntity) { + this.activeChest = blockEntity; + } +diff --git a/src/main/java/net/minecraft/world/item/ArmorItem.java b/src/main/java/net/minecraft/world/item/ArmorItem.java +index 786e4a8700cb84b16dd9b8892a0d1d5803924d81..b108ca4c7900ccf6a14ebea01c21c103459054f8 100644 +--- a/src/main/java/net/minecraft/world/item/ArmorItem.java ++++ b/src/main/java/net/minecraft/world/item/ArmorItem.java +@@ -69,7 +69,7 @@ public class ArmorItem extends Item implements Equipable { + return false; + } else { + LivingEntity entityliving = (LivingEntity) list.get(0); +- EquipmentSlot enumitemslot = Mob.getEquipmentSlotForItem(armor); ++ EquipmentSlot enumitemslot = pointer.level().purpurConfig.dispenserApplyCursedArmor ? Mob.getEquipmentSlotForItem(armor) : Mob.getSlotForDispenser(armor); if (enumitemslot == null) return false; // Purpur + ItemStack itemstack1 = armor.copyWithCount(1); // Paper - shrink below and single item in event + // CraftBukkit start + Level world = pointer.level(); +diff --git a/src/main/java/net/minecraft/world/item/ArmorStandItem.java b/src/main/java/net/minecraft/world/item/ArmorStandItem.java +index 1634a7d5ff06583408cf2f02f2b5f90931b1e02a..dfe8473a880cbddfc3ac6a9c97f1a500624eeb38 100644 +--- a/src/main/java/net/minecraft/world/item/ArmorStandItem.java ++++ b/src/main/java/net/minecraft/world/item/ArmorStandItem.java +@@ -58,6 +58,14 @@ public class ArmorStandItem extends Item { + return InteractionResult.FAIL; + } + // CraftBukkit end ++ // Purpur start ++ if (!world.purpurConfig.persistentDroppableEntityDisplayNames) { ++ entityarmorstand.setCustomName(null); ++ } ++ if (world.purpurConfig.armorstandSetNameVisible) { ++ entityarmorstand.setCustomNameVisible(true); ++ } ++ // Purpur end + worldserver.addFreshEntityWithPassengers(entityarmorstand); + world.playSound((Player) null, entityarmorstand.getX(), entityarmorstand.getY(), entityarmorstand.getZ(), SoundEvents.ARMOR_STAND_PLACE, SoundSource.BLOCKS, 0.75F, 0.8F); + entityarmorstand.gameEvent(GameEvent.ENTITY_PLACE, context.getPlayer()); +diff --git a/src/main/java/net/minecraft/world/item/AxeItem.java b/src/main/java/net/minecraft/world/item/AxeItem.java +index 9fd2d97ff0e05578a3e6a0b86dc1974691845c5d..58343722399404530d497648155dbc254d6a865a 100644 +--- a/src/main/java/net/minecraft/world/item/AxeItem.java ++++ b/src/main/java/net/minecraft/world/item/AxeItem.java +@@ -56,13 +56,15 @@ public class AxeItem extends DiggerItem { + Level level = context.getLevel(); + BlockPos blockPos = context.getClickedPos(); + Player player = context.getPlayer(); +- Optional optional = this.evaluateNewBlockState(level, blockPos, player, level.getBlockState(blockPos)); ++ Optional optional = this.evaluateActionable(level, blockPos, player, level.getBlockState(blockPos)); // Purpur + if (optional.isEmpty()) { + return InteractionResult.PASS; + } else { ++ org.purpurmc.purpur.tool.Actionable actionable = optional.get(); // Purpur ++ BlockState state = actionable.into().withPropertiesOf(level.getBlockState(blockPos)); // Purpur + ItemStack itemStack = context.getItemInHand(); + // Paper start - EntityChangeBlockEvent +- if (!org.bukkit.craftbukkit.event.CraftEventFactory.callEntityChangeBlockEvent(player, blockPos, optional.get())) { ++ if (!org.bukkit.craftbukkit.event.CraftEventFactory.callEntityChangeBlockEvent(player, blockPos, state)) { // Purpur + return InteractionResult.PASS; + } + // Paper end +@@ -70,32 +72,41 @@ public class AxeItem extends DiggerItem { + CriteriaTriggers.ITEM_USED_ON_BLOCK.trigger((ServerPlayer)player, blockPos, itemStack); + } + +- level.setBlock(blockPos, optional.get(), 11); +- level.gameEvent(GameEvent.BLOCK_CHANGE, blockPos, GameEvent.Context.of(player, optional.get())); ++ // Purpur start ++ level.setBlock(blockPos, state, 11); ++ actionable.drops().forEach((drop, chance) -> { ++ if (level.random.nextDouble() < chance) { ++ Block.popResourceFromFace(level, blockPos, context.getClickedFace(), new ItemStack(drop)); ++ } ++ }); ++ level.gameEvent(GameEvent.BLOCK_CHANGE, blockPos, GameEvent.Context.of(player, state)); ++ // Purpur end + if (player != null) { + itemStack.hurtAndBreak(1, player, LivingEntity.getSlotForHand(context.getHand())); + } + +- return InteractionResult.sidedSuccess(level.isClientSide); ++ return InteractionResult.SUCCESS; // Purpur - force arm swing + } + } + +- private Optional evaluateNewBlockState(Level world, BlockPos pos, @Nullable Player player, BlockState state) { +- Optional optional = this.getStripped(state); ++ private Optional evaluateActionable(Level world, BlockPos pos, @Nullable Player player, BlockState state) { // Purpur ++ Optional optional = Optional.ofNullable(world.purpurConfig.axeStrippables.get(state.getBlock())); // Purpur + if (optional.isPresent()) { +- world.playSound(player, pos, SoundEvents.AXE_STRIP, SoundSource.BLOCKS, 1.0F, 1.0F); ++ world.playSound(STRIPPABLES.containsKey(state.getBlock()) ? player : null, pos, SoundEvents.AXE_STRIP, SoundSource.BLOCKS, 1.0F, 1.0F); // Purpur - force sound + return optional; + } else { +- Optional optional2 = WeatheringCopper.getPrevious(state); ++ Optional optional2 = Optional.ofNullable(world.purpurConfig.axeWeatherables.get(state.getBlock())); // Purpur + if (optional2.isPresent()) { +- world.playSound(player, pos, SoundEvents.AXE_SCRAPE, SoundSource.BLOCKS, 1.0F, 1.0F); ++ world.playSound(WeatheringCopper.getPrevious(state).isPresent() ? player : null, pos, SoundEvents.AXE_SCRAPE, SoundSource.BLOCKS, 1.0F, 1.0F); // Purpur - force sound + world.levelEvent(player, 3005, pos, 0); + return optional2; + } else { +- Optional optional3 = Optional.ofNullable(HoneycombItem.WAX_OFF_BY_BLOCK.get().get(state.getBlock())) +- .map(block -> block.withPropertiesOf(state)); ++ // Purpur start ++ Optional optional3 = Optional.ofNullable(world.purpurConfig.axeWaxables.get(state.getBlock())); ++ // .map(block -> block.withPropertiesOf(state)); ++ // Purpur end + if (optional3.isPresent()) { +- world.playSound(player, pos, SoundEvents.AXE_WAX_OFF, SoundSource.BLOCKS, 1.0F, 1.0F); ++ world.playSound(HoneycombItem.WAX_OFF_BY_BLOCK.get().containsKey(state.getBlock()) ? player : null, pos, SoundEvents.AXE_WAX_OFF, SoundSource.BLOCKS, 1.0F, 1.0F); // Purpur - force sound + world.levelEvent(player, 3004, pos, 0); + return optional3; + } else { +diff --git a/src/main/java/net/minecraft/world/item/BlockItem.java b/src/main/java/net/minecraft/world/item/BlockItem.java +index 96fb69ec6db2e7c8c728435f0c537b076259b2fb..2649188930653610b8aaaeb18797c80879cd572a 100644 +--- a/src/main/java/net/minecraft/world/item/BlockItem.java ++++ b/src/main/java/net/minecraft/world/item/BlockItem.java +@@ -219,6 +219,7 @@ public class BlockItem extends Item { + + if (tileentity != null) { + if (!world.isClientSide && tileentity.onlyOpCanSetNbt() && (player == null || !(player.canUseGameMasterBlocks() || (player.getAbilities().instabuild && player.getBukkitEntity().hasPermission("minecraft.nbt.place"))))) { // Spigot - add permission ++ if (!(!world.isClientSide && world.purpurConfig.silkTouchEnabled && tileentity instanceof net.minecraft.world.level.block.entity.SpawnerBlockEntity && player.getBukkitEntity().hasPermission("purpur.drop.spawners"))) + return false; + } + +@@ -259,6 +260,7 @@ public class BlockItem extends Item { + ItemContainerContents itemcontainercontents = (ItemContainerContents) entity.getItem().set(DataComponents.CONTAINER, ItemContainerContents.EMPTY); + + if (itemcontainercontents != null) { ++ if (entity.level().purpurConfig.shulkerBoxItemDropContentsWhenDestroyed && entity.getItem().is(Items.SHULKER_BOX)) // Purpur + ItemUtils.onContainerDestroyed(entity, itemcontainercontents.nonEmptyItemsCopy()); + } + +diff --git a/src/main/java/net/minecraft/world/item/BoatItem.java b/src/main/java/net/minecraft/world/item/BoatItem.java +index eb74d45ad458b80cf8455297c3bc550186adaea3..ef01856c487e4ab982996e01537618233592ac32 100644 +--- a/src/main/java/net/minecraft/world/item/BoatItem.java ++++ b/src/main/java/net/minecraft/world/item/BoatItem.java +@@ -72,6 +72,11 @@ public class BoatItem extends Item { + + entityboat.setVariant(this.type); + entityboat.setYRot(user.getYRot()); ++ // Purpur start ++ if (!world.purpurConfig.persistentDroppableEntityDisplayNames) { ++ entityboat.setCustomName(null); ++ } ++ // Purpur end + if (!world.noCollision(entityboat, entityboat.getBoundingBox())) { + return InteractionResultHolder.fail(itemstack); + } else { +diff --git a/src/main/java/net/minecraft/world/item/BowItem.java b/src/main/java/net/minecraft/world/item/BowItem.java +index 5ca843df5b4caa668953e5e36a9b20fabeb35046..ff39d3614f360918d74b54b817bc227f89d34c9c 100644 +--- a/src/main/java/net/minecraft/world/item/BowItem.java ++++ b/src/main/java/net/minecraft/world/item/BowItem.java +@@ -29,9 +29,9 @@ public class BowItem extends ProjectileWeaponItem { + int i = this.getUseDuration(stack) - remainingUseTicks; + float f = getPowerForTime(i); + if (!((double)f < 0.1)) { +- List list = draw(stack, itemStack, player); ++ List list = draw(stack, itemStack, player, !((itemStack.is(Items.ARROW) && world.purpurConfig.infinityWorksWithNormalArrows) || (itemStack.is(Items.TIPPED_ARROW) && world.purpurConfig.infinityWorksWithTippedArrows) || (itemStack.is(Items.SPECTRAL_ARROW) && world.purpurConfig.infinityWorksWithSpectralArrows))); + if (!world.isClientSide() && !list.isEmpty()) { +- this.shoot(world, player, player.getUsedItemHand(), stack, list, f * 3.0F, 1.0F, f == 1.0F, null); ++ this.shoot(world, player, player.getUsedItemHand(), stack, list, f * 3.0F, (float) world.purpurConfig.bowProjectileOffset, f == 1.0F, null); // Purpur + } + + world.playSound( +@@ -81,7 +81,7 @@ public class BowItem extends ProjectileWeaponItem { + public InteractionResultHolder use(Level world, Player user, InteractionHand hand) { + ItemStack itemStack = user.getItemInHand(hand); + boolean bl = !user.getProjectile(itemStack).isEmpty(); +- if (!user.hasInfiniteMaterials() && !bl) { ++ if (!(world.purpurConfig.infinityWorksWithoutArrows && net.minecraft.world.item.enchantment.EnchantmentHelper.getItemEnchantmentLevel(net.minecraft.world.item.enchantment.Enchantments.INFINITY, itemStack) > 0) && !user.hasInfiniteMaterials() && !bl) { + return InteractionResultHolder.fail(itemStack); + } else { + user.startUsingItem(hand); +diff --git a/src/main/java/net/minecraft/world/item/BucketItem.java b/src/main/java/net/minecraft/world/item/BucketItem.java +index 49557d6f22c5725c663a231deab019d4f6fe95fa..046652e8f9c5dcdf7c90acb9391214cac46bd7d8 100644 +--- a/src/main/java/net/minecraft/world/item/BucketItem.java ++++ b/src/main/java/net/minecraft/world/item/BucketItem.java +@@ -194,7 +194,7 @@ public class BucketItem extends Item implements DispensibleContainerItem { + // CraftBukkit end + if (!flag2) { + return movingobjectpositionblock != null && this.emptyContents(entityhuman, world, movingobjectpositionblock.getBlockPos().relative(movingobjectpositionblock.getDirection()), (BlockHitResult) null, enumdirection, clicked, itemstack, enumhand); // CraftBukkit +- } else if (world.dimensionType().ultraWarm() && this.content.is(FluidTags.WATER)) { ++ } else if ((world.dimensionType().ultraWarm() || (world.isTheEnd() && !org.purpurmc.purpur.PurpurConfig.allowWaterPlacementInTheEnd)) && this.content.is(FluidTags.WATER)) { // Purpur + int i = blockposition.getX(); + int j = blockposition.getY(); + int k = blockposition.getZ(); +@@ -202,7 +202,7 @@ public class BucketItem extends Item implements DispensibleContainerItem { + world.playSound(entityhuman, blockposition, SoundEvents.FIRE_EXTINGUISH, SoundSource.BLOCKS, 0.5F, 2.6F + (world.random.nextFloat() - world.random.nextFloat()) * 0.8F); + + for (int l = 0; l < 8; ++l) { +- world.addParticle(ParticleTypes.LARGE_SMOKE, (double) i + Math.random(), (double) j + Math.random(), (double) k + Math.random(), 0.0D, 0.0D, 0.0D); ++ ((ServerLevel) world).sendParticles(null, ParticleTypes.LARGE_SMOKE, (double) i + Math.random(), (double) j + Math.random(), (double) k + Math.random(), 1, 0.0D, 0.0D, 0.0D, 0.0D, true); // Purpur + } + + return true; +diff --git a/src/main/java/net/minecraft/world/item/CrossbowItem.java b/src/main/java/net/minecraft/world/item/CrossbowItem.java +index 0f6504a7160bc304b3af554f8740c65e2987cd37..9a8092602c96ddd77c8e6fcfe7a6f5ed733023a2 100644 +--- a/src/main/java/net/minecraft/world/item/CrossbowItem.java ++++ b/src/main/java/net/minecraft/world/item/CrossbowItem.java +@@ -60,7 +60,7 @@ public class CrossbowItem extends ProjectileWeaponItem { + ItemStack itemStack = user.getItemInHand(hand); + ChargedProjectiles chargedProjectiles = itemStack.get(DataComponents.CHARGED_PROJECTILES); + if (chargedProjectiles != null && !chargedProjectiles.isEmpty()) { +- this.performShooting(world, user, hand, itemStack, getShootingPower(chargedProjectiles), 1.0F, null); ++ this.performShooting(world, user, hand, itemStack, getShootingPower(chargedProjectiles), (float) world.purpurConfig.crossbowProjectileOffset, null); // Purpur + return InteractionResultHolder.consume(itemStack); + } else if (!user.getProjectile(itemStack).isEmpty()) { + this.startSoundPlayed = false; +@@ -107,7 +107,7 @@ public class CrossbowItem extends ProjectileWeaponItem { + return CrossbowItem.tryLoadProjectiles(shooter, crossbow, true); + } + private static boolean tryLoadProjectiles(LivingEntity shooter, ItemStack crossbow, boolean consume) { +- List list = draw(crossbow, shooter.getProjectile(crossbow), shooter, consume); ++ List list = draw(crossbow, shooter.getProjectile(crossbow), shooter, (org.purpurmc.purpur.PurpurConfig.allowCrossbowInfinity && EnchantmentHelper.getItemEnchantmentLevel(Enchantments.INFINITY, crossbow) > 0) || consume); + // Paper end - Add EntityLoadCrossbowEvent + if (!list.isEmpty()) { + crossbow.set(DataComponents.CHARGED_PROJECTILES, ChargedProjectiles.of(list)); +diff --git a/src/main/java/net/minecraft/world/item/DyeColor.java b/src/main/java/net/minecraft/world/item/DyeColor.java +index 2202798612cad53aff28c499b8909a7292a37ad5..5ed2b7d15686fc9aa6dc7c03c337433cb3ee2cbd 100644 +--- a/src/main/java/net/minecraft/world/item/DyeColor.java ++++ b/src/main/java/net/minecraft/world/item/DyeColor.java +@@ -105,4 +105,10 @@ public enum DyeColor implements StringRepresentable { + public String getSerializedName() { + return this.name; + } ++ ++ // Purpur start ++ public static DyeColor random(net.minecraft.util.RandomSource random) { ++ return values()[random.nextInt(values().length)]; ++ } ++ // Purpur end + } +diff --git a/src/main/java/net/minecraft/world/item/EggItem.java b/src/main/java/net/minecraft/world/item/EggItem.java +index 4ebd634cff286b10868e26eeb3ecf34abdcab22e..7dc811335caa46870d1d895899a1e6c21980382d 100644 +--- a/src/main/java/net/minecraft/world/item/EggItem.java ++++ b/src/main/java/net/minecraft/world/item/EggItem.java +@@ -27,7 +27,7 @@ public class EggItem extends Item implements ProjectileItem { + ThrownEgg entityegg = new ThrownEgg(world, user); + + entityegg.setItem(itemstack); +- entityegg.shootFromRotation(user, user.getXRot(), user.getYRot(), 0.0F, 1.5F, 1.0F); ++ entityegg.shootFromRotation(user, user.getXRot(), user.getYRot(), 0.0F, 1.5F, (float) world.purpurConfig.eggProjectileOffset); // Purpur + // Paper start - PlayerLaunchProjectileEvent + com.destroystokyo.paper.event.player.PlayerLaunchProjectileEvent event = new com.destroystokyo.paper.event.player.PlayerLaunchProjectileEvent((org.bukkit.entity.Player) user.getBukkitEntity(), org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(itemstack), (org.bukkit.entity.Projectile) entityegg.getBukkitEntity()); + if (event.callEvent() && world.addFreshEntity(entityegg)) { +diff --git a/src/main/java/net/minecraft/world/item/EndCrystalItem.java b/src/main/java/net/minecraft/world/item/EndCrystalItem.java +index dd1bdb4bb87a3a59c229ba76b36841d199717624..54607cea2622f259aedfe425b60e2317e4167bf9 100644 +--- a/src/main/java/net/minecraft/world/item/EndCrystalItem.java ++++ b/src/main/java/net/minecraft/world/item/EndCrystalItem.java +@@ -27,7 +27,7 @@ public class EndCrystalItem extends Item { + BlockPos blockposition = context.getClickedPos(); + BlockState iblockdata = world.getBlockState(blockposition); + +- if (!iblockdata.is(Blocks.OBSIDIAN) && !iblockdata.is(Blocks.BEDROCK)) { ++ if (!world.purpurConfig.endCrystalPlaceAnywhere && !iblockdata.is(Blocks.OBSIDIAN) && !iblockdata.is(Blocks.BEDROCK)) { + return InteractionResult.FAIL; + } else { + BlockPos blockposition1 = blockposition.above(); final BlockPos aboveBlockPosition = blockposition1; // Paper - OBFHELPER +diff --git a/src/main/java/net/minecraft/world/item/EnderpearlItem.java b/src/main/java/net/minecraft/world/item/EnderpearlItem.java +index 20a91d798d31a71b3c05efa2cc5bda55494e26cc..11b04455f09d8bfdf44499bb8359dc715c2daffd 100644 +--- a/src/main/java/net/minecraft/world/item/EnderpearlItem.java ++++ b/src/main/java/net/minecraft/world/item/EnderpearlItem.java +@@ -24,7 +24,7 @@ public class EnderpearlItem extends Item { + ThrownEnderpearl entityenderpearl = new ThrownEnderpearl(world, user); + + entityenderpearl.setItem(itemstack); +- entityenderpearl.shootFromRotation(user, user.getXRot(), user.getYRot(), 0.0F, 1.5F, 1.0F); ++ entityenderpearl.shootFromRotation(user, user.getXRot(), user.getYRot(), 0.0F, 1.5F, (float) world.purpurConfig.enderPearlProjectileOffset); // Purpur + // Paper start - PlayerLaunchProjectileEvent + com.destroystokyo.paper.event.player.PlayerLaunchProjectileEvent event = new com.destroystokyo.paper.event.player.PlayerLaunchProjectileEvent((org.bukkit.entity.Player) user.getBukkitEntity(), org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(itemstack), (org.bukkit.entity.Projectile) entityenderpearl.getBukkitEntity()); + if (event.callEvent() && world.addFreshEntity(entityenderpearl)) { +@@ -36,7 +36,7 @@ public class EnderpearlItem extends Item { + + world.playSound((Player) null, user.getX(), user.getY(), user.getZ(), SoundEvents.ENDER_PEARL_THROW, SoundSource.NEUTRAL, 0.5F, 0.4F / (net.minecraft.world.entity.Entity.SHARED_RANDOM.nextFloat() * 0.4F + 0.8F)); + user.awardStat(Stats.ITEM_USED.get(this)); +- user.getCooldowns().addCooldown(this, 20); ++ user.getCooldowns().addCooldown(this, user.getAbilities().instabuild ? world.purpurConfig.enderPearlCooldownCreative : world.purpurConfig.enderPearlCooldown); // Purpur + } else { + // Paper end - PlayerLaunchProjectileEvent + if (user instanceof net.minecraft.server.level.ServerPlayer) { +diff --git a/src/main/java/net/minecraft/world/item/FireworkRocketItem.java b/src/main/java/net/minecraft/world/item/FireworkRocketItem.java +index 218f2f085309f04438f8b07bc41cf242583db2dc..ea8e49b42b9dde74784189430be66ed6978015dd 100644 +--- a/src/main/java/net/minecraft/world/item/FireworkRocketItem.java ++++ b/src/main/java/net/minecraft/world/item/FireworkRocketItem.java +@@ -65,6 +65,14 @@ public class FireworkRocketItem extends Item implements ProjectileItem { + com.destroystokyo.paper.event.player.PlayerElytraBoostEvent event = new com.destroystokyo.paper.event.player.PlayerElytraBoostEvent((org.bukkit.entity.Player) user.getBukkitEntity(), org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(itemStack), (org.bukkit.entity.Firework) fireworkRocketEntity.getBukkitEntity(), org.bukkit.craftbukkit.CraftEquipmentSlot.getHand(hand)); + if (event.callEvent() && world.addFreshEntity(fireworkRocketEntity)) { + user.awardStat(Stats.ITEM_USED.get(this)); ++ // Purpur start ++ if (world.purpurConfig.elytraDamagePerFireworkBoost > 0) { ++ ItemStack chestItem = user.getItemBySlot(net.minecraft.world.entity.EquipmentSlot.CHEST); ++ if (chestItem.getItem() == Items.ELYTRA) { ++ chestItem.hurtAndBreak(world.purpurConfig.elytraDamagePerFireworkBoost, user, net.minecraft.world.entity.EquipmentSlot.CHEST); ++ } ++ } ++ // Purpur end + if (event.shouldConsume() && !user.hasInfiniteMaterials()) { + itemStack.shrink(1); + } else ((net.minecraft.server.level.ServerPlayer) user).getBukkitEntity().updateInventory(); +diff --git a/src/main/java/net/minecraft/world/item/HangingEntityItem.java b/src/main/java/net/minecraft/world/item/HangingEntityItem.java +index 530167ce8e5bb72a418f8ec61411e38a5892fd72..35dc7546793dba34bf6debad3f214f61a8fb4f4e 100644 +--- a/src/main/java/net/minecraft/world/item/HangingEntityItem.java ++++ b/src/main/java/net/minecraft/world/item/HangingEntityItem.java +@@ -73,6 +73,11 @@ public class HangingEntityItem extends Item { + + if (!customdata.isEmpty()) { + EntityType.updateCustomEntityTag(world, entityhuman, (Entity) object, customdata); ++ // Purpur start ++ if (!world.purpurConfig.persistentDroppableEntityDisplayNames) { ++ ((Entity) object).setCustomName(null); ++ } ++ // Purpur end + } + + if (((HangingEntity) object).survives()) { +diff --git a/src/main/java/net/minecraft/world/item/HoeItem.java b/src/main/java/net/minecraft/world/item/HoeItem.java +index 06497b5141e611cc7a1b6030a7b9c54b5c4eda06..28df1b3230762e52b5458ac93a85c9a5d41eb6a6 100644 +--- a/src/main/java/net/minecraft/world/item/HoeItem.java ++++ b/src/main/java/net/minecraft/world/item/HoeItem.java +@@ -46,15 +46,23 @@ public class HoeItem extends DiggerItem { + public InteractionResult useOn(UseOnContext context) { + Level level = context.getLevel(); + BlockPos blockPos = context.getClickedPos(); +- Pair, Consumer> pair = TILLABLES.get(level.getBlockState(blockPos).getBlock()); +- if (pair == null) { +- return InteractionResult.PASS; +- } else { +- Predicate predicate = pair.getFirst(); +- Consumer consumer = pair.getSecond(); ++ // Purpur start ++ Block clickedBlock = level.getBlockState(blockPos).getBlock(); ++ var tillable = level.purpurConfig.hoeTillables.get(clickedBlock); ++ if (tillable == null) { return InteractionResult.PASS; } else { ++ Predicate predicate = tillable.condition().predicate(); ++ Consumer consumer = (ctx) -> { ++ level.setBlock(blockPos, tillable.into().defaultBlockState(), 11); ++ tillable.drops().forEach((drop, chance) -> { ++ if (level.random.nextDouble() < chance) { ++ Block.popResourceFromFace(level, blockPos, ctx.getClickedFace(), new ItemStack(drop)); ++ } ++ }); ++ }; ++ // Purpur end + if (predicate.test(context)) { + Player player = context.getPlayer(); +- level.playSound(player, blockPos, SoundEvents.HOE_TILL, SoundSource.BLOCKS, 1.0F, 1.0F); ++ if (!TILLABLES.containsKey(clickedBlock)) level.playSound(null, blockPos, SoundEvents.HOE_TILL, SoundSource.BLOCKS, 1.0F, 1.0F); // Purpur - force sound + if (!level.isClientSide) { + consumer.accept(context); + if (player != null) { +@@ -62,7 +70,7 @@ public class HoeItem extends DiggerItem { + } + } + +- return InteractionResult.sidedSuccess(level.isClientSide); ++ return InteractionResult.SUCCESS; // Purpur - force arm swing + } else { + return InteractionResult.PASS; + } +diff --git a/src/main/java/net/minecraft/world/item/ItemStack.java b/src/main/java/net/minecraft/world/item/ItemStack.java +index a45389d64c04cd4c2a35fbc511595be0535a8665..fb614432722ea27cd044d6cfd3c6a72f797979f1 100644 +--- a/src/main/java/net/minecraft/world/item/ItemStack.java ++++ b/src/main/java/net/minecraft/world/item/ItemStack.java +@@ -475,6 +475,7 @@ public final class ItemStack implements DataComponentHolder { + world.preventPoiUpdated = true; // CraftBukkit - SPIGOT-5710 + for (BlockState blockstate : blocks) { + blockstate.update(true, false); ++ ((CraftBlock) blockstate.getBlock()).getNMS().getBlock().forgetPlacer(); // Purpur + } + world.preventPoiUpdated = false; + +@@ -506,6 +507,7 @@ public final class ItemStack implements DataComponentHolder { + if (!(block.getBlock() instanceof BaseEntityBlock)) { // Containers get placed automatically + block.onPlace(world, newblockposition, oldBlock, true, context); // Paper - pass context + } ++ block.getBlock().forgetPlacer(); // Purpur + + world.notifyAndUpdatePhysics(newblockposition, null, oldBlock, block, world.getBlockState(newblockposition), updateFlag, 512); // send null chunk as chunk.k() returns false by this point + } +@@ -636,6 +638,16 @@ public final class ItemStack implements DataComponentHolder { + return this.isDamageableItem() && this.getDamageValue() > 0; + } + ++ // Purpur start ++ public float getDamagePercent() { ++ if (isDamaged()) { ++ return (float) getDamageValue() / (float) getMaxDamage(); ++ } else { ++ return 0F; ++ } ++ } ++ // Purpur end ++ + public int getDamageValue() { + return Mth.clamp((Integer) this.getOrDefault(DataComponents.DAMAGE, 0), 0, this.getMaxDamage()); + } +@@ -653,7 +665,7 @@ public final class ItemStack implements DataComponentHolder { + int j; + + if (amount > 0) { +- j = EnchantmentHelper.getItemEnchantmentLevel(Enchantments.UNBREAKING, this); ++ j = (getItem() == Items.ELYTRA && player != null && player.level().purpurConfig.elytraIgnoreUnbreaking) ? 0 : EnchantmentHelper.getItemEnchantmentLevel(Enchantments.UNBREAKING, this); + int k = 0; + + for (int l = 0; j > 0 && l < amount; ++l) { +@@ -729,6 +741,12 @@ public final class ItemStack implements DataComponentHolder { + this.hurtAndBreak(amount, randomsource, entity, () -> { // Paper - Add EntityDamageItemEvent + entity.broadcastBreakEvent(slot); + Item item = this.getItem(); ++ // Purpur start ++ if (item == Items.ELYTRA) { ++ setDamageValue(getMaxDamage() - 1); ++ return; ++ } ++ // Purpur end + // CraftBukkit start - Check for item breaking + if (this.count == 1 && entity instanceof net.minecraft.world.entity.player.Player) { + org.bukkit.craftbukkit.event.CraftEventFactory.callPlayerItemBreakEvent((net.minecraft.world.entity.player.Player) entity, this); +@@ -1210,6 +1228,16 @@ public final class ItemStack implements DataComponentHolder { + return !((ItemEnchantments) this.getOrDefault(DataComponents.ENCHANTMENTS, ItemEnchantments.EMPTY)).isEmpty(); + } + ++ // Purpur start ++ public boolean hasEnchantment(Enchantment enchantment) { ++ if (isEnchanted()) { ++ ItemEnchantments enchantments = this.getOrDefault(DataComponents.ENCHANTMENTS, ItemEnchantments.EMPTY); ++ return new net.minecraft.advancements.critereon.EnchantmentPredicate(enchantment, net.minecraft.advancements.critereon.MinMaxBounds.Ints.atLeast(1)).containedIn(enchantments); ++ } ++ return false; ++ } ++ // Purpur end ++ + public ItemEnchantments getEnchantments() { + return (ItemEnchantments) this.getOrDefault(DataComponents.ENCHANTMENTS, ItemEnchantments.EMPTY); + } +diff --git a/src/main/java/net/minecraft/world/item/Items.java b/src/main/java/net/minecraft/world/item/Items.java +index d00b59efb754594cf532f8598f4b6d3b29693232..42b322879629afb2d2fc64a215f010f5d5ce9e02 100644 +--- a/src/main/java/net/minecraft/world/item/Items.java ++++ b/src/main/java/net/minecraft/world/item/Items.java +@@ -338,7 +338,7 @@ public class Items { + public static final Item PURPUR_BLOCK = registerBlock(Blocks.PURPUR_BLOCK); + public static final Item PURPUR_PILLAR = registerBlock(Blocks.PURPUR_PILLAR); + public static final Item PURPUR_STAIRS = registerBlock(Blocks.PURPUR_STAIRS); +- public static final Item SPAWNER = registerBlock(Blocks.SPAWNER); ++ public static final Item SPAWNER = registerBlock(new org.purpurmc.purpur.item.SpawnerItem(Blocks.SPAWNER, new Item.Properties().rarity(Rarity.EPIC))); // Purpur + public static final Item CHEST = registerBlock(Blocks.CHEST, settings -> settings.component(DataComponents.CONTAINER, ItemContainerContents.EMPTY)); + public static final Item CRAFTING_TABLE = registerBlock(Blocks.CRAFTING_TABLE); + public static final Item FARMLAND = registerBlock(Blocks.FARMLAND); +@@ -1909,7 +1909,7 @@ public class Items { + "sweet_berries", new ItemNameBlockItem(Blocks.SWEET_BERRY_BUSH, new Item.Properties().food(Foods.SWEET_BERRIES)) + ); + public static final Item GLOW_BERRIES = registerItem( +- "glow_berries", new ItemNameBlockItem(Blocks.CAVE_VINES, new Item.Properties().food(Foods.GLOW_BERRIES)) ++ "glow_berries", new org.purpurmc.purpur.item.GlowBerryItem(Blocks.CAVE_VINES, new Item.Properties().food(Foods.GLOW_BERRIES)) // Purpur + ); + public static final Item CAMPFIRE = registerBlock(Blocks.CAMPFIRE, settings -> settings.component(DataComponents.CONTAINER, ItemContainerContents.EMPTY)); + public static final Item SOUL_CAMPFIRE = registerBlock( +diff --git a/src/main/java/net/minecraft/world/item/MapItem.java b/src/main/java/net/minecraft/world/item/MapItem.java +index ce461b1a8d7fab87ae28e30205f6fab67f1808b6..608390ed36710a419de1542b80340dd3fcc7299c 100644 +--- a/src/main/java/net/minecraft/world/item/MapItem.java ++++ b/src/main/java/net/minecraft/world/item/MapItem.java +@@ -195,6 +195,7 @@ public class MapItem extends ComplexItem { + public static void renderBiomePreviewMap(ServerLevel world, ItemStack map) { + MapItemSavedData mapItemSavedData = getSavedData(map, world); + if (mapItemSavedData != null) { ++ mapItemSavedData.isExplorerMap = true; // Purpur + if (world.dimension() == mapItemSavedData.dimension) { + int i = 1 << mapItemSavedData.scale; + int j = mapItemSavedData.centerX; +diff --git a/src/main/java/net/minecraft/world/item/MilkBucketItem.java b/src/main/java/net/minecraft/world/item/MilkBucketItem.java +index 0f83ae4b0d5f52ff9ccfff6bbcc31153d45bd619..d0751274e89042715cab8e9e72387042356e3244 100644 +--- a/src/main/java/net/minecraft/world/item/MilkBucketItem.java ++++ b/src/main/java/net/minecraft/world/item/MilkBucketItem.java +@@ -26,7 +26,9 @@ public class MilkBucketItem extends Item { + + stack.consume(1, user); + if (!world.isClientSide) { ++ net.minecraft.world.effect.MobEffectInstance badOmen = user.getEffect(net.minecraft.world.effect.MobEffects.BAD_OMEN); // Purpur + user.removeAllEffects(org.bukkit.event.entity.EntityPotionEffectEvent.Cause.MILK); // CraftBukkit ++ if (!world.purpurConfig.milkCuresBadOmen && badOmen != null) user.addEffect(badOmen); // Purpur + } + + return stack.isEmpty() ? new ItemStack(Items.BUCKET) : stack; +diff --git a/src/main/java/net/minecraft/world/item/MinecartItem.java b/src/main/java/net/minecraft/world/item/MinecartItem.java +index 66074445d3908b9bb1c8d70e1e27d057720ec8e5..0fd4f2ab929df479360755a3f1e58a933ae59520 100644 +--- a/src/main/java/net/minecraft/world/item/MinecartItem.java ++++ b/src/main/java/net/minecraft/world/item/MinecartItem.java +@@ -120,8 +120,9 @@ public class MinecartItem extends Item { + BlockState iblockdata = world.getBlockState(blockposition); + + if (!iblockdata.is(BlockTags.RAILS)) { +- return InteractionResult.FAIL; +- } else { ++ if (!world.purpurConfig.minecartPlaceAnywhere) return InteractionResult.FAIL; ++ if (iblockdata.isSolid()) blockposition = blockposition.relative(context.getClickedFace()); ++ } // else { // Purpur - place minecarts anywhere + ItemStack itemstack = context.getItemInHand(); + + if (world instanceof ServerLevel) { +@@ -146,6 +147,6 @@ public class MinecartItem extends Item { + + itemstack.shrink(1); + return InteractionResult.sidedSuccess(world.isClientSide); +- } ++ // } // Purpur - place minecarts anywhere + } + } +diff --git a/src/main/java/net/minecraft/world/item/NameTagItem.java b/src/main/java/net/minecraft/world/item/NameTagItem.java +index 774c982f28b4f127fc3f036c19dfb47fb9ae3264..d49b60e7e643498b49c03593dc0da2f8e4459902 100644 +--- a/src/main/java/net/minecraft/world/item/NameTagItem.java ++++ b/src/main/java/net/minecraft/world/item/NameTagItem.java +@@ -23,6 +23,7 @@ public class NameTagItem extends Item { + if (!event.callEvent()) return InteractionResult.PASS; + LivingEntity newEntity = ((org.bukkit.craftbukkit.entity.CraftLivingEntity) event.getEntity()).getHandle(); + newEntity.setCustomName(event.getName() != null ? io.papermc.paper.adventure.PaperAdventure.asVanilla(event.getName()) : null); ++ if (user.level().purpurConfig.armorstandFixNametags && entity instanceof net.minecraft.world.entity.decoration.ArmorStand) entity.setCustomNameVisible(true); // Purpur + if (event.isPersistent() && newEntity instanceof Mob mob) { + // Paper end - Add PlayerNameEntityEvent + mob.setPersistenceRequired(); +diff --git a/src/main/java/net/minecraft/world/item/ProjectileWeaponItem.java b/src/main/java/net/minecraft/world/item/ProjectileWeaponItem.java +index f91ce87491b18f4f4ae6458192d1f320b308102a..aec96d297401b705ca2af97904739afdf1134574 100644 +--- a/src/main/java/net/minecraft/world/item/ProjectileWeaponItem.java ++++ b/src/main/java/net/minecraft/world/item/ProjectileWeaponItem.java +@@ -131,6 +131,14 @@ public abstract class ProjectileWeaponItem extends Item { + entityarrow.setPierceLevel((byte) k); + } + ++ // Purpur start ++ int lootingLevel = EnchantmentHelper.getItemEnchantmentLevel(Enchantments.LOOTING, weaponStack); ++ ++ if (lootingLevel > 0) { ++ entityarrow.setLootingLevel(lootingLevel); ++ } ++ // Purpur end ++ + return entityarrow; + } + +diff --git a/src/main/java/net/minecraft/world/item/ShovelItem.java b/src/main/java/net/minecraft/world/item/ShovelItem.java +index 24f6a158e4759aac3be8da4cf5e0d40bd295355b..6b7dbb570f8a698c87c6bce992d84d87b55176e6 100644 +--- a/src/main/java/net/minecraft/world/item/ShovelItem.java ++++ b/src/main/java/net/minecraft/world/item/ShovelItem.java +@@ -47,9 +47,12 @@ public class ShovelItem extends DiggerItem { + BlockState blockState2 = FLATTENABLES.get(blockState.getBlock()); + BlockState blockState3 = null; + Runnable afterAction = null; // Paper +- if (blockState2 != null && level.getBlockState(blockPos.above()).isAir()) { +- afterAction = () -> level.playSound(player, blockPos, SoundEvents.SHOVEL_FLATTEN, SoundSource.BLOCKS, 1.0F, 1.0F); // Paper +- blockState3 = blockState2; ++ // Purpur start ++ var flattenable = level.purpurConfig.shovelFlattenables.get(blockState.getBlock()); ++ if (flattenable != null && level.getBlockState(blockPos.above()).isAir()) { ++ afterAction = () -> {if (!FLATTENABLES.containsKey(blockState.getBlock())) level.playSound(null, blockPos, SoundEvents.SHOVEL_FLATTEN, SoundSource.BLOCKS, 1.0F, 1.0F);}; // Paper ++ blockState3 = flattenable.into().defaultBlockState(); ++ // Purpur end + } else if (blockState.getBlock() instanceof CampfireBlock && blockState.getValue(CampfireBlock.LIT)) { + afterAction = () -> { // Paper + if (!level.isClientSide()) { +@@ -76,7 +79,7 @@ public class ShovelItem extends DiggerItem { + } + } + +- return InteractionResult.sidedSuccess(level.isClientSide); ++ return InteractionResult.SUCCESS; // Purpur - force arm swing + } else { + return InteractionResult.PASS; + } +diff --git a/src/main/java/net/minecraft/world/item/SnowballItem.java b/src/main/java/net/minecraft/world/item/SnowballItem.java +index 32b170551a2f5bdc88d29f4d03750bfe3974e71b..a41fb0dd26af367fc2470a7913a84d864bc505f7 100644 +--- a/src/main/java/net/minecraft/world/item/SnowballItem.java ++++ b/src/main/java/net/minecraft/world/item/SnowballItem.java +@@ -28,7 +28,7 @@ public class SnowballItem extends Item implements ProjectileItem { + Snowball entitysnowball = new Snowball(world, user); + + entitysnowball.setItem(itemstack); +- entitysnowball.shootFromRotation(user, user.getXRot(), user.getYRot(), 0.0F, 1.5F, 1.0F); ++ entitysnowball.shootFromRotation(user, user.getXRot(), user.getYRot(), 0.0F, 1.5F, (float) world.purpurConfig.snowballProjectileOffset); // Purpur + // Paper start - PlayerLaunchProjectileEvent + com.destroystokyo.paper.event.player.PlayerLaunchProjectileEvent event = new com.destroystokyo.paper.event.player.PlayerLaunchProjectileEvent((org.bukkit.entity.Player) user.getBukkitEntity(), org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(itemstack), (org.bukkit.entity.Projectile) entitysnowball.getBukkitEntity()); + if (event.callEvent() && world.addFreshEntity(entitysnowball)) { +diff --git a/src/main/java/net/minecraft/world/item/SpawnEggItem.java b/src/main/java/net/minecraft/world/item/SpawnEggItem.java +index 9cea8da84f39bb3f687139ef213ccea358724dee..076e6858222b92f8409f1f5cad398582c1fd6bcb 100644 +--- a/src/main/java/net/minecraft/world/item/SpawnEggItem.java ++++ b/src/main/java/net/minecraft/world/item/SpawnEggItem.java +@@ -74,6 +74,15 @@ public class SpawnEggItem extends Item { + Spawner spawner = (Spawner) tileentity; + + entitytypes = this.getType(itemstack); ++ ++ // Purpur start ++ org.bukkit.block.Block bukkitBlock = world.getWorld().getBlockAt(blockposition.getX(), blockposition.getY(), blockposition.getZ()); ++ org.purpurmc.purpur.event.PlayerSetSpawnerTypeWithEggEvent event = new org.purpurmc.purpur.event.PlayerSetSpawnerTypeWithEggEvent((org.bukkit.entity.Player) context.getPlayer().getBukkitEntity(), bukkitBlock, (org.bukkit.block.CreatureSpawner) bukkitBlock.getState(), org.bukkit.entity.EntityType.fromName(entitytypes.getName())); ++ if (!event.callEvent()) { ++ return InteractionResult.FAIL; ++ } ++ entitytypes = EntityType.getFromBukkitType(event.getEntityType()); ++ // Purpur end + spawner.setEntityId(entitytypes, world.getRandom()); + world.sendBlockUpdated(blockposition, iblockdata, iblockdata, 3); + world.gameEvent((Entity) context.getPlayer(), (Holder) GameEvent.BLOCK_CHANGE, blockposition); +diff --git a/src/main/java/net/minecraft/world/item/ThrowablePotionItem.java b/src/main/java/net/minecraft/world/item/ThrowablePotionItem.java +index 369955746f4b51f69fa01103e3771dd74fc6c8f0..e6edd3a0fa2578900cdbe8bd588219dc3fd3af5f 100644 +--- a/src/main/java/net/minecraft/world/item/ThrowablePotionItem.java ++++ b/src/main/java/net/minecraft/world/item/ThrowablePotionItem.java +@@ -21,7 +21,7 @@ public class ThrowablePotionItem extends PotionItem implements ProjectileItem { + if (!world.isClientSide) { + ThrownPotion thrownPotion = new ThrownPotion(world, user); + thrownPotion.setItem(itemStack); +- thrownPotion.shootFromRotation(user, user.getXRot(), user.getYRot(), -20.0F, 0.5F, 1.0F); ++ thrownPotion.shootFromRotation(user, user.getXRot(), user.getYRot(), -20.0F, 0.5F, (float) world.purpurConfig.throwablePotionProjectileOffset); // Purpur + // Paper start - PlayerLaunchProjectileEvent + com.destroystokyo.paper.event.player.PlayerLaunchProjectileEvent event = new com.destroystokyo.paper.event.player.PlayerLaunchProjectileEvent((org.bukkit.entity.Player) user.getBukkitEntity(), org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(itemStack), (org.bukkit.entity.Projectile) thrownPotion.getBukkitEntity()); + if (event.callEvent() && world.addFreshEntity(thrownPotion)) { +diff --git a/src/main/java/net/minecraft/world/item/TridentItem.java b/src/main/java/net/minecraft/world/item/TridentItem.java +index 47de500fddb0716d142f8f5876a82a95afaa06fa..905a020dd7b365d51d5addadbbeb9555d03c5238 100644 +--- a/src/main/java/net/minecraft/world/item/TridentItem.java ++++ b/src/main/java/net/minecraft/world/item/TridentItem.java +@@ -76,11 +76,19 @@ public class TridentItem extends Item implements ProjectileItem { + if (k == 0) { + ThrownTrident entitythrowntrident = new ThrownTrident(world, entityhuman, stack); + +- entitythrowntrident.shootFromRotation(entityhuman, entityhuman.getXRot(), entityhuman.getYRot(), 0.0F, 2.5F + (float) k * 0.5F, 1.0F); ++ entitythrowntrident.shootFromRotation(entityhuman, entityhuman.getXRot(), entityhuman.getYRot(), 0.0F, 2.5F + (float) k * 0.5F, (float) world.purpurConfig.tridentProjectileOffset); // Purpur + if (entityhuman.hasInfiniteMaterials()) { + entitythrowntrident.pickup = AbstractArrow.Pickup.CREATIVE_ONLY; + } + ++ // Purpur start ++ int lootingLevel = EnchantmentHelper.getItemEnchantmentLevel(net.minecraft.world.item.enchantment.Enchantments.LOOTING, stack); ++ ++ if (lootingLevel > 0) { ++ entitythrowntrident.setLootingLevel(lootingLevel); ++ } ++ // Purpur end ++ + // CraftBukkit start + // Paper start - PlayerLaunchProjectileEvent + com.destroystokyo.paper.event.player.PlayerLaunchProjectileEvent event = new com.destroystokyo.paper.event.player.PlayerLaunchProjectileEvent((org.bukkit.entity.Player) entityhuman.getBukkitEntity(), org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(stack), (org.bukkit.entity.Projectile) entitythrowntrident.getBukkitEntity()); +@@ -123,6 +131,14 @@ public class TridentItem extends Item implements ProjectileItem { + f3 *= f6 / f5; + f4 *= f6 / f5; + org.bukkit.craftbukkit.event.CraftEventFactory.callPlayerRiptideEvent(entityhuman, stack, f2, f3, f4); // CraftBukkit ++ ++ // Purpur start ++ ItemStack chestItem = entityhuman.getItemBySlot(EquipmentSlot.CHEST); ++ if (chestItem.getItem() == Items.ELYTRA && world.purpurConfig.elytraDamagePerTridentBoost > 0) { ++ chestItem.hurtAndBreak(world.purpurConfig.elytraDamagePerTridentBoost, entityhuman, EquipmentSlot.CHEST); ++ } ++ // Purpur end ++ + entityhuman.push((double) f2, (double) f3, (double) f4); + entityhuman.startAutoSpinAttack(20); + if (entityhuman.onGround()) { +diff --git a/src/main/java/net/minecraft/world/item/crafting/Ingredient.java b/src/main/java/net/minecraft/world/item/crafting/Ingredient.java +index e314f36951e9ac15c57137e24fce8c410373130a..21dfb8e91c5427ac12133de2c05d923d87adf5ba 100644 +--- a/src/main/java/net/minecraft/world/item/crafting/Ingredient.java ++++ b/src/main/java/net/minecraft/world/item/crafting/Ingredient.java +@@ -41,6 +41,7 @@ public final class Ingredient implements Predicate { + @Nullable + private IntList stackingIds; + public boolean exact; // CraftBukkit ++ public Predicate predicate; // Purpur + public static final Codec CODEC = Ingredient.codec(true); + public static final Codec CODEC_NONEMPTY = Ingredient.codec(false); + +@@ -72,6 +73,12 @@ public final class Ingredient implements Predicate { + } else if (this.isEmpty()) { + return itemstack.isEmpty(); + } else { ++ // Purpur start ++ if (predicate != null) { ++ return predicate.test(itemstack.asBukkitCopy()); ++ } ++ // Purpur end ++ + ItemStack[] aitemstack = this.getItems(); + int i = aitemstack.length; + +diff --git a/src/main/java/net/minecraft/world/item/enchantment/ArrowInfiniteEnchantment.java b/src/main/java/net/minecraft/world/item/enchantment/ArrowInfiniteEnchantment.java +index 81cc05c929d612898609965d82454b89cd18f9f5..fa73c3d4b58ad8379963a9866d8f09e1d6abd663 100644 +--- a/src/main/java/net/minecraft/world/item/enchantment/ArrowInfiniteEnchantment.java ++++ b/src/main/java/net/minecraft/world/item/enchantment/ArrowInfiniteEnchantment.java +@@ -7,6 +7,6 @@ public class ArrowInfiniteEnchantment extends Enchantment { + + @Override + public boolean checkCompatibility(Enchantment other) { +- return !(other instanceof MendingEnchantment) && super.checkCompatibility(other); ++ return !(other instanceof MendingEnchantment) && super.checkCompatibility(other) || other instanceof MendingEnchantment && org.purpurmc.purpur.PurpurConfig.allowInfinityMending; // Purpur + } + } +diff --git a/src/main/java/net/minecraft/world/item/enchantment/EnchantmentHelper.java b/src/main/java/net/minecraft/world/item/enchantment/EnchantmentHelper.java +index d2f0463b0e74983eb2e3dfca9a268e9502b86257..6d0363cec691996be416ab22ef9d825196399158 100644 +--- a/src/main/java/net/minecraft/world/item/enchantment/EnchantmentHelper.java ++++ b/src/main/java/net/minecraft/world/item/enchantment/EnchantmentHelper.java +@@ -237,6 +237,29 @@ public class EnchantmentHelper { + return getItemEnchantmentLevel(Enchantments.CHANNELING, stack) > 0; + } + ++ // Purpur start ++ @Nullable ++ public static Map.Entry getMostDamagedEquipment(Enchantment enchantment, LivingEntity entity) { ++ Map map = enchantment.getSlotItems(entity); ++ if (map.isEmpty()) { ++ return null; ++ } ++ Map.Entry item = null; ++ float maxPercent = 0F; ++ for (Map.Entry entry : map.entrySet()) { ++ ItemStack itemstack = entry.getValue(); ++ if (!itemstack.isEmpty() && itemstack.isDamaged() && getItemEnchantmentLevel(enchantment, itemstack) > 0) { ++ float percent = itemstack.getDamagePercent(); ++ if (item == null || percent > maxPercent) { ++ item = entry; ++ maxPercent = percent; ++ } ++ } ++ } ++ return item; ++ } ++ // Purpur end ++ + @Nullable + public static java.util.Map.Entry getRandomItemWith(Enchantment enchantment, LivingEntity entity) { + return getRandomItemWith(enchantment, entity, stack -> true); +diff --git a/src/main/java/net/minecraft/world/item/enchantment/ItemEnchantments.java b/src/main/java/net/minecraft/world/item/enchantment/ItemEnchantments.java +index af18de11dd55938b6091f5ab183bd3fe4e8df152..2c741860afa1fa4d5798c68b84ec3fe13157ff09 100644 +--- a/src/main/java/net/minecraft/world/item/enchantment/ItemEnchantments.java ++++ b/src/main/java/net/minecraft/world/item/enchantment/ItemEnchantments.java +@@ -37,7 +37,7 @@ public class ItemEnchantments implements TooltipProvider { + public static final ItemEnchantments EMPTY = new ItemEnchantments(new Object2IntAVLTreeMap<>(ENCHANTMENT_ORDER), true); + // Paper end + public static final int MAX_LEVEL = 255; +- private static final Codec LEVEL_CODEC = Codec.intRange(0, 255); ++ private static final Codec LEVEL_CODEC = Codec.intRange(0, (org.purpurmc.purpur.PurpurConfig.clampEnchantLevels ? 255 : 32767)); // Purpur + private static final Codec>> LEVELS_CODEC = Codec.unboundedMap( // Paper + BuiltInRegistries.ENCHANTMENT.holderByNameCodec(), LEVEL_CODEC + ) +@@ -72,7 +72,7 @@ public class ItemEnchantments implements TooltipProvider { + + for (Entry> entry : enchantments.object2IntEntrySet()) { + int i = entry.getIntValue(); +- if (i < 0 || i > 255) { ++ if (i < 0 || i > (org.purpurmc.purpur.PurpurConfig.clampEnchantLevels ? 255 : 32767)) { // Purpur + throw new IllegalArgumentException("Enchantment " + entry.getKey() + " has invalid level " + i); + } + } +@@ -169,13 +169,13 @@ public class ItemEnchantments implements TooltipProvider { + if (level <= 0) { + this.enchantments.removeInt(enchantment.builtInRegistryHolder()); + } else { +- this.enchantments.put(enchantment.builtInRegistryHolder(), Math.min(level, 255)); ++ this.enchantments.put(enchantment.builtInRegistryHolder(), Math.min(level, (org.purpurmc.purpur.PurpurConfig.clampEnchantLevels ? 255 : 32767))); + } + } + + public void upgrade(Enchantment enchantment, int level) { + if (level > 0) { +- this.enchantments.merge(enchantment.builtInRegistryHolder(), Math.min(level, 255), Integer::max); ++ this.enchantments.merge(enchantment.builtInRegistryHolder(), Math.min(level, (org.purpurmc.purpur.PurpurConfig.clampEnchantLevels ? 255 : 32767)), Integer::max); + } + } + +diff --git a/src/main/java/net/minecraft/world/item/trading/MerchantOffer.java b/src/main/java/net/minecraft/world/item/trading/MerchantOffer.java +index 0efc8d997b34302c3e0a5d7ec73a11a940dbeefe..af157881d440b34cfe79fbc9b03cc9ef28515eb8 100644 +--- a/src/main/java/net/minecraft/world/item/trading/MerchantOffer.java ++++ b/src/main/java/net/minecraft/world/item/trading/MerchantOffer.java +@@ -131,7 +131,12 @@ public class MerchantOffer { + } + + public void updateDemand() { +- this.demand = Math.max(0, this.demand + this.uses - (this.maxUses - this.uses)); // Paper - Fix MC-163962 ++ // Purpur start ++ this.updateDemand(0); ++ } ++ public void updateDemand(int minimumDemand) { ++ this.demand = Math.max(minimumDemand, this.demand + this.uses - (this.maxUses - this.uses)); // Paper - Fix MC-163962 ++ // Purpur end + } + + public ItemStack assemble() { +diff --git a/src/main/java/net/minecraft/world/level/BaseSpawner.java b/src/main/java/net/minecraft/world/level/BaseSpawner.java +index f57e1b78204dff661ad5d3ee93a88a00330af2dc..967af8771ff8564c715d89f4b4b69b16c25add59 100644 +--- a/src/main/java/net/minecraft/world/level/BaseSpawner.java ++++ b/src/main/java/net/minecraft/world/level/BaseSpawner.java +@@ -59,6 +59,7 @@ public abstract class BaseSpawner { + } + + public boolean isNearPlayer(Level world, BlockPos pos) { ++ if (world.purpurConfig.spawnerDeactivateByRedstone && world.hasNeighborSignal(pos)) return false; // Purpur + return world.hasNearbyAlivePlayerThatAffectsSpawning((double) pos.getX() + 0.5D, (double) pos.getY() + 0.5D, (double) pos.getZ() + 0.5D, (double) this.requiredPlayerRange); // Paper - Affects Spawning API + } + +diff --git a/src/main/java/net/minecraft/world/level/EntityGetter.java b/src/main/java/net/minecraft/world/level/EntityGetter.java +index ea0aee88c7d901034427db201c1b2430f8a1d522..1f28bfb435c1e4d97da713f96c452abab3fda91a 100644 +--- a/src/main/java/net/minecraft/world/level/EntityGetter.java ++++ b/src/main/java/net/minecraft/world/level/EntityGetter.java +@@ -192,7 +192,7 @@ public interface EntityGetter { + + default boolean hasNearbyAlivePlayer(double x, double y, double z, double range) { + for (Player player : this.players()) { +- if (EntitySelector.NO_SPECTATORS.test(player) && EntitySelector.LIVING_ENTITY_STILL_ALIVE.test(player)) { ++ if (EntitySelector.NO_SPECTATORS.test(player) && EntitySelector.LIVING_ENTITY_STILL_ALIVE.test(player) && EntitySelector.notAfk.test(player)) { // Purpur + double d = player.distanceToSqr(x, y, z); + if (range < 0.0 || d < range * range) { + return true; +diff --git a/src/main/java/net/minecraft/world/level/Explosion.java b/src/main/java/net/minecraft/world/level/Explosion.java +index dc88014c4d9f172cc54e5d77b488128f9ffbc73d..f5e84bf8817e2d53557e0909d8c9e0e0e8206a16 100644 +--- a/src/main/java/net/minecraft/world/level/Explosion.java ++++ b/src/main/java/net/minecraft/world/level/Explosion.java +@@ -98,7 +98,7 @@ public class Explosion { + this.hitPlayers = Maps.newHashMap(); + this.level = world; + this.source = entity; +- this.radius = (float) Math.max(power, 0.0); // CraftBukkit - clamp bad values ++ this.radius = (float) (world == null || world.purpurConfig.explosionClampRadius ? Math.max(power, 0.0) : power); // CraftBukkit - clamp bad values // Purpur + this.x = x; + this.y = y; + this.z = z; +@@ -426,10 +426,29 @@ public class Explosion { + + public void explode() { + // CraftBukkit start +- if (this.radius < 0.1F) { ++ if ((this.level == null || this.level.purpurConfig.explosionClampRadius) && this.radius < 0.1F) { // Purpur + return; + } + // CraftBukkit end ++ ++ // Purpur start - add PreExplodeEvents ++ if(this.source != null){ ++ Location location = new Location(this.level.getWorld(), this.x, this.y, this.z); ++ if(!new org.purpurmc.purpur.event.entity.PreEntityExplodeEvent(this.source.getBukkitEntity(), location, this.blockInteraction == Explosion.BlockInteraction.DESTROY_WITH_DECAY ? 1.0F / this.radius : 1.0F).callEvent()) { ++ this.wasCanceled = true; ++ return; ++ } ++ }else { ++ Location location = new Location(this.level.getWorld(), this.x, this.y, this.z); ++ org.bukkit.block.Block block = location.getBlock(); ++ org.bukkit.block.BlockState blockState = (this.damageSource.blockState != null) ? this.damageSource.blockState : block.getState(); ++ if(!new org.purpurmc.purpur.event.PreBlockExplodeEvent(location.getBlock(), this.blockInteraction == Explosion.BlockInteraction.DESTROY_WITH_DECAY ? 1.0F / this.radius : 1.0F, blockState).callEvent()) { ++ this.wasCanceled = true; ++ return; ++ } ++ } ++ //Purpur end ++ + this.level.gameEvent(this.source, (Holder) GameEvent.EXPLODE, new Vec3(this.x, this.y, this.z)); + Set set = Sets.newHashSet(); + boolean flag = true; +@@ -681,7 +700,7 @@ public class Explosion { + } + + if (flag1) { +- this.level.getProfiler().push("explosion_blocks"); ++ //this.level.getProfiler().push("explosion_blocks"); // Purpur + List> list = new ArrayList(); + + Util.shuffle(this.toBlow, this.level.random); +@@ -759,7 +778,7 @@ public class Explosion { + Block.popResource(this.level, (BlockPos) pair.getSecond(), (ItemStack) pair.getFirst()); + } + +- this.level.getProfiler().pop(); ++ //this.level.getProfiler().pop(); // Purpur + } + + if (this.fire) { +diff --git a/src/main/java/net/minecraft/world/level/Level.java b/src/main/java/net/minecraft/world/level/Level.java +index b4ef3ad2c17168085372f1fe46809f02d9dfe74a..eda2f8cc034cf46293be1be117a60cf8b663c303 100644 +--- a/src/main/java/net/minecraft/world/level/Level.java ++++ b/src/main/java/net/minecraft/world/level/Level.java +@@ -170,6 +170,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + // Paper end - add paper world config + + public final com.destroystokyo.paper.antixray.ChunkPacketBlockController chunkPacketBlockController; // Paper - Anti-Xray ++ public final org.purpurmc.purpur.PurpurWorldConfig purpurConfig; // Purpur + public final co.aikar.timings.WorldTimingsHandler timings; // Paper + public static BlockPos lastPhysicsProblem; // Spigot + private org.spigotmc.TickLimiter entityLimiter; +@@ -187,6 +188,49 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + } + // Paper end - fix and optimise world upgrading + ++ // Purpur start ++ private com.google.common.cache.Cache playerBreedingCooldowns; ++ ++ private com.google.common.cache.Cache getNewBreedingCooldownCache() { ++ return com.google.common.cache.CacheBuilder.newBuilder().expireAfterWrite(this.purpurConfig.animalBreedingCooldownSeconds, java.util.concurrent.TimeUnit.SECONDS).build(); ++ } ++ ++ public void resetBreedingCooldowns() { ++ this.playerBreedingCooldowns = this.getNewBreedingCooldownCache(); ++ } ++ ++ public boolean hasBreedingCooldown(java.util.UUID player, Class animalType) { // Purpur ++ return this.playerBreedingCooldowns.getIfPresent(new BreedingCooldownPair(player, animalType)) != null; ++ } ++ ++ public void addBreedingCooldown(java.util.UUID player, Class animalType) { ++ this.playerBreedingCooldowns.put(new BreedingCooldownPair(player, animalType), new Object()); ++ } ++ ++ private static final class BreedingCooldownPair { ++ private final java.util.UUID playerUUID; ++ private final Class animalType; ++ ++ public BreedingCooldownPair(java.util.UUID playerUUID, Class animalType) { ++ this.playerUUID = playerUUID; ++ this.animalType = animalType; ++ } ++ ++ @Override ++ public boolean equals(Object o) { ++ if (this == o) return true; ++ if (o == null || getClass() != o.getClass()) return false; ++ BreedingCooldownPair that = (BreedingCooldownPair) o; ++ return playerUUID.equals(that.playerUUID) && animalType.equals(that.animalType); ++ } ++ ++ @Override ++ public int hashCode() { ++ return java.util.Objects.hash(playerUUID, animalType); ++ } ++ } ++ // Purpur end ++ + public CraftWorld getWorld() { + return this.world; + } +@@ -216,6 +260,8 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + protected Level(WritableLevelData worlddatamutable, ResourceKey resourcekey, RegistryAccess iregistrycustom, Holder holder, Supplier supplier, boolean flag, boolean flag1, long i, int j, org.bukkit.generator.ChunkGenerator gen, org.bukkit.generator.BiomeProvider biomeProvider, org.bukkit.World.Environment env, java.util.function.Function paperWorldConfigCreator, java.util.concurrent.Executor executor) { // Paper - create paper world config; Async-Anti-Xray: Pass executor + this.spigotConfig = new org.spigotmc.SpigotWorldConfig(((net.minecraft.world.level.storage.PrimaryLevelData) worlddatamutable).getLevelName()); // Spigot + this.paperConfig = paperWorldConfigCreator.apply(this.spigotConfig); // Paper - create paper world config ++ this.purpurConfig = new org.purpurmc.purpur.PurpurWorldConfig(((net.minecraft.world.level.storage.PrimaryLevelData) worlddatamutable).getLevelName(), env); // Purpur ++ this.playerBreedingCooldowns = this.getNewBreedingCooldownCache(); // Purpur + this.generator = gen; + this.world = new CraftWorld((ServerLevel) this, gen, biomeProvider, env); + +@@ -1257,18 +1303,18 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + } + + protected void tickBlockEntities() { +- ProfilerFiller gameprofilerfiller = this.getProfiler(); ++ //ProfilerFiller gameprofilerfiller = this.getProfiler(); // Purpur + +- gameprofilerfiller.push("blockEntities"); +- this.timings.tileEntityPending.startTiming(); // Spigot ++ //gameprofilerfiller.push("blockEntities"); // Purpur ++ //this.timings.tileEntityPending.startTiming(); // Spigot // Purpur + this.tickingBlockEntities = true; + if (!this.pendingBlockEntityTickers.isEmpty()) { + this.blockEntityTickers.addAll(this.pendingBlockEntityTickers); + this.pendingBlockEntityTickers.clear(); + } +- this.timings.tileEntityPending.stopTiming(); // Spigot ++ //this.timings.tileEntityPending.stopTiming(); // Spigot // Purpur + +- this.timings.tileEntityTick.startTiming(); // Spigot ++ //this.timings.tileEntityTick.startTiming(); // Spigot // Purpur + // Spigot start + // Iterator iterator = this.blockEntityTickers.iterator(); + boolean flag = this.tickRateManager().runsNormally(); +@@ -1297,10 +1343,10 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + } + this.blockEntityTickers.removeAll(toRemove); // Paper - Fix MC-117075 + +- this.timings.tileEntityTick.stopTiming(); // Spigot ++ //this.timings.tileEntityTick.stopTiming(); // Spigot // Purpur + this.tickingBlockEntities = false; + co.aikar.timings.TimingHistory.tileEntityTicks += this.blockEntityTickers.size(); // Paper +- gameprofilerfiller.pop(); ++ //gameprofilerfiller.pop(); // Purpur + this.spigotConfig.currentPrimedTnt = 0; // Spigot + } + +@@ -1515,7 +1561,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + + @Override + public List getEntities(@Nullable Entity except, AABB box, Predicate predicate) { +- this.getProfiler().incrementCounter("getEntities"); ++ //this.getProfiler().incrementCounter("getEntities"); // Purpur + List list = Lists.newArrayList(); + ((ServerLevel)this).getEntityLookup().getEntities(except, box, list, predicate); // Paper - optimise this call + return list; +@@ -1534,7 +1580,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + } + + public void getEntities(EntityTypeTest filter, AABB box, Predicate predicate, List result, int limit) { +- this.getProfiler().incrementCounter("getEntities"); ++ //this.getProfiler().incrementCounter("getEntities"); // Purpur + // Paper start - optimise this call + //TODO use limit + if (filter instanceof net.minecraft.world.entity.EntityType entityTypeTest) { +@@ -1789,6 +1835,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + } + + public ProfilerFiller getProfiler() { ++ //if (true || gg.pufferfish.pufferfish.PufferfishConfig.disableMethodProfiler) return net.minecraft.util.profiling.InactiveProfiler.INSTANCE; // Pufferfish // Purpur // Purpur - TODO: Pufferfish + return (ProfilerFiller) this.profiler.get(); + } + +@@ -1900,4 +1947,13 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + return null; + } + // Paper end - optimize redstone (Alternate Current) ++ // Purpur start ++ public boolean isNether() { ++ return getWorld().getEnvironment() == org.bukkit.World.Environment.NETHER; ++ } ++ ++ public boolean isTheEnd() { ++ return getWorld().getEnvironment() == org.bukkit.World.Environment.THE_END; ++ } ++ // Purpur end + } +diff --git a/src/main/java/net/minecraft/world/level/NaturalSpawner.java b/src/main/java/net/minecraft/world/level/NaturalSpawner.java +index ed8032495af9ce9c23419224814b8d27e4a97c17..0f90a6803851eba51e164772c984b1cd1193d882 100644 +--- a/src/main/java/net/minecraft/world/level/NaturalSpawner.java ++++ b/src/main/java/net/minecraft/world/level/NaturalSpawner.java +@@ -127,8 +127,8 @@ public final class NaturalSpawner { + } + + public static void spawnForChunk(ServerLevel world, LevelChunk chunk, NaturalSpawner.SpawnState info, boolean spawnAnimals, boolean spawnMonsters, boolean rareSpawn) { +- world.getProfiler().push("spawner"); +- world.timings.mobSpawn.startTiming(); // Spigot ++ //world.getProfiler().push("spawner"); // Purpur ++ //world.timings.mobSpawn.startTiming(); // Spigot // Purpur + MobCategory[] aenumcreaturetype = NaturalSpawner.SPAWNING_CATEGORIES; + int i = aenumcreaturetype.length; + +@@ -181,8 +181,8 @@ public final class NaturalSpawner { + } + } + +- world.timings.mobSpawn.stopTiming(); // Spigot +- world.getProfiler().pop(); ++ //world.timings.mobSpawn.stopTiming(); // Spigot // Purpur ++ //world.getProfiler().pop(); // Purpur + } + + // Paper start - Add mobcaps commands +@@ -253,7 +253,7 @@ public final class NaturalSpawner { + blockposition_mutableblockposition.set(l, i, i1); + double d0 = (double) l + 0.5D; + double d1 = (double) i1 + 0.5D; +- Player entityhuman = world.getNearestPlayer(d0, (double) i, d1, -1.0D, false); ++ Player entityhuman = world.getNearestPlayer(d0, (double) i, d1, -1.0D, world.purpurConfig.mobSpawningIgnoreCreativePlayers); // Purpur + + if (entityhuman != null) { + double d2 = entityhuman.distanceToSqr(d0, (double) i, d1); +diff --git a/src/main/java/net/minecraft/world/level/block/AnvilBlock.java b/src/main/java/net/minecraft/world/level/block/AnvilBlock.java +index 923357251ad950ec4f893e8771fcfa99de8a60c5..78a341ac80806f86f2ca0bd895fb091a9257519e 100644 +--- a/src/main/java/net/minecraft/world/level/block/AnvilBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/AnvilBlock.java +@@ -59,6 +59,53 @@ public class AnvilBlock extends FallingBlock { + return this.defaultBlockState().setValue(FACING, ctx.getHorizontalDirection().getClockWise()); + } + ++ // Purpur start - repairable/damageable anvils ++ @Override ++ protected net.minecraft.world.ItemInteractionResult useItemOn(final net.minecraft.world.item.ItemStack stack, final BlockState state, final Level world, final BlockPos pos, final Player player, final net.minecraft.world.InteractionHand hand, final BlockHitResult hit) { ++ if (world.purpurConfig.anvilRepairIngotsAmount > 0 && stack.is(net.minecraft.world.item.Items.IRON_INGOT)) { ++ if (stack.getCount() < world.purpurConfig.anvilRepairIngotsAmount) { ++ // not enough iron ingots, play "error" sound and consume ++ world.playSound(null, pos, net.minecraft.sounds.SoundEvents.ANVIL_HIT, net.minecraft.sounds.SoundSource.BLOCKS, 1.0F, 1.0F); ++ return net.minecraft.world.ItemInteractionResult.CONSUME; ++ } ++ if (state.is(Blocks.DAMAGED_ANVIL)) { ++ world.setBlock(pos, Blocks.CHIPPED_ANVIL.defaultBlockState().setValue(FACING, state.getValue(FACING)), 3); ++ } else if (state.is(Blocks.CHIPPED_ANVIL)) { ++ world.setBlock(pos, Blocks.ANVIL.defaultBlockState().setValue(FACING, state.getValue(FACING)), 3); ++ } else if (state.is(Blocks.ANVIL)) { ++ // anvil is already fully repaired, play "error" sound and consume ++ world.playSound(null, pos, net.minecraft.sounds.SoundEvents.ANVIL_HIT, net.minecraft.sounds.SoundSource.BLOCKS, 1.0F, 1.0F); ++ return net.minecraft.world.ItemInteractionResult.CONSUME; ++ } ++ if (!player.getAbilities().instabuild) { ++ stack.shrink(world.purpurConfig.anvilRepairIngotsAmount); ++ } ++ world.playSound(null, pos, net.minecraft.sounds.SoundEvents.ANVIL_PLACE, net.minecraft.sounds.SoundSource.BLOCKS, 1.0F, 1.0F); ++ return net.minecraft.world.ItemInteractionResult.CONSUME; ++ } ++ if (world.purpurConfig.anvilDamageObsidianAmount > 0 && stack.is(net.minecraft.world.item.Items.OBSIDIAN)) { ++ if (stack.getCount() < world.purpurConfig.anvilDamageObsidianAmount) { ++ // not enough obsidian, play "error" sound and consume ++ world.playSound(null, pos, net.minecraft.sounds.SoundEvents.ANVIL_HIT, net.minecraft.sounds.SoundSource.BLOCKS, 1.0F, 1.0F); ++ return net.minecraft.world.ItemInteractionResult.CONSUME; ++ } ++ if (state.is(Blocks.DAMAGED_ANVIL)) { ++ world.destroyBlock(pos, false); ++ } else if (state.is(Blocks.CHIPPED_ANVIL)) { ++ world.setBlock(pos, Blocks.DAMAGED_ANVIL.defaultBlockState().setValue(FACING, state.getValue(FACING)), 3); ++ } else if (state.is(Blocks.ANVIL)) { ++ world.setBlock(pos, Blocks.CHIPPED_ANVIL.defaultBlockState().setValue(FACING, state.getValue(FACING)), 3); ++ } ++ if (!player.getAbilities().instabuild) { ++ stack.shrink(world.purpurConfig.anvilDamageObsidianAmount); ++ } ++ world.playSound(null, pos, net.minecraft.sounds.SoundEvents.ANVIL_LAND, net.minecraft.sounds.SoundSource.BLOCKS, 1.0F, 1.0F); ++ return net.minecraft.world.ItemInteractionResult.CONSUME; ++ } ++ return net.minecraft.world.ItemInteractionResult.PASS_TO_DEFAULT_BLOCK_INTERACTION; ++ } ++ // Purpur end ++ + @Override + protected InteractionResult useWithoutItem(BlockState state, Level world, BlockPos pos, Player player, BlockHitResult hit) { + if (world.isClientSide) { +diff --git a/src/main/java/net/minecraft/world/level/block/AzaleaBlock.java b/src/main/java/net/minecraft/world/level/block/AzaleaBlock.java +index fad69dfc20574ab23634b14252b50929cca75b21..7082486f6b760bed2a61938f493d5124722b58e2 100644 +--- a/src/main/java/net/minecraft/world/level/block/AzaleaBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/AzaleaBlock.java +@@ -49,6 +49,20 @@ public class AzaleaBlock extends BushBlock implements BonemealableBlock { + + @Override + public void performBonemeal(ServerLevel world, RandomSource random, BlockPos pos, BlockState state) { ++ // Purpur start ++ growTree(world, random, pos, state); ++ } ++ ++ @Override ++ public void randomTick(net.minecraft.world.level.block.state.BlockState state, ServerLevel world, BlockPos pos, RandomSource random) { ++ double chance = state.getBlock() == Blocks.FLOWERING_AZALEA ? world.purpurConfig.floweringAzaleaGrowthChance : world.purpurConfig.azaleaGrowthChance; ++ if (chance > 0.0D && world.getMaxLocalRawBrightness(pos.above()) > 9 && random.nextDouble() < chance) { ++ growTree(world, random, pos, state); ++ } ++ } ++ ++ private void growTree(ServerLevel world, RandomSource random, BlockPos pos, net.minecraft.world.level.block.state.BlockState state) { ++ // Purpur end + TreeGrower.AZALEA.growTree(world, world.getChunkSource().getGenerator(), pos, state, random); + } + +diff --git a/src/main/java/net/minecraft/world/level/block/BaseCoralPlantTypeBlock.java b/src/main/java/net/minecraft/world/level/block/BaseCoralPlantTypeBlock.java +index ce9f189bdafec26360bfadd0f36a8bc2726e132b..d5465b48531fd4b4094874c135274abf985ee71a 100644 +--- a/src/main/java/net/minecraft/world/level/block/BaseCoralPlantTypeBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/BaseCoralPlantTypeBlock.java +@@ -38,6 +38,7 @@ public abstract class BaseCoralPlantTypeBlock extends Block implements SimpleWat + } + + protected static boolean scanForWater(BlockState state, BlockGetter world, BlockPos pos) { ++ if (!((net.minecraft.world.level.LevelAccessor) world).getMinecraftWorld().purpurConfig.coralDieOutsideWater) return true; // Purpur + if (state.getValue(WATERLOGGED)) { + return true; + } else { +diff --git a/src/main/java/net/minecraft/world/level/block/BedBlock.java b/src/main/java/net/minecraft/world/level/block/BedBlock.java +index f726c3839ab93cc81fee26bfeb821bead3533b5e..a4bae9631acfc363d22b89fb76965183d9dc79f1 100644 +--- a/src/main/java/net/minecraft/world/level/block/BedBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/BedBlock.java +@@ -104,7 +104,7 @@ public class BedBlock extends HorizontalDirectionalBlock implements EntityBlock + + Vec3 vec3d = pos.getCenter(); + +- world.explode((Entity) null, world.damageSources().badRespawnPointExplosion(vec3d), (ExplosionDamageCalculator) null, vec3d, 5.0F, true, Level.ExplosionInteraction.BLOCK); ++ if (world.purpurConfig.bedExplode) world.explode((Entity) null, world.damageSources().badRespawnPointExplosion(vec3d), (ExplosionDamageCalculator) null, vec3d, (float) world.purpurConfig.bedExplosionPower, world.purpurConfig.bedExplosionFire, world.purpurConfig.bedExplosionEffect); // Purpur + return InteractionResult.SUCCESS; + } else if ((Boolean) state.getValue(BedBlock.OCCUPIED)) { + if (!BedBlock.canSetSpawn(world)) return this.explodeBed(state, world, pos); // Paper - check explode first +@@ -156,7 +156,7 @@ public class BedBlock extends HorizontalDirectionalBlock implements EntityBlock + + Vec3 vec3d = blockposition.getCenter(); + +- world.explode((Entity) null, world.damageSources().badRespawnPointExplosion(vec3d, world, iblockdata, blockposition), (ExplosionDamageCalculator) null, vec3d, 5.0F, true, Level.ExplosionInteraction.BLOCK); // CraftBukkit - add state ++ if (world.purpurConfig.bedExplode) world.explode((Entity) null, world.damageSources().badRespawnPointExplosion(vec3d, world, iblockdata, blockposition), (ExplosionDamageCalculator) null, vec3d, (float) world.purpurConfig.bedExplosionPower, world.purpurConfig.bedExplosionFire, world.purpurConfig.bedExplosionEffect); // CraftBukkit - add state // Purpur + return InteractionResult.SUCCESS; + } + } +@@ -180,7 +180,7 @@ public class BedBlock extends HorizontalDirectionalBlock implements EntityBlock + + @Override + public void fallOn(Level world, BlockState state, BlockPos pos, Entity entity, float fallDistance) { +- super.fallOn(world, state, pos, entity, fallDistance * 0.5F); ++ super.fallOn(world, state, pos, entity, fallDistance); // Purpur + } + + @Override +diff --git a/src/main/java/net/minecraft/world/level/block/BigDripleafBlock.java b/src/main/java/net/minecraft/world/level/block/BigDripleafBlock.java +index 8240c32d676a88aa23dcd052ee0136767e54fb0d..372c4ab9d390d5afd98947f21c79aae06b15064d 100644 +--- a/src/main/java/net/minecraft/world/level/block/BigDripleafBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/BigDripleafBlock.java +@@ -244,7 +244,7 @@ public class BigDripleafBlock extends HorizontalDirectionalBlock implements Bone + BigDripleafBlock.playTiltSound(world, blockposition, soundeffect); + } + +- int i = BigDripleafBlock.DELAY_UNTIL_NEXT_TILT_STATE.getInt(tilt); ++ int i = world.purpurConfig.bigDripleafTiltDelay.getOrDefault(tilt, -1); // Purpur + + if (i != -1) { + world.scheduleTick(blockposition, (Block) this, i); +diff --git a/src/main/java/net/minecraft/world/level/block/Block.java b/src/main/java/net/minecraft/world/level/block/Block.java +index cf8b8c8efd1c9c81eb5f02d75bd75875eb66771f..c6ed00f89127a2857dfb9ce9e04513e5cf510dfb 100644 +--- a/src/main/java/net/minecraft/world/level/block/Block.java ++++ b/src/main/java/net/minecraft/world/level/block/Block.java +@@ -89,6 +89,10 @@ public class Block extends BlockBehaviour implements ItemLike { + public static final int UPDATE_LIMIT = 512; + protected final StateDefinition stateDefinition; + private BlockState defaultBlockState; ++ // Purpur start ++ public float fallDamageMultiplier = 1.0F; ++ public float fallDistanceMultiplier = 1.0F; ++ // Purpur end + // Paper start + public final boolean isDestroyable() { + return io.papermc.paper.configuration.GlobalConfiguration.get().unsupportedSettings.allowPermanentBlockBreakExploits || +@@ -438,7 +442,17 @@ public class Block extends BlockBehaviour implements ItemLike { + } // Paper - fix drops not preventing stats/food exhaustion + } + +- public void setPlacedBy(Level world, BlockPos pos, BlockState state, @Nullable LivingEntity placer, ItemStack itemStack) {} ++ // Purpur start ++ @Nullable protected LivingEntity placer = null; ++ ++ public void setPlacedBy(Level world, BlockPos pos, BlockState state, @Nullable LivingEntity placer, ItemStack itemStack) { ++ this.placer = placer; ++ } ++ ++ public void forgetPlacer() { ++ this.placer = null; ++ } ++ // Purpur end + + public boolean isPossibleToRespawnInThis(BlockState state) { + return !state.isSolid() && !state.liquid(); +@@ -457,7 +471,7 @@ public class Block extends BlockBehaviour implements ItemLike { + } + + public void fallOn(Level world, BlockState state, BlockPos pos, Entity entity, float fallDistance) { +- entity.causeFallDamage(fallDistance, 1.0F, entity.damageSources().fall()); ++ entity.causeFallDamage(fallDistance * fallDistanceMultiplier, fallDamageMultiplier, entity.damageSources().fall()); // Purpur + } + + public void updateEntityAfterFallOn(BlockGetter world, Entity entity) { +diff --git a/src/main/java/net/minecraft/world/level/block/Blocks.java b/src/main/java/net/minecraft/world/level/block/Blocks.java +index 260906f493416d98ab574a7262fce5e9b7e40c64..ce639e4a2d87202a10ef4fc73274c4b2c4e95720 100644 +--- a/src/main/java/net/minecraft/world/level/block/Blocks.java ++++ b/src/main/java/net/minecraft/world/level/block/Blocks.java +@@ -7389,6 +7389,7 @@ public class Blocks { + BlockBehaviour.Properties.of() + .mapColor(MapColor.PLANT) + .forceSolidOff() ++ .randomTicks() // Purpur + .instabreak() + .sound(SoundType.AZALEA) + .noOcclusion() +@@ -7401,6 +7402,7 @@ public class Blocks { + BlockBehaviour.Properties.of() + .mapColor(MapColor.PLANT) + .forceSolidOff() ++ .randomTicks() // Purpur + .instabreak() + .sound(SoundType.FLOWERING_AZALEA) + .noOcclusion() +diff --git a/src/main/java/net/minecraft/world/level/block/BushBlock.java b/src/main/java/net/minecraft/world/level/block/BushBlock.java +index a7b4b5600e3c889c69ac22294899713d50b5fe5c..a27e298ffdfa6956be9cde429d2cd45483a51fed 100644 +--- a/src/main/java/net/minecraft/world/level/block/BushBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/BushBlock.java +@@ -52,4 +52,24 @@ public abstract class BushBlock extends Block { + protected boolean isPathfindable(BlockState state, PathComputationType type) { + return type == PathComputationType.AIR && !this.hasCollision ? true : super.isPathfindable(state, type); + } ++ ++ // Purpur start ++ public void playerDestroyAndReplant(net.minecraft.world.level.Level world, net.minecraft.world.entity.player.Player player, BlockPos pos, BlockState state, @javax.annotation.Nullable net.minecraft.world.level.block.entity.BlockEntity blockEntity, net.minecraft.world.item.ItemStack itemInHand, net.minecraft.world.level.ItemLike itemToReplant) { ++ player.awardStat(net.minecraft.stats.Stats.BLOCK_MINED.get(this)); ++ player.causeFoodExhaustion(0.005F, org.bukkit.event.entity.EntityExhaustionEvent.ExhaustionReason.BLOCK_MINED); ++ java.util.List dropList = Block.getDrops(state, (net.minecraft.server.level.ServerLevel) world, pos, blockEntity, player, itemInHand); ++ ++ boolean planted = false; ++ for (net.minecraft.world.item.ItemStack itemToDrop : dropList) { ++ if (!planted && itemToDrop.getItem() == itemToReplant) { ++ world.setBlock(pos, defaultBlockState(), 3); ++ itemToDrop.setCount(itemToDrop.getCount() - 1); ++ planted = true; ++ } ++ Block.popResource(world, pos, itemToDrop); ++ } ++ ++ state.spawnAfterBreak((net.minecraft.server.level.ServerLevel) world, pos, itemInHand, true); ++ } ++ // Purpur end + } +diff --git a/src/main/java/net/minecraft/world/level/block/CactusBlock.java b/src/main/java/net/minecraft/world/level/block/CactusBlock.java +index ff4dda48116a2969704b355ff96407ba869b466e..066181ed274a492762baebf05bf51ac7848878cc 100644 +--- a/src/main/java/net/minecraft/world/level/block/CactusBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/CactusBlock.java +@@ -23,7 +23,7 @@ import net.minecraft.world.phys.shapes.CollisionContext; + import net.minecraft.world.phys.shapes.VoxelShape; + import org.bukkit.craftbukkit.event.CraftEventFactory; // CraftBukkit + +-public class CactusBlock extends Block { ++public class CactusBlock extends Block implements BonemealableBlock { // Purpur + + public static final MapCodec CODEC = simpleCodec(CactusBlock::new); + public static final IntegerProperty AGE = BlockStateProperties.AGE_15; +@@ -114,7 +114,7 @@ public class CactusBlock extends Block { + + enumdirection = (Direction) iterator.next(); + iblockdata1 = world.getBlockState(pos.relative(enumdirection)); +- } while (!iblockdata1.isSolid() && !world.getFluidState(pos.relative(enumdirection)).is(FluidTags.LAVA)); ++ } while ((!world.getWorldBorder().world.purpurConfig.cactusBreaksFromSolidNeighbors || !iblockdata1.isSolid()) && !world.getFluidState(pos.relative(enumdirection)).is(FluidTags.LAVA)); // Purpur + + return false; + } +@@ -134,4 +134,34 @@ public class CactusBlock extends Block { + protected boolean isPathfindable(BlockState state, PathComputationType type) { + return false; + } ++ ++ // Purpur start ++ @Override ++ public boolean isValidBonemealTarget(final LevelReader world, final BlockPos pos, final BlockState state) { ++ if (!((Level) world).purpurConfig.cactusAffectedByBonemeal || !world.isEmptyBlock(pos.above())) return false; ++ ++ int cactusHeight = 0; ++ while (world.getBlockState(pos.below(cactusHeight)).is(this)) { ++ cactusHeight++; ++ } ++ ++ return cactusHeight < ((Level) world).paperConfig().maxGrowthHeight.cactus; ++ } ++ ++ @Override ++ public boolean isBonemealSuccess(Level world, RandomSource random, BlockPos pos, BlockState state) { ++ return true; ++ } ++ ++ @Override ++ public void performBonemeal(ServerLevel world, RandomSource random, BlockPos pos, BlockState state) { ++ int cactusHeight = 0; ++ while (world.getBlockState(pos.below(cactusHeight)).is(this)) { ++ cactusHeight++; ++ } ++ for (int i = 0; i <= world.paperConfig().maxGrowthHeight.cactus - cactusHeight; i++) { ++ world.setBlockAndUpdate(pos.above(i), state.setValue(CactusBlock.AGE, 0)); ++ } ++ } ++ // Purpur end + } +diff --git a/src/main/java/net/minecraft/world/level/block/CampfireBlock.java b/src/main/java/net/minecraft/world/level/block/CampfireBlock.java +index d6fffb0953494e8667cc456137cac0f5deebfbb6..f7a2244b998aebe354d38eec7aa22fd94ce404c9 100644 +--- a/src/main/java/net/minecraft/world/level/block/CampfireBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/CampfireBlock.java +@@ -133,7 +133,7 @@ public class CampfireBlock extends BaseEntityBlock implements SimpleWaterloggedB + BlockPos blockposition = ctx.getClickedPos(); + boolean flag = world.getFluidState(blockposition).getType() == Fluids.WATER; + +- return (BlockState) ((BlockState) ((BlockState) ((BlockState) this.defaultBlockState().setValue(CampfireBlock.WATERLOGGED, flag)).setValue(CampfireBlock.SIGNAL_FIRE, this.isSmokeSource(world.getBlockState(blockposition.below())))).setValue(CampfireBlock.LIT, !flag)).setValue(CampfireBlock.FACING, ctx.getHorizontalDirection()); ++ return (BlockState) ((BlockState) ((BlockState) ((BlockState) this.defaultBlockState().setValue(CampfireBlock.WATERLOGGED, flag)).setValue(CampfireBlock.SIGNAL_FIRE, this.isSmokeSource(world.getBlockState(blockposition.below())))).setValue(CampfireBlock.LIT, world.purpurConfig.campFireLitWhenPlaced ? !flag : world.purpurConfig.campFireLitWhenPlaced)).setValue(CampfireBlock.FACING, ctx.getHorizontalDirection()); // Purpur + } + + @Override +diff --git a/src/main/java/net/minecraft/world/level/block/CarvedPumpkinBlock.java b/src/main/java/net/minecraft/world/level/block/CarvedPumpkinBlock.java +index 655f51902e5d24643d41c4ec981743543c0890a5..e6a299eeda5d18274fa3b1fb542b217a074c1d83 100644 +--- a/src/main/java/net/minecraft/world/level/block/CarvedPumpkinBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/CarvedPumpkinBlock.java +@@ -71,7 +71,7 @@ public class CarvedPumpkinBlock extends HorizontalDirectionalBlock { + SnowGolem entitysnowman = (SnowGolem) EntityType.SNOW_GOLEM.create(world); + + if (entitysnowman != null) { +- CarvedPumpkinBlock.spawnGolemInWorld(world, shapedetector_shapedetectorcollection, entitysnowman, shapedetector_shapedetectorcollection.getBlock(0, 2, 0).getPos()); ++ CarvedPumpkinBlock.spawnGolemInWorld(world, shapedetector_shapedetectorcollection, entitysnowman, shapedetector_shapedetectorcollection.getBlock(0, 2, 0).getPos(), this.placer); // Purpur + } + } else { + BlockPattern.BlockPatternMatch shapedetector_shapedetectorcollection1 = this.getOrCreateIronGolemFull().find(world, pos); +@@ -81,7 +81,7 @@ public class CarvedPumpkinBlock extends HorizontalDirectionalBlock { + + if (entityirongolem != null) { + entityirongolem.setPlayerCreated(true); +- CarvedPumpkinBlock.spawnGolemInWorld(world, shapedetector_shapedetectorcollection1, entityirongolem, shapedetector_shapedetectorcollection1.getBlock(1, 2, 0).getPos()); ++ CarvedPumpkinBlock.spawnGolemInWorld(world, shapedetector_shapedetectorcollection1, entityirongolem, shapedetector_shapedetectorcollection1.getBlock(1, 2, 0).getPos(), this.placer); // Purpur + } + } + } +@@ -89,6 +89,16 @@ public class CarvedPumpkinBlock extends HorizontalDirectionalBlock { + } + + private static void spawnGolemInWorld(Level world, BlockPattern.BlockPatternMatch patternResult, Entity entity, BlockPos pos) { ++ // Purpur start ++ spawnGolemInWorld(world, patternResult, entity, pos, null); ++ } ++ private static void spawnGolemInWorld(Level world, BlockPattern.BlockPatternMatch patternResult, Entity entity, BlockPos pos, net.minecraft.world.entity.LivingEntity placer) { ++ if (entity instanceof SnowGolem snowGolem) { ++ snowGolem.setSummoner(placer == null ? null : placer.getUUID()); ++ } else if (entity instanceof IronGolem ironGolem) { ++ ironGolem.setSummoner(placer == null ? null : placer.getUUID()); ++ } ++ // Purpur end + // clearPatternBlocks(world, shapedetector_shapedetectorcollection); // CraftBukkit - moved down + entity.moveTo((double) pos.getX() + 0.5D, (double) pos.getY() + 0.05D, (double) pos.getZ() + 0.5D, 0.0F, 0.0F); + // CraftBukkit start +diff --git a/src/main/java/net/minecraft/world/level/block/CauldronBlock.java b/src/main/java/net/minecraft/world/level/block/CauldronBlock.java +index c9968934f4ecaa8d81e545f279b3001c7b1ce545..03e4fce6f8226451365fc2831b5bf1e5e6091730 100644 +--- a/src/main/java/net/minecraft/world/level/block/CauldronBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/CauldronBlock.java +@@ -37,7 +37,7 @@ public class CauldronBlock extends AbstractCauldronBlock { + } + + protected static boolean shouldHandlePrecipitation(Level world, Biome.Precipitation precipitation) { +- return precipitation == Biome.Precipitation.RAIN ? world.getRandom().nextFloat() < 0.05F : (precipitation == Biome.Precipitation.SNOW ? world.getRandom().nextFloat() < 0.1F : false); ++ return precipitation == Biome.Precipitation.RAIN ? world.getRandom().nextFloat() < world.purpurConfig.cauldronRainChance : (precipitation == Biome.Precipitation.SNOW ? world.getRandom().nextFloat() < world.purpurConfig.cauldronPowderSnowChance : false); // Purpur + } + + @Override +diff --git a/src/main/java/net/minecraft/world/level/block/CaveVinesBlock.java b/src/main/java/net/minecraft/world/level/block/CaveVinesBlock.java +index 635fc086d832c641f840cf36d18cdc0fcc3beef3..e3ff7b8059da499cfde1106f6d51156931b292dc 100644 +--- a/src/main/java/net/minecraft/world/level/block/CaveVinesBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/CaveVinesBlock.java +@@ -94,4 +94,11 @@ public class CaveVinesBlock extends GrowingPlantHeadBlock implements Bonemealabl + public void performBonemeal(ServerLevel world, RandomSource random, BlockPos pos, BlockState state) { + world.setBlock(pos, state.setValue(BERRIES, Boolean.valueOf(true)), 2); + } ++ ++ // Purpur start ++ @Override ++ public int getMaxGrowthAge() { ++ return org.purpurmc.purpur.PurpurConfig.caveVinesMaxGrowthAge; ++ } ++ // Purpur end + } +diff --git a/src/main/java/net/minecraft/world/level/block/ChangeOverTimeBlock.java b/src/main/java/net/minecraft/world/level/block/ChangeOverTimeBlock.java +index daae7fd6e0148cfba8e359d990748a0c83a3376e..0e06b1bcd906e92c083dc74d56d6d0a2a36f62a7 100644 +--- a/src/main/java/net/minecraft/world/level/block/ChangeOverTimeBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/ChangeOverTimeBlock.java +@@ -67,7 +67,7 @@ public interface ChangeOverTimeBlock> { + } + + float f = (float) (k + 1) / (float) (k + j + 1); +- float f1 = f * f * this.getChanceModifier(); ++ float f1 = world.purpurConfig.disableOxidationProximityPenalty ? this.getChanceModifier() : f * f * this.getChanceModifier(); // Purpur + + return random.nextFloat() < f1 ? this.getNext(state) : Optional.empty(); + } +diff --git a/src/main/java/net/minecraft/world/level/block/ChestBlock.java b/src/main/java/net/minecraft/world/level/block/ChestBlock.java +index 491474b66856fccb038ee436968c9a5d3e4bf75c..a66499c9bd9af9da5d261a3c1aa23b1d436d4008 100644 +--- a/src/main/java/net/minecraft/world/level/block/ChestBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/ChestBlock.java +@@ -343,6 +343,7 @@ public class ChestBlock extends AbstractChestBlock implements + } + + private static boolean isBlockedChestByBlock(BlockGetter world, BlockPos pos) { ++ if (world instanceof Level && ((Level) world).purpurConfig.chestOpenWithBlockOnTop) return false; // Purpur + BlockPos blockposition1 = pos.above(); + + return world.getBlockState(blockposition1).isRedstoneConductor(world, blockposition1); +diff --git a/src/main/java/net/minecraft/world/level/block/ComposterBlock.java b/src/main/java/net/minecraft/world/level/block/ComposterBlock.java +index d3d12f9114173f4971f95d7ef895a4374705bd3f..f34159f8d6c51af2341bf49db0d6d6f0417919cf 100644 +--- a/src/main/java/net/minecraft/world/level/block/ComposterBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/ComposterBlock.java +@@ -237,18 +237,27 @@ public class ComposterBlock extends Block implements WorldlyContainerHolder { + int i = (Integer) state.getValue(ComposterBlock.LEVEL); + + if (i < 8 && ComposterBlock.COMPOSTABLES.containsKey(stack.getItem())) { +- if (i < 7 && !world.isClientSide) { +- BlockState iblockdata1 = ComposterBlock.addItem(player, state, world, pos, stack); +- // Paper start - handle cancelled events +- if (iblockdata1 == null) { +- return ItemInteractionResult.SKIP_DEFAULT_BLOCK_INTERACTION; +- } +- // Paper end +- +- world.levelEvent(1500, pos, state != iblockdata1 ? 1 : 0); +- player.awardStat(Stats.ITEM_USED.get(stack.getItem())); +- stack.consume(1, player); ++ // Purpur start - sneak to bulk process composter ++ BlockState newState = process(i, player, state, world, pos, stack); ++ if (newState == null) { ++ return ItemInteractionResult.SKIP_DEFAULT_BLOCK_INTERACTION; + } ++ if (world.purpurConfig.composterBulkProcess && player.isShiftKeyDown() && newState != state) { ++ BlockState oldState; ++ int oldCount, newCount, oldLevel, newLevel; ++ do { ++ oldState = newState; ++ oldCount = stack.getCount(); ++ oldLevel = oldState.getValue(ComposterBlock.LEVEL); ++ newState = process(oldLevel, player, oldState, world, pos, stack); ++ if (newState == null) { ++ return ItemInteractionResult.SKIP_DEFAULT_BLOCK_INTERACTION; ++ } ++ newCount = stack.getCount(); ++ newLevel = newState.getValue(ComposterBlock.LEVEL); ++ } while (newCount > 0 && (newCount != oldCount || newLevel != oldLevel || newState != oldState)); ++ } ++ // Purpur end + + return ItemInteractionResult.sidedSuccess(world.isClientSide); + } else { +@@ -256,6 +265,25 @@ public class ComposterBlock extends Block implements WorldlyContainerHolder { + } + } + ++ // Purpur start - sneak to bulk process composter ++ private static @Nullable BlockState process(int level, Player player, BlockState state, Level world, BlockPos pos, ItemStack stack) { ++ if (level < 7 && !world.isClientSide) { ++ BlockState iblockdata1 = ComposterBlock.addItem(player, state, world, pos, stack); ++ // Paper start - handle cancelled events ++ if (iblockdata1 == null) { ++ return null; ++ } ++ // Paper end ++ ++ world.levelEvent(1500, pos, state != iblockdata1 ? 1 : 0); ++ player.awardStat(Stats.ITEM_USED.get(stack.getItem())); ++ stack.consume(1, player); ++ return iblockdata1; ++ } ++ return state; ++ } ++ // Purpur end ++ + @Override + protected InteractionResult useWithoutItem(BlockState state, Level world, BlockPos pos, Player player, BlockHitResult hit) { + int i = (Integer) state.getValue(ComposterBlock.LEVEL); +diff --git a/src/main/java/net/minecraft/world/level/block/CoralBlock.java b/src/main/java/net/minecraft/world/level/block/CoralBlock.java +index 81fe0dea8e6e23c4a78f07fc2f9c0d68cd683f11..bff97b7d3909f2ec9e58a341b901b3741927543f 100644 +--- a/src/main/java/net/minecraft/world/level/block/CoralBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/CoralBlock.java +@@ -59,6 +59,7 @@ public class CoralBlock extends Block { + } + + protected boolean scanForWater(BlockGetter world, BlockPos pos) { ++ if (!((net.minecraft.world.level.LevelAccessor) world).getMinecraftWorld().purpurConfig.coralDieOutsideWater) return true; // Purpur + Direction[] aenumdirection = Direction.values(); + int i = aenumdirection.length; + +diff --git a/src/main/java/net/minecraft/world/level/block/CropBlock.java b/src/main/java/net/minecraft/world/level/block/CropBlock.java +index 112d2feba5f75a2a873b595617780515945c10e4..5a190834baef60c7b61074393f8856a933902d81 100644 +--- a/src/main/java/net/minecraft/world/level/block/CropBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/CropBlock.java +@@ -179,7 +179,7 @@ public class CropBlock extends BushBlock implements BonemealableBlock { + @Override + protected void entityInside(BlockState state, Level world, BlockPos pos, Entity entity) { + if (!new io.papermc.paper.event.entity.EntityInsideBlockEvent(entity.getBukkitEntity(), org.bukkit.craftbukkit.block.CraftBlock.at(world, pos)).callEvent()) { return; } // Paper - Add EntityInsideBlockEvent +- if (entity instanceof Ravager && CraftEventFactory.callEntityChangeBlockEvent(entity, pos, Blocks.AIR.defaultBlockState(), !world.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING))) { // CraftBukkit ++ if (entity instanceof Ravager && world.purpurConfig.ravagerGriefableBlocks.contains(world.getBlockState(pos).getBlock()) && CraftEventFactory.callEntityChangeBlockEvent(entity, pos, Blocks.AIR.defaultBlockState(), (!world.purpurConfig.ravagerBypassMobGriefing && !world.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING)))) { // CraftBukkit // Purpur + world.destroyBlock(pos, true, entity); + } + +@@ -214,4 +214,15 @@ public class CropBlock extends BushBlock implements BonemealableBlock { + protected void createBlockStateDefinition(StateDefinition.Builder builder) { + builder.add(CropBlock.AGE); + } ++ ++ // Purpur start ++ @Override ++ public void playerDestroy(Level world, net.minecraft.world.entity.player.Player player, BlockPos pos, BlockState state, @javax.annotation.Nullable net.minecraft.world.level.block.entity.BlockEntity blockEntity, ItemStack itemInHand, boolean includeDrops, boolean dropExp) { ++ if (world.purpurConfig.hoeReplantsCrops && itemInHand.getItem() instanceof net.minecraft.world.item.HoeItem) { ++ super.playerDestroyAndReplant(world, player, pos, state, blockEntity, itemInHand, getBaseSeedId()); ++ } else { ++ super.playerDestroy(world, player, pos, state, blockEntity, itemInHand, includeDrops, dropExp); ++ } ++ } ++ // Purpur end + } +diff --git a/src/main/java/net/minecraft/world/level/block/DoorBlock.java b/src/main/java/net/minecraft/world/level/block/DoorBlock.java +index 42d43b7a7e3b7c53cc80b8706c130e660f2c72da..96199441202ad929ad0274574704635c538a93c7 100644 +--- a/src/main/java/net/minecraft/world/level/block/DoorBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/DoorBlock.java +@@ -198,6 +198,7 @@ public class DoorBlock extends Block { + protected InteractionResult useWithoutItem(BlockState state, Level world, BlockPos pos, Player player, BlockHitResult hit) { + if (!this.type.canOpenByHand()) { + return InteractionResult.PASS; ++ } else if (requiresRedstone(world, state, pos)) { return InteractionResult.CONSUME; // Purpur + } else { + state = (BlockState) state.cycle(DoorBlock.OPEN); + world.setBlock(pos, state, 10); +@@ -299,4 +300,18 @@ public class DoorBlock extends Block { + flag = false; + return flag; + } ++ ++ // Purpur start ++ public static boolean requiresRedstone(Level level, BlockState state, BlockPos pos) { ++ if (level.purpurConfig.doorRequiresRedstone.contains(state.getBlock())) { ++ // force update client ++ BlockPos otherPos = pos.relative(state.getValue(DoorBlock.HALF) == DoubleBlockHalf.LOWER ? Direction.UP : Direction.DOWN); ++ BlockState otherState = level.getBlockState(otherPos); ++ level.sendBlockUpdated(pos, state, state, 3); ++ level.sendBlockUpdated(otherPos, otherState, otherState, 3); ++ return true; ++ } ++ return false; ++ } ++ // Purpur end + } +diff --git a/src/main/java/net/minecraft/world/level/block/DragonEggBlock.java b/src/main/java/net/minecraft/world/level/block/DragonEggBlock.java +index fbe15cdd5b9bca2ab4b1e871abbbdbff49ade8a4..23d113842bf774bdc74e0dffcc97b642bc8684f1 100644 +--- a/src/main/java/net/minecraft/world/level/block/DragonEggBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/DragonEggBlock.java +@@ -48,8 +48,8 @@ public class DragonEggBlock extends FallingBlock { + } + + private void teleport(BlockState state, Level world, BlockPos pos) { ++ if (!world.purpurConfig.dragonEggTeleport) return; // Purpur + WorldBorder worldborder = world.getWorldBorder(); +- + for (int i = 0; i < 1000; ++i) { + BlockPos blockposition1 = pos.offset(world.random.nextInt(16) - world.random.nextInt(16), world.random.nextInt(8) - world.random.nextInt(8), world.random.nextInt(16) - world.random.nextInt(16)); + +diff --git a/src/main/java/net/minecraft/world/level/block/EnchantingTableBlock.java b/src/main/java/net/minecraft/world/level/block/EnchantingTableBlock.java +index 151e856dda3aa262c846ce8793650ee582bfb749..be0ed8a14e5726d5fcea1864302b18fb75fde2b4 100644 +--- a/src/main/java/net/minecraft/world/level/block/EnchantingTableBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/EnchantingTableBlock.java +@@ -124,4 +124,18 @@ public class EnchantingTableBlock extends BaseEntityBlock { + protected boolean isPathfindable(BlockState state, PathComputationType type) { + return false; + } ++ ++ // Purpur start ++ @Override ++ public void onRemove(BlockState state, Level level, BlockPos pos, BlockState newState, boolean moved) { ++ BlockEntity blockEntity = level.getBlockEntity(pos); ++ ++ if (level.purpurConfig.enchantmentTableLapisPersists && blockEntity instanceof EnchantingTableBlockEntity enchantmentTable) { ++ net.minecraft.world.Containers.dropItemStack(level, pos.getX(), pos.getY(), pos.getZ(), new net.minecraft.world.item.ItemStack(net.minecraft.world.item.Items.LAPIS_LAZULI, enchantmentTable.getLapis())); ++ level.updateNeighbourForOutputSignal(pos, this); ++ } ++ ++ super.onRemove(state, level, pos, newState, moved); ++ } ++ // Purpur end + } +diff --git a/src/main/java/net/minecraft/world/level/block/EndPortalBlock.java b/src/main/java/net/minecraft/world/level/block/EndPortalBlock.java +index 7272d70c672b54dcf595beafd7a2ed33c96e35cb..d7f33c676bba279661583d908d3a58c86d853545 100644 +--- a/src/main/java/net/minecraft/world/level/block/EndPortalBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/EndPortalBlock.java +@@ -54,6 +54,14 @@ public class EndPortalBlock extends BaseEntityBlock { + protected void entityInside(BlockState state, Level world, BlockPos pos, Entity entity) { + if (!new io.papermc.paper.event.entity.EntityInsideBlockEvent(entity.getBukkitEntity(), org.bukkit.craftbukkit.block.CraftBlock.at(world, pos)).callEvent()) { return; } // Paper - Add EntityInsideBlockEvent + if (world instanceof ServerLevel && entity.canChangeDimensions() && Shapes.joinIsNotEmpty(Shapes.create(entity.getBoundingBox().move((double) (-pos.getX()), (double) (-pos.getY()), (double) (-pos.getZ()))), state.getShape(world, pos), BooleanOp.AND)) { ++ // Purpur start ++ if (entity.isPassenger() || entity.isVehicle()) { ++ if (new org.purpurmc.purpur.event.entity.EntityTeleportHinderedEvent(entity.getBukkitEntity(), entity.isPassenger() ? org.purpurmc.purpur.event.entity.EntityTeleportHinderedEvent.Reason.IS_PASSENGER : org.purpurmc.purpur.event.entity.EntityTeleportHinderedEvent.Reason.IS_VEHICLE, PlayerTeleportEvent.TeleportCause.END_PORTAL).callEvent()) { ++ this.entityInside(state, world, pos, entity); ++ } ++ return; ++ } ++ // Purpur end + ResourceKey resourcekey = world.getTypeKey() == LevelStem.END ? Level.OVERWORLD : Level.END; // CraftBukkit - SPIGOT-6152: send back to main overworld in custom ends + ServerLevel worldserver = ((ServerLevel) world).getServer().getLevel(resourcekey); + +diff --git a/src/main/java/net/minecraft/world/level/block/EnderChestBlock.java b/src/main/java/net/minecraft/world/level/block/EnderChestBlock.java +index 5f9858ef8d0ec1a74d469ab4426eb1db068873fd..d7ef772444b301d0a3f167679027195e4150b064 100644 +--- a/src/main/java/net/minecraft/world/level/block/EnderChestBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/EnderChestBlock.java +@@ -91,7 +91,7 @@ public class EnderChestBlock extends AbstractChestBlock i + EnderChestBlockEntity enderChestBlockEntity = (EnderChestBlockEntity)blockEntity; + playerEnderChestContainer.setActiveChest(enderChestBlockEntity); + player.openMenu( +- new SimpleMenuProvider((i, inventory, playerx) -> ChestMenu.threeRows(i, inventory, playerEnderChestContainer), CONTAINER_TITLE) ++ new SimpleMenuProvider((i, inventory, playerx) -> org.purpurmc.purpur.PurpurConfig.enderChestSixRows ? getEnderChestSixRows(i, inventory, player, playerEnderChestContainer) : ChestMenu.threeRows(i, inventory, playerEnderChestContainer), CONTAINER_TITLE) // Purpur + ); + player.awardStat(Stats.OPEN_ENDERCHEST); + PiglinAi.angerNearbyPiglins(player, true); +@@ -102,6 +102,35 @@ public class EnderChestBlock extends AbstractChestBlock i + } + } + ++ // Purpur start ++ private ChestMenu getEnderChestSixRows(int syncId, net.minecraft.world.entity.player.Inventory inventory, Player player, PlayerEnderChestContainer playerEnderChestContainer) { ++ if (org.purpurmc.purpur.PurpurConfig.enderChestPermissionRows) { ++ org.bukkit.craftbukkit.entity.CraftHumanEntity bukkitPlayer = player.getBukkitEntity(); ++ if (bukkitPlayer.hasPermission("purpur.enderchest.rows.six")) { ++ player.sixRowEnderchestSlotCount = 54; ++ return ChestMenu.sixRows(syncId, inventory, playerEnderChestContainer); ++ } else if (bukkitPlayer.hasPermission("purpur.enderchest.rows.five")) { ++ player.sixRowEnderchestSlotCount = 45; ++ return ChestMenu.fiveRows(syncId, inventory, playerEnderChestContainer); ++ } else if (bukkitPlayer.hasPermission("purpur.enderchest.rows.four")) { ++ player.sixRowEnderchestSlotCount = 36; ++ return ChestMenu.fourRows(syncId, inventory, playerEnderChestContainer); ++ } else if (bukkitPlayer.hasPermission("purpur.enderchest.rows.three")) { ++ player.sixRowEnderchestSlotCount = 27; ++ return ChestMenu.threeRows(syncId, inventory, playerEnderChestContainer); ++ } else if (bukkitPlayer.hasPermission("purpur.enderchest.rows.two")) { ++ player.sixRowEnderchestSlotCount = 18; ++ return ChestMenu.twoRows(syncId, inventory, playerEnderChestContainer); ++ } else if (bukkitPlayer.hasPermission("purpur.enderchest.rows.one")) { ++ player.sixRowEnderchestSlotCount = 9; ++ return ChestMenu.oneRow(syncId, inventory, playerEnderChestContainer); ++ } ++ } ++ player.sixRowEnderchestSlotCount = -1; ++ return ChestMenu.sixRows(syncId, inventory, playerEnderChestContainer); ++ } ++ // Purpur end ++ + @Override + public BlockEntity newBlockEntity(BlockPos pos, BlockState state) { + return new EnderChestBlockEntity(pos, state); +diff --git a/src/main/java/net/minecraft/world/level/block/FarmBlock.java b/src/main/java/net/minecraft/world/level/block/FarmBlock.java +index d59e33e7326489c6d55d316d0130f22235f4c63c..d0ec0722496ed931b48c4e7076fddbb1ed36e111 100644 +--- a/src/main/java/net/minecraft/world/level/block/FarmBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/FarmBlock.java +@@ -111,7 +111,7 @@ public class FarmBlock extends Block { + @Override + public void fallOn(Level world, BlockState state, BlockPos pos, Entity entity, float fallDistance) { + super.fallOn(world, state, pos, entity, fallDistance); // CraftBukkit - moved here as game rules / events shouldn't affect fall damage. +- if (!world.isClientSide && world.random.nextFloat() < fallDistance - 0.5F && entity instanceof LivingEntity && (entity instanceof Player || world.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING)) && entity.getBbWidth() * entity.getBbWidth() * entity.getBbHeight() > 0.512F) { ++ if (!world.isClientSide && (world.purpurConfig.farmlandTrampleHeight >= 0D ? fallDistance >= world.purpurConfig.farmlandTrampleHeight : world.random.nextFloat() < fallDistance - 0.5F) && entity instanceof LivingEntity && (entity instanceof Player || world.purpurConfig.farmlandBypassMobGriefing || world.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING)) && entity.getBbWidth() * entity.getBbWidth() * entity.getBbHeight() > 0.512F) { // Purpur + // CraftBukkit start - Interact soil + org.bukkit.event.Cancellable cancellable; + if (entity instanceof Player) { +@@ -125,6 +125,22 @@ public class FarmBlock extends Block { + return; + } + ++ // Purpur start ++ if (world.purpurConfig.farmlandTramplingDisabled) return; ++ if (world.purpurConfig.farmlandTramplingOnlyPlayers && !(entity instanceof Player)) return; ++ if (world.purpurConfig.farmlandAlpha) { ++ Block block = world.getBlockState(pos.below()).getBlock(); ++ if (block instanceof FenceBlock || block instanceof WallBlock) { ++ return; ++ } ++ } ++ if (world.purpurConfig.farmlandTramplingFeatherFalling) { ++ Iterator armor = ((LivingEntity) entity).getArmorSlots().iterator(); ++ if (armor.hasNext() && net.minecraft.world.item.enchantment.EnchantmentHelper.getItemEnchantmentLevel(net.minecraft.world.item.enchantment.Enchantments.FEATHER_FALLING, armor.next()) >= (int) entity.fallDistance) { ++ return; ++ } ++ } ++ // Purpur end + if (!CraftEventFactory.callEntityChangeBlockEvent(entity, pos, Blocks.DIRT.defaultBlockState())) { + return; + } +@@ -172,7 +188,7 @@ public class FarmBlock extends Block { + } + } + +- return false; ++ return ((ServerLevel) world).purpurConfig.farmlandGetsMoistFromBelow && world.getFluidState(pos.relative(Direction.DOWN)).is(FluidTags.WATER); // Purpur; + // Paper end - Perf: remove abstract block iteration + } + +diff --git a/src/main/java/net/minecraft/world/level/block/GrowingPlantHeadBlock.java b/src/main/java/net/minecraft/world/level/block/GrowingPlantHeadBlock.java +index cf05da1c86e3018db11dc079bf50317b6639e5cc..8e9903899ac91e9431f00675c1f5ac4a18e61593 100644 +--- a/src/main/java/net/minecraft/world/level/block/GrowingPlantHeadBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/GrowingPlantHeadBlock.java +@@ -34,12 +34,12 @@ public abstract class GrowingPlantHeadBlock extends GrowingPlantBlock implements + + @Override + public BlockState getStateForPlacement(LevelAccessor world) { +- return (BlockState) this.defaultBlockState().setValue(GrowingPlantHeadBlock.AGE, world.getRandom().nextInt(25)); ++ return (BlockState) this.defaultBlockState().setValue(GrowingPlantHeadBlock.AGE, world.getRandom().nextInt(getMaxGrowthAge())); // Purpur + } + + @Override + protected boolean isRandomlyTicking(BlockState state) { +- return (Integer) state.getValue(GrowingPlantHeadBlock.AGE) < 25; ++ return (Integer) state.getValue(GrowingPlantHeadBlock.AGE) < getMaxGrowthAge(); // Purpur + } + + @Override +@@ -55,7 +55,7 @@ public abstract class GrowingPlantHeadBlock extends GrowingPlantBlock implements + } else { + modifier = world.spigotConfig.caveVinesModifier; + } +- if ((Integer) state.getValue(GrowingPlantHeadBlock.AGE) < 25 && random.nextDouble() < ((modifier / 100.0D) * this.growPerTickProbability)) { // Spigot - SPIGOT-7159: Better modifier resolution ++ if ((Integer) state.getValue(GrowingPlantHeadBlock.AGE) < getMaxGrowthAge() && random.nextDouble() < ((modifier / 100.0D) * this.growPerTickProbability)) { // Spigot - SPIGOT-7159: Better modifier resolution // Purpur + // Spigot end + BlockPos blockposition1 = pos.relative(this.growthDirection); + +@@ -77,11 +77,11 @@ public abstract class GrowingPlantHeadBlock extends GrowingPlantBlock implements + } + + public BlockState getMaxAgeState(BlockState state) { +- return (BlockState) state.setValue(GrowingPlantHeadBlock.AGE, 25); ++ return (BlockState) state.setValue(GrowingPlantHeadBlock.AGE, getMaxGrowthAge()); // Purpur + } + + public boolean isMaxAge(BlockState state) { +- return (Integer) state.getValue(GrowingPlantHeadBlock.AGE) == 25; ++ return (Integer) state.getValue(GrowingPlantHeadBlock.AGE) >= getMaxGrowthAge(); // Purpur + } + + protected BlockState updateBodyAfterConvertedFromHead(BlockState from, BlockState to) { +@@ -123,13 +123,13 @@ public abstract class GrowingPlantHeadBlock extends GrowingPlantBlock implements + @Override + public void performBonemeal(ServerLevel world, RandomSource random, BlockPos pos, BlockState state) { + BlockPos blockposition1 = pos.relative(this.growthDirection); +- int i = Math.min((Integer) state.getValue(GrowingPlantHeadBlock.AGE) + 1, 25); ++ int i = Math.min((Integer) state.getValue(GrowingPlantHeadBlock.AGE) + 1, getMaxGrowthAge()); // Purpur + int j = this.getBlocksToGrowWhenBonemealed(random); + + for (int k = 0; k < j && this.canGrowInto(world.getBlockState(blockposition1)); ++k) { + world.setBlockAndUpdate(blockposition1, (BlockState) state.setValue(GrowingPlantHeadBlock.AGE, i)); + blockposition1 = blockposition1.relative(this.growthDirection); +- i = Math.min(i + 1, 25); ++ i = Math.min(i + 1, getMaxGrowthAge()); // Purpur + } + + } +@@ -142,4 +142,6 @@ public abstract class GrowingPlantHeadBlock extends GrowingPlantBlock implements + protected GrowingPlantHeadBlock getHeadBlock() { + return this; + } ++ ++ public abstract int getMaxGrowthAge(); // Purpur + } +diff --git a/src/main/java/net/minecraft/world/level/block/HayBlock.java b/src/main/java/net/minecraft/world/level/block/HayBlock.java +index ef364aa171a48482a45bc18cfe730ec20c3f7be6..74971d90506aa253d5ee821b5390fb2551a3a393 100644 +--- a/src/main/java/net/minecraft/world/level/block/HayBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/HayBlock.java +@@ -23,6 +23,6 @@ public class HayBlock extends RotatedPillarBlock { + + @Override + public void fallOn(Level world, BlockState state, BlockPos pos, Entity entity, float fallDistance) { +- entity.causeFallDamage(fallDistance, 0.2F, world.damageSources().fall()); ++ super.fallOn(world, state, pos, entity, fallDistance); // Purpur + } + } +diff --git a/src/main/java/net/minecraft/world/level/block/IceBlock.java b/src/main/java/net/minecraft/world/level/block/IceBlock.java +index 013302623d3ca3ff88f242d740af935dcf4844a6..13dd8bc7d2f6b71a5f1779dde53c5c84d83538ce 100644 +--- a/src/main/java/net/minecraft/world/level/block/IceBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/IceBlock.java +@@ -41,7 +41,7 @@ public class IceBlock extends HalfTransparentBlock { + public void afterDestroy(Level world, BlockPos pos, ItemStack tool) { + // Paper end - Improve Block#breakNaturally API + if (EnchantmentHelper.getItemEnchantmentLevel(Enchantments.SILK_TOUCH, tool) == 0) { +- if (world.dimensionType().ultraWarm()) { ++ if (world.isNether() || (world.isTheEnd() && !org.purpurmc.purpur.PurpurConfig.allowWaterPlacementInTheEnd)) { // Purpur + world.removeBlock(pos, false); + return; + } +@@ -69,7 +69,7 @@ public class IceBlock extends HalfTransparentBlock { + return; + } + // CraftBukkit end +- if (world.dimensionType().ultraWarm()) { ++ if (world.isNether() || (world.isTheEnd() && !org.purpurmc.purpur.PurpurConfig.allowWaterPlacementInTheEnd)) { // Purpur + world.removeBlock(pos, false); + } else { + world.setBlockAndUpdate(pos, IceBlock.meltsInto()); +diff --git a/src/main/java/net/minecraft/world/level/block/KelpBlock.java b/src/main/java/net/minecraft/world/level/block/KelpBlock.java +index 784b19bc78c8ad9476b6dac37b6778a409a7c675..d49dd8b20d3785cc9482ed2a34fbd7aed4c9e537 100644 +--- a/src/main/java/net/minecraft/world/level/block/KelpBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/KelpBlock.java +@@ -72,4 +72,11 @@ public class KelpBlock extends GrowingPlantHeadBlock implements LiquidBlockConta + protected FluidState getFluidState(BlockState state) { + return Fluids.WATER.getSource(false); + } ++ ++ // Purpur start ++ @Override ++ public int getMaxGrowthAge() { ++ return org.purpurmc.purpur.PurpurConfig.kelpMaxGrowthAge; ++ } ++ // Purpur end + } +diff --git a/src/main/java/net/minecraft/world/level/block/LiquidBlock.java b/src/main/java/net/minecraft/world/level/block/LiquidBlock.java +index 84623c632d8c2f0fa7ec939c711316d757117d23..1851035b9fdcc076442d0699567a3b020e6425d6 100644 +--- a/src/main/java/net/minecraft/world/level/block/LiquidBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/LiquidBlock.java +@@ -137,7 +137,7 @@ public class LiquidBlock extends Block implements BucketPickup { + + @Override + protected void onPlace(BlockState state, Level world, BlockPos pos, BlockState oldState, boolean notify) { +- if (this.shouldSpreadLiquid(world, pos, state)) { ++ if (world.purpurConfig.tickFluids && this.shouldSpreadLiquid(world, pos, state)) { // Purpur + world.scheduleTick(pos, state.getFluidState().getType(), this.getFlowSpeed(world, pos)); // Paper - Configurable speed for water flowing over lava + } + +@@ -165,7 +165,7 @@ public class LiquidBlock extends Block implements BucketPickup { + + @Override + protected BlockState updateShape(BlockState state, Direction direction, BlockState neighborState, LevelAccessor world, BlockPos pos, BlockPos neighborPos) { +- if (state.getFluidState().isSource() || neighborState.getFluidState().isSource()) { ++ if (world.getMinecraftWorld().purpurConfig.tickFluids && state.getFluidState().isSource() || neighborState.getFluidState().isSource()) { // Purpur + world.scheduleTick(pos, state.getFluidState().getType(), this.fluid.getTickDelay(world)); + } + +@@ -174,7 +174,7 @@ public class LiquidBlock extends Block implements BucketPickup { + + @Override + protected void neighborChanged(BlockState state, Level world, BlockPos pos, Block sourceBlock, BlockPos sourcePos, boolean notify) { +- if (this.shouldSpreadLiquid(world, pos, state)) { ++ if (world.purpurConfig.tickFluids && this.shouldSpreadLiquid(world, pos, state)) { // Purpur + world.scheduleTick(pos, state.getFluidState().getType(), this.getFlowSpeed(world, pos)); // Paper - Configurable speed for water flowing over lava + } + +diff --git a/src/main/java/net/minecraft/world/level/block/MagmaBlock.java b/src/main/java/net/minecraft/world/level/block/MagmaBlock.java +index 77bbdc15472d656fd40e841a70e34d3d31580819..55ae530fac54236ea5614f8e92c30febc744f179 100644 +--- a/src/main/java/net/minecraft/world/level/block/MagmaBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/MagmaBlock.java +@@ -29,7 +29,7 @@ public class MagmaBlock extends Block { + + @Override + public void stepOn(Level world, BlockPos pos, BlockState state, Entity entity) { +- if (!entity.isSteppingCarefully() && entity instanceof LivingEntity && !EnchantmentHelper.hasFrostWalker((LivingEntity) entity)) { ++ if ((!entity.isSteppingCarefully() || world.purpurConfig.magmaBlockDamageWhenSneaking) && entity instanceof LivingEntity && (world.purpurConfig.magmaBlockDamageWithFrostWalker || !EnchantmentHelper.hasFrostWalker((LivingEntity) entity))) { // Purpur + entity.hurt(world.damageSources().hotFloor().directBlock(world, pos), 1.0F); // CraftBukkit + } + +diff --git a/src/main/java/net/minecraft/world/level/block/NetherPortalBlock.java b/src/main/java/net/minecraft/world/level/block/NetherPortalBlock.java +index a9e3078cefcae8cc4672d514a7add162590d48df..8d5e841d8cc69bf09a9f1b6248633a72ce5fe1d7 100644 +--- a/src/main/java/net/minecraft/world/level/block/NetherPortalBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/NetherPortalBlock.java +@@ -60,7 +60,7 @@ public class NetherPortalBlock extends Block { + + @Override + protected void randomTick(BlockState state, ServerLevel world, BlockPos pos, RandomSource random) { +- if (world.spigotConfig.enableZombiePigmenPortalSpawns && world.dimensionType().natural() && world.getGameRules().getBoolean(GameRules.RULE_DOMOBSPAWNING) && random.nextInt(2000) < world.getDifficulty().getId()) { // Spigot ++ if (world.spigotConfig.enableZombiePigmenPortalSpawns && world.dimensionType().natural() && world.getGameRules().getBoolean(GameRules.RULE_DOMOBSPAWNING) && random.nextInt(world.purpurConfig.piglinPortalSpawnModifier) < world.getDifficulty().getId()) { // Spigot // Purpur + while (world.getBlockState(pos).is((Block) this)) { + pos = pos.below(); + } +@@ -92,6 +92,14 @@ public class NetherPortalBlock extends Block { + protected void entityInside(BlockState state, Level world, BlockPos pos, Entity entity) { + if (!new io.papermc.paper.event.entity.EntityInsideBlockEvent(entity.getBukkitEntity(), org.bukkit.craftbukkit.block.CraftBlock.at(world, pos)).callEvent()) { return; } // Paper - Add EntityInsideBlockEvent + if (entity.canChangeDimensions()) { ++ // Purpur start ++ if (entity.isPassenger() || entity.isVehicle()) { ++ if (new org.purpurmc.purpur.event.entity.EntityTeleportHinderedEvent(entity.getBukkitEntity(), entity.isPassenger() ? org.purpurmc.purpur.event.entity.EntityTeleportHinderedEvent.Reason.IS_PASSENGER : org.purpurmc.purpur.event.entity.EntityTeleportHinderedEvent.Reason.IS_VEHICLE, org.bukkit.event.player.PlayerTeleportEvent.TeleportCause.NETHER_PORTAL).callEvent()) { ++ this.entityInside(state, world, pos, entity); ++ } ++ return; ++ } ++ // Purpur end + // CraftBukkit start - Entity in portal + EntityPortalEnterEvent event = new EntityPortalEnterEvent(entity.getBukkitEntity(), new org.bukkit.Location(world.getWorld(), pos.getX(), pos.getY(), pos.getZ())); + world.getCraftServer().getPluginManager().callEvent(event); +diff --git a/src/main/java/net/minecraft/world/level/block/NetherWartBlock.java b/src/main/java/net/minecraft/world/level/block/NetherWartBlock.java +index acbd60a2f162fe0e254e36d0e8e7face3fc8a7b3..b8355ea1de26c4b6905f477fb4e110f1762447b4 100644 +--- a/src/main/java/net/minecraft/world/level/block/NetherWartBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/NetherWartBlock.java +@@ -16,7 +16,7 @@ import net.minecraft.world.level.block.state.properties.IntegerProperty; + import net.minecraft.world.phys.shapes.CollisionContext; + import net.minecraft.world.phys.shapes.VoxelShape; + +-public class NetherWartBlock extends BushBlock { ++public class NetherWartBlock extends BushBlock implements BonemealableBlock { // Purpur + + public static final MapCodec CODEC = simpleCodec(NetherWartBlock::new); + public static final int MAX_AGE = 3; +@@ -68,4 +68,32 @@ public class NetherWartBlock extends BushBlock { + protected void createBlockStateDefinition(StateDefinition.Builder builder) { + builder.add(NetherWartBlock.AGE); + } ++ ++ // Purpur start ++ @Override ++ public void playerDestroy(net.minecraft.world.level.Level world, net.minecraft.world.entity.player.Player player, BlockPos pos, BlockState state, @javax.annotation.Nullable net.minecraft.world.level.block.entity.BlockEntity blockEntity, ItemStack itemInHand, boolean includeDrops, boolean dropExp) { ++ if (world.purpurConfig.hoeReplantsNetherWarts && itemInHand.getItem() instanceof net.minecraft.world.item.HoeItem) { ++ super.playerDestroyAndReplant(world, player, pos, state, blockEntity, itemInHand, Items.NETHER_WART); ++ } else { ++ super.playerDestroy(world, player, pos, state, blockEntity, itemInHand, includeDrops, dropExp); ++ } ++ } ++ ++ @Override ++ public boolean isValidBonemealTarget(final net.minecraft.world.level.LevelReader world, final BlockPos pos, final BlockState state) { ++ return ((net.minecraft.world.level.Level) world).purpurConfig.netherWartAffectedByBonemeal && state.getValue(NetherWartBlock.AGE) < 3; ++ } ++ ++ @Override ++ public boolean isBonemealSuccess(net.minecraft.world.level.Level world, RandomSource random, BlockPos pos, BlockState state) { ++ return true; ++ } ++ ++ @Override ++ public void performBonemeal(ServerLevel world, RandomSource random, BlockPos pos, BlockState state) { ++ int i = Math.min(3, state.getValue(NetherWartBlock.AGE) + 1); ++ state = state.setValue(NetherWartBlock.AGE, i); ++ org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockGrowEvent(world, pos, state, 2); // CraftBukkit ++ } ++ // Purpur end + } +diff --git a/src/main/java/net/minecraft/world/level/block/NoteBlock.java b/src/main/java/net/minecraft/world/level/block/NoteBlock.java +index 1d82cfe7af0dc42f88901fb0c44896771fdf8a93..43dd972b374daa1072608f3a68e812e7fb733a2b 100644 +--- a/src/main/java/net/minecraft/world/level/block/NoteBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/NoteBlock.java +@@ -95,7 +95,7 @@ public class NoteBlock extends Block { + } + + private void playNote(@Nullable Entity entity, BlockState state, Level world, BlockPos pos) { +- if (((NoteBlockInstrument) state.getValue(NoteBlock.INSTRUMENT)).worksAboveNoteBlock() || world.getBlockState(pos.above()).isAir()) { ++ if (world.purpurConfig.noteBlockIgnoreAbove || ((NoteBlockInstrument) state.getValue(NoteBlock.INSTRUMENT)).worksAboveNoteBlock() || world.getBlockState(pos.above()).isAir()) { // Purpur + // CraftBukkit start + // org.bukkit.event.block.NotePlayEvent event = org.bukkit.craftbukkit.event.CraftEventFactory.callNotePlayEvent(world, pos, state.getValue(NoteBlock.INSTRUMENT), state.getValue(NoteBlock.NOTE)); + // if (event.isCancelled()) { +diff --git a/src/main/java/net/minecraft/world/level/block/ObserverBlock.java b/src/main/java/net/minecraft/world/level/block/ObserverBlock.java +index b38fbe5121f293f425d7673a6ce49b11d0ced0d9..2a74f42672b92393b52a61c27c5b8af77c8c6070 100644 +--- a/src/main/java/net/minecraft/world/level/block/ObserverBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/ObserverBlock.java +@@ -71,6 +71,7 @@ public class ObserverBlock extends DirectionalBlock { + @Override + protected BlockState updateShape(BlockState state, Direction direction, BlockState neighborState, LevelAccessor world, BlockPos pos, BlockPos neighborPos) { + if (state.getValue(ObserverBlock.FACING) == direction && !(Boolean) state.getValue(ObserverBlock.POWERED)) { ++ if (!world.getMinecraftWorld().purpurConfig.disableObserverClocks || !(neighborState.getBlock() instanceof ObserverBlock) || neighborState.getValue(ObserverBlock.FACING).getOpposite() != direction) // Purpur + this.startSignal(world, pos); + } + +diff --git a/src/main/java/net/minecraft/world/level/block/PointedDripstoneBlock.java b/src/main/java/net/minecraft/world/level/block/PointedDripstoneBlock.java +index a2bd54dae4b0460d200f6d5300194a7ef5a28830..bf189a171530abfc9bba5db5a305feb391f2cbee 100644 +--- a/src/main/java/net/minecraft/world/level/block/PointedDripstoneBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/PointedDripstoneBlock.java +@@ -190,7 +190,7 @@ public class PointedDripstoneBlock extends Block implements Fallable, SimpleWate + + @VisibleForTesting + public static void maybeTransferFluid(BlockState state, ServerLevel world, BlockPos pos, float dripChance) { +- if (dripChance <= 0.17578125F || dripChance <= 0.05859375F) { ++ if (dripChance <= world.purpurConfig.cauldronDripstoneWaterFillChance || dripChance <= world.purpurConfig.cauldronDripstoneLavaFillChance) { // Purpur + if (PointedDripstoneBlock.isStalactiteStartPos(state, world, pos)) { + Optional optional = PointedDripstoneBlock.getFluidAboveStalactite(world, pos, state); + +@@ -199,13 +199,13 @@ public class PointedDripstoneBlock extends Block implements Fallable, SimpleWate + float f1; + + if (fluidtype == Fluids.WATER) { +- f1 = 0.17578125F; ++ f1 = world.purpurConfig.cauldronDripstoneWaterFillChance; // Purpur + } else { + if (fluidtype != Fluids.LAVA) { + return; + } + +- f1 = 0.05859375F; ++ f1 = world.purpurConfig.cauldronDripstoneLavaFillChance; // Purpur + } + + if (dripChance < f1) { +diff --git a/src/main/java/net/minecraft/world/level/block/PowderSnowBlock.java b/src/main/java/net/minecraft/world/level/block/PowderSnowBlock.java +index a6e6545402904141ffc6218a0158b0e9c67217c8..5eac1a54398dfa5571b98fb6eefca9d2bf9b2793 100644 +--- a/src/main/java/net/minecraft/world/level/block/PowderSnowBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/PowderSnowBlock.java +@@ -80,7 +80,7 @@ public class PowderSnowBlock extends Block implements BucketPickup { + if (!world.isClientSide) { + // CraftBukkit start + if (entity.isOnFire() && entity.mayInteract(world, pos)) { +- if (!org.bukkit.craftbukkit.event.CraftEventFactory.callEntityChangeBlockEvent(entity, pos, Blocks.AIR.defaultBlockState(), !(world.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING) || entity instanceof Player))) { ++ if (!org.bukkit.craftbukkit.event.CraftEventFactory.callEntityChangeBlockEvent(entity, pos, Blocks.AIR.defaultBlockState(), !((world.purpurConfig.powderSnowBypassMobGriefing || world.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING)) || entity instanceof Player))) { + return; + } + // CraftBukkit end +diff --git a/src/main/java/net/minecraft/world/level/block/PoweredRailBlock.java b/src/main/java/net/minecraft/world/level/block/PoweredRailBlock.java +index 9603d8c84ff483030dc08e82d3579b89e5c1f6e9..8fc65c32a3c6e6842a76b36f45e1b1c23abbc480 100644 +--- a/src/main/java/net/minecraft/world/level/block/PoweredRailBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/PoweredRailBlock.java +@@ -30,7 +30,7 @@ public class PoweredRailBlock extends BaseRailBlock { + } + + protected boolean findPoweredRailSignal(Level world, BlockPos pos, BlockState state, boolean flag, int distance) { +- if (distance >= 8) { ++ if (distance >= world.purpurConfig.railActivationRange) { // Purpur + return false; + } else { + int j = pos.getX(); +diff --git a/src/main/java/net/minecraft/world/level/block/RespawnAnchorBlock.java b/src/main/java/net/minecraft/world/level/block/RespawnAnchorBlock.java +index be85535767bc79875c38da78a209d33d4be87c8a..2b840a5516073da46207552688428d86fc99975b 100644 +--- a/src/main/java/net/minecraft/world/level/block/RespawnAnchorBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/RespawnAnchorBlock.java +@@ -149,7 +149,7 @@ public class RespawnAnchorBlock extends Block { + }; + Vec3 vec3d = explodedPos.getCenter(); + +- world.explode((Entity) null, world.damageSources().badRespawnPointExplosion(vec3d, world, state, explodedPos), explosiondamagecalculator, vec3d, 5.0F, true, Level.ExplosionInteraction.BLOCK); // CraftBukkit - add state ++ if (world.purpurConfig.respawnAnchorExplode)world.explode((Entity) null, world.damageSources().badRespawnPointExplosion(vec3d, world, state, explodedPos), explosiondamagecalculator, vec3d, (float) world.purpurConfig.respawnAnchorExplosionPower, world.purpurConfig.respawnAnchorExplosionFire, world.purpurConfig.respawnAnchorExplosionEffect);// CraftBukkit - add state // Purpur + } + + public static boolean canSetSpawn(Level world) { +diff --git a/src/main/java/net/minecraft/world/level/block/SculkShriekerBlock.java b/src/main/java/net/minecraft/world/level/block/SculkShriekerBlock.java +index b6b367492ebe2af3e63381bef935c6077f6ddb27..09f34c30d9a03751ed826b26375ac5aee778cce4 100644 +--- a/src/main/java/net/minecraft/world/level/block/SculkShriekerBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/SculkShriekerBlock.java +@@ -134,7 +134,7 @@ public class SculkShriekerBlock extends BaseEntityBlock implements SimpleWaterlo + @Nullable + @Override + public BlockState getStateForPlacement(BlockPlaceContext ctx) { +- return (BlockState) this.defaultBlockState().setValue(SculkShriekerBlock.WATERLOGGED, ctx.getLevel().getFluidState(ctx.getClickedPos()).getType() == Fluids.WATER); ++ return (BlockState) this.defaultBlockState().setValue(SculkShriekerBlock.WATERLOGGED, ctx.getLevel().getFluidState(ctx.getClickedPos()).getType() == Fluids.WATER).setValue(SculkShriekerBlock.CAN_SUMMON, ctx.getLevel().purpurConfig.sculkShriekerCanSummonDefault); // Purpur + } + + @Override +diff --git a/src/main/java/net/minecraft/world/level/block/SlabBlock.java b/src/main/java/net/minecraft/world/level/block/SlabBlock.java +index fa29eb15934b3dad171d27c21d99b2451cfe553b..ba4aa69425d796d306791ea193f9c6b21a065f0b 100644 +--- a/src/main/java/net/minecraft/world/level/block/SlabBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/SlabBlock.java +@@ -138,4 +138,25 @@ public class SlabBlock extends Block implements SimpleWaterloggedBlock { + return false; + } + } ++ ++ // Purpur start ++ public boolean halfBreak(BlockState state, BlockPos pos, net.minecraft.server.level.ServerPlayer player) { ++ if (state.getValue(SlabBlock.TYPE) != SlabType.DOUBLE) { ++ return false; ++ } ++ net.minecraft.world.phys.HitResult result = player.getRayTrace(16, net.minecraft.world.level.ClipContext.Fluid.NONE); ++ if (result.getType() != net.minecraft.world.phys.HitResult.Type.BLOCK) { ++ return false; ++ } ++ double hitY = result.getLocation().y(); ++ int blockY = org.bukkit.util.NumberConversions.floor(hitY); ++ player.level().setBlock(pos, state.setValue(SlabBlock.TYPE, (hitY - blockY > 0.5 || blockY - pos.getY() == 1) ? SlabType.BOTTOM : SlabType.TOP), 3); ++ if (!player.getAbilities().instabuild) { ++ net.minecraft.world.entity.item.ItemEntity item = new net.minecraft.world.entity.item.ItemEntity(player.level(), pos.getX(), pos.getY(), pos.getZ(), new ItemStack(asItem())); ++ item.setDefaultPickUpDelay(); ++ player.level().addFreshEntity(item); ++ } ++ return true; ++ } ++ // Purpur end + } +diff --git a/src/main/java/net/minecraft/world/level/block/SnowLayerBlock.java b/src/main/java/net/minecraft/world/level/block/SnowLayerBlock.java +index 93e8e5107ac047c1f2579b4fe6b0a202edb695f6..f82d275aac7bf3949d3dcc412c7e39e115c69458 100644 +--- a/src/main/java/net/minecraft/world/level/block/SnowLayerBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/SnowLayerBlock.java +@@ -88,6 +88,12 @@ public class SnowLayerBlock extends Block { + protected boolean canSurvive(BlockState state, LevelReader world, BlockPos pos) { + BlockState iblockdata1 = world.getBlockState(pos.below()); + ++ // Purpur start ++ if (iblockdata1.is(Blocks.BLUE_ICE) && !world.getWorldBorder().world.purpurConfig.snowOnBlueIce) { ++ return false; ++ } ++ // Purpur end ++ + return iblockdata1.is(BlockTags.SNOW_LAYER_CANNOT_SURVIVE_ON) ? false : (iblockdata1.is(BlockTags.SNOW_LAYER_CAN_SURVIVE_ON) ? true : Block.isFaceFull(iblockdata1.getCollisionShape(world, pos.below()), Direction.UP) || iblockdata1.is((Block) this) && (Integer) iblockdata1.getValue(SnowLayerBlock.LAYERS) == 8); + } + +diff --git a/src/main/java/net/minecraft/world/level/block/SpawnerBlock.java b/src/main/java/net/minecraft/world/level/block/SpawnerBlock.java +index 4f190a40b8474aa06a92c8afcc06d0044120ff7b..66c17bdfecdfbcfb2d853e561432dd51a8f7ed46 100644 +--- a/src/main/java/net/minecraft/world/level/block/SpawnerBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/SpawnerBlock.java +@@ -42,6 +42,57 @@ public class SpawnerBlock extends BaseEntityBlock { + return createTickerHelper(type, BlockEntityType.MOB_SPAWNER, world.isClientSide ? SpawnerBlockEntity::clientTick : SpawnerBlockEntity::serverTick); + } + ++ // Purpur start ++ @Override ++ public void playerDestroy(Level level, net.minecraft.world.entity.player.Player player, BlockPos pos, BlockState state, @Nullable BlockEntity blockEntity, ItemStack stack, boolean includeDrops, boolean dropExp) { ++ if (level.purpurConfig.silkTouchEnabled && player.getBukkitEntity().hasPermission("purpur.drop.spawners") && isSilkTouch(level, stack)) { ++ ItemStack item = new ItemStack(Blocks.SPAWNER.asItem()); ++ ++ net.minecraft.world.level.SpawnData nextSpawnData = blockEntity instanceof SpawnerBlockEntity spawnerBlock ? spawnerBlock.getSpawner().nextSpawnData : null; ++ java.util.Optional> type = java.util.Optional.empty(); ++ if (nextSpawnData != null) { ++ type = net.minecraft.world.entity.EntityType.by(nextSpawnData.getEntityToSpawn()); ++ net.minecraft.world.level.SpawnData.CODEC.encodeStart(net.minecraft.nbt.NbtOps.INSTANCE, nextSpawnData).result().ifPresent(tag -> item.set(net.minecraft.core.component.DataComponents.CUSTOM_DATA, net.minecraft.world.item.component.CustomData.EMPTY.update(compoundTag -> compoundTag.put("Purpur.SpawnData", tag)))); ++ } ++ ++ if (type.isPresent()) { ++ final net.kyori.adventure.text.Component mobName = io.papermc.paper.adventure.PaperAdventure.asAdventure(type.get().getDescription()); ++ ++ String name = level.purpurConfig.silkTouchSpawnerName; ++ if (name != null && !name.isEmpty() && !name.equals("Monster Spawner")) { ++ net.kyori.adventure.text.Component displayName = net.kyori.adventure.text.minimessage.MiniMessage.miniMessage().deserialize(name, net.kyori.adventure.text.minimessage.tag.resolver.Placeholder.component("mob", mobName)); ++ if (name.startsWith("")) { ++ displayName = displayName.decoration(net.kyori.adventure.text.format.TextDecoration.ITALIC, false); ++ } ++ item.set(net.minecraft.core.component.DataComponents.CUSTOM_NAME, io.papermc.paper.adventure.PaperAdventure.asVanilla(displayName)); ++ } ++ ++ List lore = level.purpurConfig.silkTouchSpawnerLore; ++ if (lore != null && !lore.isEmpty()) { ++ ++ List loreComponentList = new java.util.ArrayList<>(); ++ for (String line : lore) { ++ net.kyori.adventure.text.Component lineComponent = net.kyori.adventure.text.minimessage.MiniMessage.miniMessage().deserialize(line, net.kyori.adventure.text.minimessage.tag.resolver.Placeholder.component("mob", mobName)); ++ if (line.startsWith("")) { ++ lineComponent = lineComponent.decoration(net.kyori.adventure.text.format.TextDecoration.ITALIC, false); ++ } ++ loreComponentList.add(io.papermc.paper.adventure.PaperAdventure.asVanilla(lineComponent)); ++ } ++ ++ item.set(net.minecraft.core.component.DataComponents.LORE, new net.minecraft.world.item.component.ItemLore(loreComponentList, loreComponentList)); ++ } ++ item.set(net.minecraft.core.component.DataComponents.HIDE_ADDITIONAL_TOOLTIP, net.minecraft.util.Unit.INSTANCE); ++ } ++ popResource(level, pos, item); ++ } ++ super.playerDestroy(level, player, pos, state, blockEntity, stack, includeDrops, dropExp); ++ } ++ ++ private boolean isSilkTouch(Level level, ItemStack stack) { ++ return stack != null && level.purpurConfig.silkTouchTools.contains(stack.getItem()) && net.minecraft.world.item.enchantment.EnchantmentHelper.getItemEnchantmentLevel(net.minecraft.world.item.enchantment.Enchantments.SILK_TOUCH, stack) >= level.purpurConfig.minimumSilkTouchSpawnerRequire; ++ } ++ // Purpur end ++ + @Override + protected void spawnAfterBreak(BlockState state, ServerLevel world, BlockPos pos, ItemStack tool, boolean dropExperience) { + super.spawnAfterBreak(state, world, pos, tool, dropExperience); +@@ -50,6 +101,7 @@ public class SpawnerBlock extends BaseEntityBlock { + + @Override + public int getExpDrop(BlockState iblockdata, ServerLevel worldserver, BlockPos blockposition, ItemStack itemstack, boolean flag) { ++ if (isSilkTouch(worldserver, itemstack)) return 0; // Purpur + if (flag) { + int i = 15 + worldserver.random.nextInt(15) + worldserver.random.nextInt(15); + +diff --git a/src/main/java/net/minecraft/world/level/block/SpongeBlock.java b/src/main/java/net/minecraft/world/level/block/SpongeBlock.java +index 902825ec9ea05f4418b45f56a008d73f217bd178..6fe44572e34ad3e3a1851e73138bd8b778eb7849 100644 +--- a/src/main/java/net/minecraft/world/level/block/SpongeBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/SpongeBlock.java +@@ -58,7 +58,7 @@ public class SpongeBlock extends Block { + + private boolean removeWaterBreadthFirstSearch(Level world, BlockPos pos) { + BlockStateListPopulator blockList = new BlockStateListPopulator(world); // CraftBukkit - Use BlockStateListPopulator +- BlockPos.breadthFirstTraversal(pos, 6, 65, (blockposition1, consumer) -> { ++ BlockPos.breadthFirstTraversal(pos, world.purpurConfig.spongeAbsorptionRadius, world.purpurConfig.spongeAbsorptionArea, (blockposition1, consumer) -> { // Purpur + Direction[] aenumdirection = SpongeBlock.ALL_DIRECTIONS; + int i = aenumdirection.length; + +@@ -77,7 +77,7 @@ public class SpongeBlock extends Block { + FluidState fluid = blockList.getFluidState(blockposition1); + // CraftBukkit end + +- if (!fluid.is(FluidTags.WATER)) { ++ if (!fluid.is(FluidTags.WATER) && (!world.purpurConfig.spongeAbsorbsLava || !fluid.is(FluidTags.LAVA)) && (!world.purpurConfig.spongeAbsorbsWaterFromMud || !iblockdata.is(Blocks.MUD))) { // Purpur + return false; + } else { + Block block = iblockdata.getBlock(); +@@ -92,6 +92,10 @@ public class SpongeBlock extends Block { + + if (iblockdata.getBlock() instanceof LiquidBlock) { + blockList.setBlock(blockposition1, Blocks.AIR.defaultBlockState(), 3); // CraftBukkit ++ // Purpur start ++ } else if (iblockdata.is(Blocks.MUD)) { ++ blockList.setBlock(blockposition1, Blocks.CLAY.defaultBlockState(), 3); ++ // Purpur end + } else { + if (!iblockdata.is(Blocks.KELP) && !iblockdata.is(Blocks.KELP_PLANT) && !iblockdata.is(Blocks.SEAGRASS) && !iblockdata.is(Blocks.TALL_SEAGRASS)) { + return false; +diff --git a/src/main/java/net/minecraft/world/level/block/StonecutterBlock.java b/src/main/java/net/minecraft/world/level/block/StonecutterBlock.java +index c6ecb378d0cb2ac05b8f22f92fb85df060038f77..b0199a8ffb1ea4cafeadedb8b833063db177b3cd 100644 +--- a/src/main/java/net/minecraft/world/level/block/StonecutterBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/StonecutterBlock.java +@@ -98,4 +98,14 @@ public class StonecutterBlock extends Block { + protected boolean isPathfindable(BlockState state, PathComputationType type) { + return false; + } ++ ++ // Purpur start ++ @Override ++ public void stepOn(Level level, BlockPos pos, BlockState state, net.minecraft.world.entity.Entity entity) { ++ if (level.purpurConfig.stonecutterDamage > 0.0F && entity instanceof net.minecraft.world.entity.LivingEntity) { ++ entity.hurt(entity.damageSources().stonecutter().directBlock(level, pos), level.purpurConfig.stonecutterDamage); ++ } ++ super.stepOn(level, pos, state, entity); ++ } ++ // Purpur end + } +diff --git a/src/main/java/net/minecraft/world/level/block/SugarCaneBlock.java b/src/main/java/net/minecraft/world/level/block/SugarCaneBlock.java +index c48c622e92cedeaa46b929c7adfedec98dd5a3fb..6449b5c424443b5f0ee7e3fce803449418fbed2a 100644 +--- a/src/main/java/net/minecraft/world/level/block/SugarCaneBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/SugarCaneBlock.java +@@ -20,7 +20,7 @@ import net.minecraft.world.level.material.FluidState; + import net.minecraft.world.phys.shapes.CollisionContext; + import net.minecraft.world.phys.shapes.VoxelShape; + +-public class SugarCaneBlock extends Block { ++public class SugarCaneBlock extends Block implements BonemealableBlock { // Purpur + + public static final MapCodec CODEC = simpleCodec(SugarCaneBlock::new); + public static final IntegerProperty AGE = BlockStateProperties.AGE_15; +@@ -113,4 +113,34 @@ public class SugarCaneBlock extends Block { + protected void createBlockStateDefinition(StateDefinition.Builder builder) { + builder.add(SugarCaneBlock.AGE); + } ++ ++ // Purpur start ++ @Override ++ public boolean isValidBonemealTarget(final LevelReader world, final BlockPos pos, final BlockState state) { ++ if (!((net.minecraft.world.level.Level) world).purpurConfig.sugarCanAffectedByBonemeal || !world.isEmptyBlock(pos.above())) return false; ++ ++ int reedHeight = 0; ++ while (world.getBlockState(pos.below(reedHeight)).is(this)) { ++ reedHeight++; ++ } ++ ++ return reedHeight < ((net.minecraft.world.level.Level) world).paperConfig().maxGrowthHeight.reeds; ++ } ++ ++ @Override ++ public boolean isBonemealSuccess(net.minecraft.world.level.Level world, RandomSource random, BlockPos pos, BlockState state) { ++ return true; ++ } ++ ++ @Override ++ public void performBonemeal(ServerLevel world, RandomSource random, BlockPos pos, BlockState state) { ++ int reedHeight = 0; ++ while (world.getBlockState(pos.below(reedHeight)).is(this)) { ++ reedHeight++; ++ } ++ for (int i = 0; i <= world.paperConfig().maxGrowthHeight.reeds - reedHeight; i++) { ++ world.setBlockAndUpdate(pos.above(i), state.setValue(SugarCaneBlock.AGE, 0)); ++ } ++ } ++ // Purpur end + } +diff --git a/src/main/java/net/minecraft/world/level/block/TurtleEggBlock.java b/src/main/java/net/minecraft/world/level/block/TurtleEggBlock.java +index a6f408e56fa6c9de82fd93555fe21e1b11ce1022..08ba90f760abb9fb62311dddd7b5bdbd0d9518d7 100644 +--- a/src/main/java/net/minecraft/world/level/block/TurtleEggBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/TurtleEggBlock.java +@@ -170,7 +170,7 @@ public class TurtleEggBlock extends Block { + private boolean shouldUpdateHatchLevel(Level world) { + float f = world.getTimeOfDay(1.0F); + +- return (double) f < 0.69D && (double) f > 0.65D ? true : world.random.nextInt(500) == 0; ++ return (double) f < 0.69D && (double) f > 0.65D ? true : world.random.nextInt(world.purpurConfig.turtleEggsRandomTickCrackChance) == 0; + } + + @Override +@@ -203,6 +203,31 @@ public class TurtleEggBlock extends Block { + } + + private boolean canDestroyEgg(Level world, Entity entity) { +- return !(entity instanceof Turtle) && !(entity instanceof Bat) ? (!(entity instanceof LivingEntity) ? false : entity instanceof Player || world.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING)) : false; ++ // Purpur start ++ if (entity instanceof Turtle || entity instanceof Bat) { ++ return false; ++ } ++ if (world.purpurConfig.turtleEggsBreakFromExpOrbs && entity instanceof net.minecraft.world.entity.ExperienceOrb) { ++ return true; ++ } ++ if (world.purpurConfig.turtleEggsBreakFromItems && entity instanceof net.minecraft.world.entity.item.ItemEntity) { ++ return true; ++ } ++ if (world.purpurConfig.turtleEggsBreakFromMinecarts && entity instanceof net.minecraft.world.entity.vehicle.AbstractMinecart) { ++ return true; ++ } ++ if (!(entity instanceof LivingEntity)) { ++ return false; ++ } ++ if (world.purpurConfig.turtleEggsTramplingFeatherFalling) { ++ java.util.Iterator armor = ((LivingEntity) entity).getArmorSlots().iterator(); ++ return !armor.hasNext() || net.minecraft.world.item.enchantment.EnchantmentHelper.getItemEnchantmentLevel(net.minecraft.world.item.enchantment.Enchantments.FEATHER_FALLING, armor.next()) < (int) entity.fallDistance; ++ } ++ if (entity instanceof Player) { ++ return true; ++ } ++ ++ return world.purpurConfig.turtleEggsBypassMobGriefing || world.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING); ++ // Purpur end + } + } +diff --git a/src/main/java/net/minecraft/world/level/block/TwistingVinesBlock.java b/src/main/java/net/minecraft/world/level/block/TwistingVinesBlock.java +index 6342bb11a162b9e6d9475c5989b1670d77e8f0fb..f8be92512446d3f0e5f0f21222bbefd04ab2838a 100644 +--- a/src/main/java/net/minecraft/world/level/block/TwistingVinesBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/TwistingVinesBlock.java +@@ -34,4 +34,11 @@ public class TwistingVinesBlock extends GrowingPlantHeadBlock { + protected boolean canGrowInto(BlockState state) { + return NetherVines.isValidGrowthState(state); + } ++ ++ // Purpur start ++ @Override ++ public int getMaxGrowthAge() { ++ return org.purpurmc.purpur.PurpurConfig.twistingVinesMaxGrowthAge; ++ } ++ // Purpur end + } +diff --git a/src/main/java/net/minecraft/world/level/block/WeepingVinesBlock.java b/src/main/java/net/minecraft/world/level/block/WeepingVinesBlock.java +index 3dec5a082606ee35a8c8d7f746480262d6a189c5..b2f6ccae9576c176263e51a232e17a08d54543b3 100644 +--- a/src/main/java/net/minecraft/world/level/block/WeepingVinesBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/WeepingVinesBlock.java +@@ -34,4 +34,11 @@ public class WeepingVinesBlock extends GrowingPlantHeadBlock { + protected boolean canGrowInto(BlockState state) { + return NetherVines.isValidGrowthState(state); + } ++ ++ // Purpur start ++ @Override ++ public int getMaxGrowthAge() { ++ return org.purpurmc.purpur.PurpurConfig.weepingVinesMaxGrowthAge; ++ } ++ // Purpur end + } +diff --git a/src/main/java/net/minecraft/world/level/block/WitherSkullBlock.java b/src/main/java/net/minecraft/world/level/block/WitherSkullBlock.java +index bbf59b2577812e74ffd45f694b83a42e043273c0..5cb06959aeaceeb98cfee34b1df804e6642f305f 100644 +--- a/src/main/java/net/minecraft/world/level/block/WitherSkullBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/WitherSkullBlock.java +@@ -79,6 +79,7 @@ public class WitherSkullBlock extends SkullBlock { + entitywither.moveTo((double) blockposition1.getX() + 0.5D, (double) blockposition1.getY() + 0.55D, (double) blockposition1.getZ() + 0.5D, shapedetector_shapedetectorcollection.getForwards().getAxis() == Direction.Axis.X ? 0.0F : 90.0F, 0.0F); + entitywither.yBodyRot = shapedetector_shapedetectorcollection.getForwards().getAxis() == Direction.Axis.X ? 0.0F : 90.0F; + entitywither.makeInvulnerable(); ++ entitywither.setSummoner(iblockdata.getBlock().placer == null ? null : iblockdata.getBlock().placer.getUUID()); // Purpur + // CraftBukkit start + if (!world.addFreshEntity(entitywither, SpawnReason.BUILD_WITHER)) { + return; +diff --git a/src/main/java/net/minecraft/world/level/block/entity/AbstractFurnaceBlockEntity.java b/src/main/java/net/minecraft/world/level/block/entity/AbstractFurnaceBlockEntity.java +index a99fe191c429bb528209dd0f31b509acf9cccbb5..c02d638b8f117156c815821207a91c55796f00b2 100644 +--- a/src/main/java/net/minecraft/world/level/block/entity/AbstractFurnaceBlockEntity.java ++++ b/src/main/java/net/minecraft/world/level/block/entity/AbstractFurnaceBlockEntity.java +@@ -45,6 +45,7 @@ import net.minecraft.world.level.Level; + import net.minecraft.world.level.block.AbstractFurnaceBlock; + import net.minecraft.world.level.block.Blocks; + import net.minecraft.world.level.block.state.BlockState; ++import net.minecraft.world.level.material.FluidState; + import net.minecraft.world.phys.Vec3; + // CraftBukkit start + import org.bukkit.craftbukkit.block.CraftBlock; +@@ -213,6 +214,22 @@ public abstract class AbstractFurnaceBlockEntity extends BaseContainerBlockEntit + } + } + ++ // Purpur start ++ public static void addFuel(ItemStack itemStack, Integer burnTime) { ++ Map map = Maps.newLinkedHashMap(); ++ map.putAll(getFuel()); ++ map.put(itemStack.getItem(), burnTime); ++ AbstractFurnaceBlockEntity.fuelCache = com.google.common.collect.ImmutableMap.copyOf(map); ++ } ++ ++ public static void removeFuel(ItemStack itemStack) { ++ Map map = Maps.newLinkedHashMap(); ++ map.putAll(getFuel()); ++ map.remove(itemStack.getItem()); ++ AbstractFurnaceBlockEntity.fuelCache = com.google.common.collect.ImmutableMap.copyOf(map); ++ } ++ // Purpur End ++ + // CraftBukkit start - add fields and methods + private int maxStack = MAX_STACK; + public List transaction = new java.util.ArrayList(); +@@ -335,6 +352,21 @@ public abstract class AbstractFurnaceBlockEntity extends BaseContainerBlockEntit + } + + ItemStack itemstack = (ItemStack) blockEntity.items.get(1); ++ // Purpur start ++ boolean usedLavaFromUnderneath = false; ++ if (world.purpurConfig.furnaceUseLavaFromUnderneath && !blockEntity.isLit() && itemstack.isEmpty() && !blockEntity.items.get(0).isEmpty() && world.getGameTime() % 20 == 0) { ++ BlockPos below = blockEntity.getBlockPos().below(); ++ BlockState belowState = world.getBlockStateIfLoaded(below); ++ if (belowState != null && belowState.is(Blocks.LAVA)) { ++ FluidState fluidState = belowState.getFluidState(); ++ if (fluidState != null && fluidState.isSource()) { ++ world.setBlock(below, Blocks.AIR.defaultBlockState(), 3); ++ itemstack = Items.LAVA_BUCKET.getDefaultInstance(); ++ usedLavaFromUnderneath = true; ++ } ++ } ++ } ++ // Purpur end + boolean flag2 = !((ItemStack) blockEntity.items.get(0)).isEmpty(); + boolean flag3 = !itemstack.isEmpty(); + +@@ -420,6 +452,7 @@ public abstract class AbstractFurnaceBlockEntity extends BaseContainerBlockEntit + setChanged(world, pos, state); + } + ++ if (usedLavaFromUnderneath) blockEntity.items.set(1, ItemStack.EMPTY); // Purpur + } + + private static boolean canBurn(RegistryAccess registryManager, @Nullable RecipeHolder recipe, NonNullList slots, int count) { +diff --git a/src/main/java/net/minecraft/world/level/block/entity/BarrelBlockEntity.java b/src/main/java/net/minecraft/world/level/block/entity/BarrelBlockEntity.java +index 6186e55014bbb9d5bedaa0e9d196879c55339d42..f4f11292d6d00f4a7c65e3e2a157bba595f70889 100644 +--- a/src/main/java/net/minecraft/world/level/block/entity/BarrelBlockEntity.java ++++ b/src/main/java/net/minecraft/world/level/block/entity/BarrelBlockEntity.java +@@ -68,7 +68,16 @@ public class BarrelBlockEntity extends RandomizableContainerBlockEntity { + + public BarrelBlockEntity(BlockPos pos, BlockState state) { + super(BlockEntityType.BARREL, pos, state); +- this.items = NonNullList.withSize(27, ItemStack.EMPTY); ++ // Purpur start ++ this.items = NonNullList.withSize(switch (org.purpurmc.purpur.PurpurConfig.barrelRows) { ++ case 6 -> 54; ++ case 5 -> 45; ++ case 4 -> 36; ++ case 2 -> 18; ++ case 1 -> 9; ++ default -> 27; ++ }, ItemStack.EMPTY); ++ // Purpur end + this.openersCounter = new ContainerOpenersCounter() { + @Override + protected void onOpen(Level world, BlockPos pos, BlockState state) { +@@ -119,7 +128,16 @@ public class BarrelBlockEntity extends RandomizableContainerBlockEntity { + + @Override + public int getContainerSize() { +- return 27; ++ // Purpur start ++ return switch (org.purpurmc.purpur.PurpurConfig.barrelRows) { ++ case 6 -> 54; ++ case 5 -> 45; ++ case 4 -> 36; ++ case 2 -> 18; ++ case 1 -> 9; ++ default -> 27; ++ }; ++ // Purpur end + } + + @Override +@@ -139,7 +157,16 @@ public class BarrelBlockEntity extends RandomizableContainerBlockEntity { + + @Override + protected AbstractContainerMenu createMenu(int syncId, Inventory playerInventory) { +- return ChestMenu.threeRows(syncId, playerInventory, this); ++ // Purpur start ++ return switch (org.purpurmc.purpur.PurpurConfig.barrelRows) { ++ case 6 -> ChestMenu.sixRows(syncId, playerInventory, this); ++ case 5 -> ChestMenu.fiveRows(syncId, playerInventory, this); ++ case 4 -> ChestMenu.fourRows(syncId, playerInventory, this); ++ case 2 -> ChestMenu.twoRows(syncId, playerInventory, this); ++ case 1 -> ChestMenu.oneRow(syncId, playerInventory, this); ++ default -> ChestMenu.threeRows(syncId, playerInventory, this); ++ }; ++ // Purpur end + } + + @Override +diff --git a/src/main/java/net/minecraft/world/level/block/entity/BeaconBlockEntity.java b/src/main/java/net/minecraft/world/level/block/entity/BeaconBlockEntity.java +index a6ffbbc1b5021564864e42c0756342352c2b8290..5ad48d2003fbd83e60f6faa68532496131935828 100644 +--- a/src/main/java/net/minecraft/world/level/block/entity/BeaconBlockEntity.java ++++ b/src/main/java/net/minecraft/world/level/block/entity/BeaconBlockEntity.java +@@ -92,6 +92,16 @@ public class BeaconBlockEntity extends BlockEntity implements MenuProvider, Name + + public double getEffectRange() { + if (this.effectRange < 0) { ++ // Purpur Start ++ if (this.level != null) { ++ switch (this.levels) { ++ case 1: return this.level.purpurConfig.beaconLevelOne; ++ case 2: return this.level.purpurConfig.beaconLevelTwo; ++ case 3: return this.level.purpurConfig.beaconLevelThree; ++ case 4: return this.level.purpurConfig.beaconLevelFour; ++ } ++ } ++ // Purpur End + return this.levels * 10 + 10; + } else { + return effectRange; +@@ -168,6 +178,7 @@ public class BeaconBlockEntity extends BlockEntity implements MenuProvider, Name + int j = pos.getY(); + int k = pos.getZ(); + BlockPos blockposition1; ++ boolean isTintedGlass = false; + + if (blockEntity.lastCheckY < j) { + blockposition1 = pos; +@@ -201,6 +212,9 @@ public class BeaconBlockEntity extends BlockEntity implements MenuProvider, Name + } + } + } else { ++ if (world.purpurConfig.beaconAllowEffectsWithTintedGlass && block.equals(Blocks.TINTED_GLASS)) { ++ isTintedGlass = true; ++ } + if (tileentitybeacon_beaconcolortracker == null || iblockdata1.getLightBlock(world, blockposition1) >= 15 && !iblockdata1.is(Blocks.BEDROCK)) { + blockEntity.checkingBeamSections.clear(); + blockEntity.lastCheckY = l; +@@ -220,7 +234,7 @@ public class BeaconBlockEntity extends BlockEntity implements MenuProvider, Name + blockEntity.levels = BeaconBlockEntity.updateBase(world, i, j, k); + } + +- if (blockEntity.levels > 0 && !blockEntity.beamSections.isEmpty()) { ++ if (blockEntity.levels > 0 && (!blockEntity.beamSections.isEmpty() || (world.purpurConfig.beaconAllowEffectsWithTintedGlass && isTintedGlass))) { + BeaconBlockEntity.applyEffects(world, pos, blockEntity.levels, blockEntity.primaryPower, blockEntity.secondaryPower, blockEntity); // Paper - Custom beacon ranges + BeaconBlockEntity.playSound(world, pos, SoundEvents.BEACON_AMBIENT); + } +diff --git a/src/main/java/net/minecraft/world/level/block/entity/BeehiveBlockEntity.java b/src/main/java/net/minecraft/world/level/block/entity/BeehiveBlockEntity.java +index 7b263fab4f0014400b3b8e7e33db32f9a125f6ba..f7a6ab35c95ffda73f17843916ddb624ad290b42 100644 +--- a/src/main/java/net/minecraft/world/level/block/entity/BeehiveBlockEntity.java ++++ b/src/main/java/net/minecraft/world/level/block/entity/BeehiveBlockEntity.java +@@ -59,7 +59,7 @@ public class BeehiveBlockEntity extends BlockEntity { + private final List stored = Lists.newArrayList(); + @Nullable + public BlockPos savedFlowerPos; +- public int maxBees = 3; // CraftBukkit - allow setting max amount of bees a hive can hold ++ public int maxBees = org.purpurmc.purpur.PurpurConfig.beeInsideBeeHive; // CraftBukkit - allow setting max amount of bees a hive can hold // Purpur + + public BeehiveBlockEntity(BlockPos pos, BlockState state) { + super(BlockEntityType.BEEHIVE, pos, state); +@@ -146,11 +146,33 @@ public class BeehiveBlockEntity extends BlockEntity { + return list; + } + ++ // Purpur start ++ public List releaseBee(BlockState iblockdata, BeehiveBlockEntity.BeeData data, BeehiveBlockEntity.BeeReleaseStatus tileentitybeehive_releasestatus, boolean force) { ++ List list = Lists.newArrayList(); ++ ++ BeehiveBlockEntity.releaseOccupant(this.level, this.worldPosition, iblockdata, data.occupant, list, tileentitybeehive_releasestatus, this.savedFlowerPos, force); ++ ++ if (!list.isEmpty()) { ++ stored.remove(data); ++ ++ super.setChanged(); ++ } ++ ++ return list; ++ } ++ // Purpur end ++ + @VisibleForDebug + public int getOccupantCount() { + return this.stored.size(); + } + ++ // Purpur start ++ public List getStored() { ++ return stored; ++ } ++ // Purpur end ++ + // Paper start - Add EntityBlockStorage clearEntities + public void clearBees() { + this.stored.clear(); +@@ -212,7 +234,7 @@ public class BeehiveBlockEntity extends BlockEntity { + } + + private static boolean releaseOccupant(Level world, BlockPos blockposition, BlockState iblockdata, BeehiveBlockEntity.Occupant tileentitybeehive_c, @Nullable List list, BeehiveBlockEntity.BeeReleaseStatus tileentitybeehive_releasestatus, @Nullable BlockPos blockposition1, boolean force) { +- if (!force && (world.isNight() || world.isRaining()) && tileentitybeehive_releasestatus != BeehiveBlockEntity.BeeReleaseStatus.EMERGENCY) { ++ if (!force && ((world.isNight() && !world.purpurConfig.beeCanWorkAtNight) || (world.isRaining() && !world.purpurConfig.beeCanWorkInRain)) && tileentitybeehive_releasestatus != BeehiveBlockEntity.BeeReleaseStatus.EMERGENCY) { // Purpur + // CraftBukkit end + return false; + } else { +@@ -471,9 +493,9 @@ public class BeehiveBlockEntity extends BlockEntity { + } + } + +- private static class BeeData { ++ public static class BeeData { // Purpur - change from private to public + +- private final BeehiveBlockEntity.Occupant occupant; ++ public final BeehiveBlockEntity.Occupant occupant; // Purpur - make public + private int exitTickCounter; // Paper - Fix bees aging inside hives; separate counter for checking if bee should exit to reduce exit attempts + private int ticksInHive; + +diff --git a/src/main/java/net/minecraft/world/level/block/entity/ConduitBlockEntity.java b/src/main/java/net/minecraft/world/level/block/entity/ConduitBlockEntity.java +index 73e532dc998e5701c1a73da846da3d3a79871b81..ff6fea842ca80c2ba51693fe62e5b74f9affdc19 100644 +--- a/src/main/java/net/minecraft/world/level/block/entity/ConduitBlockEntity.java ++++ b/src/main/java/net/minecraft/world/level/block/entity/ConduitBlockEntity.java +@@ -168,7 +168,7 @@ public class ConduitBlockEntity extends BlockEntity { + if ((l > 1 || i1 > 1 || j1 > 1) && (i == 0 && (i1 == 2 || j1 == 2) || j == 0 && (l == 2 || j1 == 2) || k == 0 && (l == 2 || i1 == 2))) { + BlockPos blockposition2 = pos.offset(i, j, k); + BlockState iblockdata = world.getBlockState(blockposition2); +- Block[] ablock = ConduitBlockEntity.VALID_BLOCKS; ++ Block[] ablock = world.purpurConfig.conduitBlocks; // Purpur + int k1 = ablock.length; + + for (int l1 = 0; l1 < k1; ++l1) { +@@ -188,13 +188,13 @@ public class ConduitBlockEntity extends BlockEntity { + + private static void applyEffects(Level world, BlockPos pos, List activatingBlocks) { + // CraftBukkit start +- ConduitBlockEntity.applyEffects(world, pos, ConduitBlockEntity.getRange(activatingBlocks)); ++ ConduitBlockEntity.applyEffects(world, pos, ConduitBlockEntity.getRange(activatingBlocks, world)); // Purpur + } + +- public static int getRange(List list) { ++ public static int getRange(List list, Level world) { // Purpur + // CraftBukkit end + int i = list.size(); +- int j = i / 7 * 16; ++ int j = i / 7 * world.purpurConfig.conduitDistance; // Purpur + // CraftBukkit start + return j; + } +@@ -237,20 +237,20 @@ public class ConduitBlockEntity extends BlockEntity { + tileentityconduit.destroyTarget = ConduitBlockEntity.findDestroyTarget(world, blockposition, tileentityconduit.destroyTargetUUID); + tileentityconduit.destroyTargetUUID = null; + } else if (tileentityconduit.destroyTarget == null) { +- List list1 = world.getEntitiesOfClass(LivingEntity.class, ConduitBlockEntity.getDestroyRangeAABB(blockposition), (entityliving1) -> { ++ List list1 = world.getEntitiesOfClass(LivingEntity.class, ConduitBlockEntity.getDestroyRangeAABB(blockposition, world), (entityliving1) -> { // Purpur + return entityliving1 instanceof Enemy && entityliving1.isInWaterOrRain(); + }); + + if (!list1.isEmpty()) { + tileentityconduit.destroyTarget = (LivingEntity) list1.get(world.random.nextInt(list1.size())); + } +- } else if (!tileentityconduit.destroyTarget.isAlive() || !blockposition.closerThan(tileentityconduit.destroyTarget.blockPosition(), 8.0D)) { ++ } else if (!tileentityconduit.destroyTarget.isAlive() || !blockposition.closerThan(tileentityconduit.destroyTarget.blockPosition(), world.purpurConfig.conduitDamageDistance)) { // Purpur + tileentityconduit.destroyTarget = null; + } + + // CraftBukkit start + if (damageTarget && tileentityconduit.destroyTarget != null) { +- if (tileentityconduit.destroyTarget.hurt(world.damageSources().magic().directBlock(world, blockposition), 4.0F)) { ++ if (tileentityconduit.destroyTarget.hurt(world.damageSources().magic().directBlock(world, blockposition), world.purpurConfig.conduitDamageAmount)) { // Purpur + world.playSound(null, tileentityconduit.destroyTarget.getX(), tileentityconduit.destroyTarget.getY(), tileentityconduit.destroyTarget.getZ(), SoundEvents.CONDUIT_ATTACK_TARGET, SoundSource.BLOCKS, 1.0F, 1.0F); + } + // CraftBukkit end +@@ -275,16 +275,22 @@ public class ConduitBlockEntity extends BlockEntity { + } + + public static AABB getDestroyRangeAABB(BlockPos pos) { ++ // Purpur start ++ return getDestroyRangeAABB(pos, null); ++ } ++ ++ private static AABB getDestroyRangeAABB(BlockPos pos, Level level) { ++ // Purpur end + int i = pos.getX(); + int j = pos.getY(); + int k = pos.getZ(); + +- return (new AABB((double) i, (double) j, (double) k, (double) (i + 1), (double) (j + 1), (double) (k + 1))).inflate(8.0D); ++ return (new AABB((double) i, (double) j, (double) k, (double) (i + 1), (double) (j + 1), (double) (k + 1))).inflate(level == null ? 8.0D : level.purpurConfig.conduitDamageDistance); // Purpur + } + + @Nullable + private static LivingEntity findDestroyTarget(Level world, BlockPos pos, UUID uuid) { +- List list = world.getEntitiesOfClass(LivingEntity.class, ConduitBlockEntity.getDestroyRangeAABB(pos), (entityliving) -> { ++ List list = world.getEntitiesOfClass(LivingEntity.class, ConduitBlockEntity.getDestroyRangeAABB(pos, world), (entityliving) -> { // Purpur + return entityliving.getUUID().equals(uuid); + }); + +diff --git a/src/main/java/net/minecraft/world/level/block/entity/EnchantingTableBlockEntity.java b/src/main/java/net/minecraft/world/level/block/entity/EnchantingTableBlockEntity.java +index d47bc2f54c4722a0b8c419b99ee57eb3cb25d750..fdeabdcc781b605d6f3ee18528fd380ffff95660 100644 +--- a/src/main/java/net/minecraft/world/level/block/entity/EnchantingTableBlockEntity.java ++++ b/src/main/java/net/minecraft/world/level/block/entity/EnchantingTableBlockEntity.java +@@ -28,6 +28,7 @@ public class EnchantingTableBlockEntity extends BlockEntity implements Nameable + private static final RandomSource RANDOM = RandomSource.create(); + @Nullable + private Component name; ++ private int lapis = 0; // Purpur + + public EnchantingTableBlockEntity(BlockPos pos, BlockState state) { + super(BlockEntityType.ENCHANTING_TABLE, pos, state); +@@ -39,6 +40,7 @@ public class EnchantingTableBlockEntity extends BlockEntity implements Nameable + if (this.hasCustomName()) { + nbt.putString("CustomName", Component.Serializer.toJson(this.name, registryLookup)); + } ++ nbt.putInt("Purpur.Lapis", this.lapis); // Purpur + } + + @Override +@@ -47,6 +49,7 @@ public class EnchantingTableBlockEntity extends BlockEntity implements Nameable + if (nbt.contains("CustomName", 8)) { + this.name = parseCustomNameSafe(nbt.getString("CustomName"), registryLookup); + } ++ this.lapis = nbt.getInt("Purpur.Lapis"); // Purpur + } + + public static void bookAnimationTick(Level world, BlockPos pos, BlockState state, EnchantingTableBlockEntity blockEntity) { +@@ -138,4 +141,14 @@ public class EnchantingTableBlockEntity extends BlockEntity implements Nameable + public void removeComponentsFromTag(CompoundTag nbt) { + nbt.remove("CustomName"); + } ++ ++ // Purpur start ++ public int getLapis() { ++ return this.lapis; ++ } ++ ++ public void setLapis(int lapis) { ++ this.lapis = lapis; ++ } ++ // Purpur + } +diff --git a/src/main/java/net/minecraft/world/level/block/entity/TheEndGatewayBlockEntity.java b/src/main/java/net/minecraft/world/level/block/entity/TheEndGatewayBlockEntity.java +index 93bd70c1dc2ba8b893a6087730071c81fb1132f4..fb9611866671880fc7a1a969da928b8f2ad15269 100644 +--- a/src/main/java/net/minecraft/world/level/block/entity/TheEndGatewayBlockEntity.java ++++ b/src/main/java/net/minecraft/world/level/block/entity/TheEndGatewayBlockEntity.java +@@ -167,6 +167,15 @@ public class TheEndGatewayBlockEntity extends TheEndPortalBlockEntity { + + public static void teleportEntity(Level world, BlockPos pos, BlockState state, Entity entity, TheEndGatewayBlockEntity blockEntity) { + if (world instanceof ServerLevel worldserver && !blockEntity.isCoolingDown()) { ++ if (!entity.canChangeDimensions()) return; // Purpur ++ // Purpur start ++ if (world.purpurConfig.imposeTeleportRestrictionsOnGateways && (entity.isVehicle() || entity.isPassenger())) { ++ if (new org.purpurmc.purpur.event.entity.EntityTeleportHinderedEvent(entity.getBukkitEntity(), entity.isPassenger() ? org.purpurmc.purpur.event.entity.EntityTeleportHinderedEvent.Reason.IS_PASSENGER : org.purpurmc.purpur.event.entity.EntityTeleportHinderedEvent.Reason.IS_VEHICLE, PlayerTeleportEvent.TeleportCause.END_GATEWAY).callEvent()) { ++ teleportEntity(world, pos, state, entity, blockEntity); ++ } ++ return; ++ } ++ // Purpur end + blockEntity.teleportCooldown = 100; + BlockPos blockposition1; + +diff --git a/src/main/java/net/minecraft/world/level/block/piston/PistonStructureResolver.java b/src/main/java/net/minecraft/world/level/block/piston/PistonStructureResolver.java +index 205e223c356634bd6bc6bd58c6f0b7fda61a6f5f..bea05cb928d540a2f19b51bb7352d032b2dd69cd 100644 +--- a/src/main/java/net/minecraft/world/level/block/piston/PistonStructureResolver.java ++++ b/src/main/java/net/minecraft/world/level/block/piston/PistonStructureResolver.java +@@ -81,7 +81,7 @@ public class PistonStructureResolver { + return true; + } else { + int i = 1; +- if (i + this.toPush.size() > 12) { ++ if (i + this.toPush.size() > this.level.purpurConfig.pistonBlockPushLimit) { // Purpur + return false; + } else { + while (isSticky(blockState)) { +@@ -95,7 +95,7 @@ public class PistonStructureResolver { + break; + } + +- if (++i + this.toPush.size() > 12) { ++ if (++i + this.toPush.size() > this.level.purpurConfig.pistonBlockPushLimit) { // Purpur + return false; + } + } +@@ -140,7 +140,7 @@ public class PistonStructureResolver { + return true; + } + +- if (this.toPush.size() >= 12) { ++ if (this.toPush.size() >= this.level.purpurConfig.pistonBlockPushLimit) { // Purpur + return false; + } + +diff --git a/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java b/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java +index 2034ca2edd3aff61d94416266e75402babd3e741..031fc626d2075cbe0941fecc188406712ab9953f 100644 +--- a/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java ++++ b/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java +@@ -86,7 +86,7 @@ public abstract class BlockBehaviour implements FeatureElement { + + protected static final Direction[] UPDATE_SHAPE_ORDER = new Direction[]{Direction.WEST, Direction.EAST, Direction.NORTH, Direction.SOUTH, Direction.DOWN, Direction.UP}; + public final boolean hasCollision; +- protected final float explosionResistance; ++ public float explosionResistance; // Purpur - protected final -> public + protected final boolean isRandomlyTicking; + protected final SoundType soundType; + protected final float friction; +@@ -94,7 +94,7 @@ public abstract class BlockBehaviour implements FeatureElement { + protected final float jumpFactor; + protected final boolean dynamicShape; + protected final FeatureFlagSet requiredFeatures; +- protected final BlockBehaviour.Properties properties; ++ public final BlockBehaviour.Properties properties; // Purpur - protected -> public + @Nullable + protected ResourceKey drops; + +diff --git a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java +index 2a8609e33716949ff1877b6d10f64a9d7a7c81e9..fd637415625fdabcac07e120e9168d09c06141d4 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java ++++ b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java +@@ -443,11 +443,11 @@ public class LevelChunk extends ChunkAccess { + if (LightEngine.hasDifferentLightProperties(this, blockposition, iblockdata1, iblockdata)) { + ProfilerFiller gameprofilerfiller = this.level.getProfiler(); + +- gameprofilerfiller.push("updateSkyLightSources"); ++ //gameprofilerfiller.push("updateSkyLightSources"); // Purpur + // Paper - starlight - remove skyLightSources +- gameprofilerfiller.popPush("queueCheckLight"); ++ //gameprofilerfiller.popPush("queueCheckLight"); // Purpur + this.level.getChunkSource().getLightEngine().checkBlock(blockposition); +- gameprofilerfiller.pop(); ++ //gameprofilerfiller.pop(); // Purpur + } + + boolean flag3 = iblockdata1.hasBlockEntity(); +@@ -785,7 +785,7 @@ public class LevelChunk extends ChunkAccess { + this.chunkHolder.getEntityChunk().callEntitiesLoadEvent(); // Paper - rewrite chunk system + + if (this.needsDecoration) { +- try (co.aikar.timings.Timing ignored = this.level.timings.chunkLoadPopulate.startTiming()) { // Paper ++ //try (co.aikar.timings.Timing ignored = this.level.timings.chunkLoadPopulate.startTiming()) { // Paper // Purpur + this.needsDecoration = false; + java.util.Random random = new java.util.Random(); + random.setSeed(this.level.getSeed()); +@@ -805,7 +805,7 @@ public class LevelChunk extends ChunkAccess { + } + } + server.getPluginManager().callEvent(new org.bukkit.event.world.ChunkPopulateEvent(bukkitChunk)); +- } // Paper ++ //} // Paper // Purpur + } + } + } +@@ -1158,10 +1158,10 @@ public class LevelChunk extends ChunkAccess { + + if (LevelChunk.this.isTicking(blockposition)) { + try { +- ProfilerFiller gameprofilerfiller = LevelChunk.this.level.getProfiler(); ++ //ProfilerFiller gameprofilerfiller = LevelChunk.this.level.getProfiler(); + +- gameprofilerfiller.push(this::getType); +- this.blockEntity.tickTimer.startTiming(); // Spigot ++ //gameprofilerfiller.push(this::getType); ++ //this.blockEntity.tickTimer.startTiming(); // Spigot // Purpur + BlockState iblockdata = LevelChunk.this.getBlockState(blockposition); + + if (this.blockEntity.getType().isValid(iblockdata)) { +@@ -1177,7 +1177,7 @@ public class LevelChunk extends ChunkAccess { + // Paper end - Remove the Block Entity if it's invalid + } + +- gameprofilerfiller.pop(); ++ //gameprofilerfiller.pop(); + } catch (Throwable throwable) { + if (throwable instanceof ThreadDeath) throw throwable; // Paper + // Paper start - Prevent block entity and entity crashes +@@ -1188,7 +1188,7 @@ public class LevelChunk extends ChunkAccess { + // Paper end - Prevent block entity and entity crashes + // Spigot start + } finally { +- this.blockEntity.tickTimer.stopTiming(); ++ //this.blockEntity.tickTimer.stopTiming(); // Purpur + // Spigot end + } + } +diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/EntityStorage.java b/src/main/java/net/minecraft/world/level/chunk/storage/EntityStorage.java +index bee39dee1b96023c907407877aedf3aafaf5e1b8..5b6bc200381a486c99061f9f5b7121c2c355b477 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/storage/EntityStorage.java ++++ b/src/main/java/net/minecraft/world/level/chunk/storage/EntityStorage.java +@@ -107,6 +107,7 @@ public class EntityStorage implements EntityPersistentStorage { + ListTag listTag = new ListTag(); + final java.util.Map, Integer> savedEntityCounts = new java.util.HashMap<>(); // Paper - Entity load/save limit per chunk + entities.forEach((entity) -> { // diff here: use entities parameter ++ if (!entity.canSaveToDisk()) return; // Purpur + // Paper start - Entity load/save limit per chunk + final EntityType entityType = entity.getType(); + final int saveLimit = level.paperConfig().chunks.entityPerChunkSaveLimit.getOrDefault(entityType, -1); +diff --git a/src/main/java/net/minecraft/world/level/levelgen/PhantomSpawner.java b/src/main/java/net/minecraft/world/level/levelgen/PhantomSpawner.java +index 1b1b475ca27e799e251d6f8a8c9fe1a4fd8bae83..04f67f7b43d2f461c776c76614dc3e5f060aea63 100644 +--- a/src/main/java/net/minecraft/world/level/levelgen/PhantomSpawner.java ++++ b/src/main/java/net/minecraft/world/level/levelgen/PhantomSpawner.java +@@ -48,7 +48,7 @@ public class PhantomSpawner implements CustomSpawner { + int spawnAttemptMaxSeconds = world.paperConfig().entities.behavior.phantomsSpawnAttemptMaxSeconds; + this.nextTick += (spawnAttemptMinSeconds + randomsource.nextInt(spawnAttemptMaxSeconds - spawnAttemptMinSeconds + 1)) * 20; + // Paper end - Ability to control player's insomnia and phantoms +- if (world.getSkyDarken() < 5 && world.dimensionType().hasSkyLight()) { ++ if (world.getSkyDarken() < world.purpurConfig.phantomSpawnMinSkyDarkness && world.dimensionType().hasSkyLight()) { // Purpur + return 0; + } else { + int i = 0; +@@ -60,10 +60,10 @@ public class PhantomSpawner implements CustomSpawner { + if (!entityplayer.isSpectator() && (!world.paperConfig().entities.behavior.phantomsDoNotSpawnOnCreativePlayers || !entityplayer.isCreative())) { // Paper - Add phantom creative and insomniac controls + BlockPos blockposition = entityplayer.blockPosition(); + +- if (!world.dimensionType().hasSkyLight() || blockposition.getY() >= world.getSeaLevel() && world.canSeeSky(blockposition)) { ++ if (!world.dimensionType().hasSkyLight() || (!world.purpurConfig.phantomSpawnOnlyAboveSeaLevel || blockposition.getY() >= world.getSeaLevel()) && (!world.purpurConfig.phantomSpawnOnlyWithVisibleSky || world.canSeeSky(blockposition))) { // Purpur + DifficultyInstance difficultydamagescaler = world.getCurrentDifficultyAt(blockposition); + +- if (difficultydamagescaler.isHarderThan(randomsource.nextFloat() * 3.0F)) { ++ if (difficultydamagescaler.isHarderThan(randomsource.nextFloat() * (float) world.purpurConfig.phantomSpawnLocalDifficultyChance)) { // Purpur + ServerStatsCounter serverstatisticmanager = entityplayer.getStats(); + int j = Mth.clamp(serverstatisticmanager.getValue(Stats.CUSTOM.get(Stats.TIME_SINCE_REST)), 1, Integer.MAX_VALUE); + boolean flag2 = true; +@@ -75,7 +75,7 @@ public class PhantomSpawner implements CustomSpawner { + + if (NaturalSpawner.isValidEmptySpawnBlock(world, blockposition1, iblockdata, fluid, EntityType.PHANTOM)) { + SpawnGroupData groupdataentity = null; +- int k = 1 + randomsource.nextInt(difficultydamagescaler.getDifficulty().getId() + 1); ++ int k = world.purpurConfig.phantomSpawnMinPerAttempt + world.random.nextInt((world.purpurConfig.phantomSpawnMaxPerAttempt < 0 ? difficultydamagescaler.getDifficulty().getId() : world.purpurConfig.phantomSpawnMaxPerAttempt - world.purpurConfig.phantomSpawnMinPerAttempt) + 1); // Purpur + + for (int l = 0; l < k; ++l) { + // Paper start - PhantomPreSpawnEvent +diff --git a/src/main/java/net/minecraft/world/level/material/FlowingFluid.java b/src/main/java/net/minecraft/world/level/material/FlowingFluid.java +index c2943d892b067b3f1fb3b93301a092e912d71f08..a091c51476214977d7a9729b5c72e8478fe4a391 100644 +--- a/src/main/java/net/minecraft/world/level/material/FlowingFluid.java ++++ b/src/main/java/net/minecraft/world/level/material/FlowingFluid.java +@@ -217,7 +217,7 @@ public abstract class FlowingFluid extends Fluid { + } + } + +- if (this.canConvertToSource(world) && j >= 2) { ++ if (this.canConvertToSource(world) && j >= getRequiredSources(world)) { + BlockState iblockdata2 = world.getBlockState(pos.below()); + FluidState fluid1 = iblockdata2.getFluidState(); + +@@ -301,6 +301,12 @@ public abstract class FlowingFluid extends Fluid { + + protected abstract boolean canConvertToSource(Level world); + ++ // Purpur start ++ protected int getRequiredSources(Level level) { ++ return 2; ++ } ++ // Purpur end ++ + protected void spreadTo(LevelAccessor world, BlockPos pos, BlockState state, Direction direction, FluidState fluidState) { + if (state.getBlock() instanceof LiquidBlockContainer) { + ((LiquidBlockContainer) state.getBlock()).placeLiquid(world, pos, state, fluidState); +diff --git a/src/main/java/net/minecraft/world/level/material/LavaFluid.java b/src/main/java/net/minecraft/world/level/material/LavaFluid.java +index 3bb4a9a1a6249e8ba2de237f801210e7f4fd5825..2d492d849ff73a738dfbcb16507feb89bf19a962 100644 +--- a/src/main/java/net/minecraft/world/level/material/LavaFluid.java ++++ b/src/main/java/net/minecraft/world/level/material/LavaFluid.java +@@ -180,7 +180,7 @@ public abstract class LavaFluid extends FlowingFluid { + + @Override + public int getTickDelay(LevelReader world) { +- return world.dimensionType().ultraWarm() ? 10 : 30; ++ return world.dimensionType().ultraWarm() ? world.getWorldBorder().world.purpurConfig.lavaSpeedNether : world.getWorldBorder().world.purpurConfig.lavaSpeedNotNether; // Purpur + } + + @Override +@@ -198,6 +198,13 @@ public abstract class LavaFluid extends FlowingFluid { + world.levelEvent(1501, pos, 0); + } + ++ // Purpur start ++ @Override ++ protected int getRequiredSources(Level level) { ++ return level.purpurConfig.lavaInfiniteRequiredSources; ++ } ++ // Purpur end ++ + @Override + protected boolean canConvertToSource(Level world) { + return world.getGameRules().getBoolean(GameRules.RULE_LAVA_SOURCE_CONVERSION); +diff --git a/src/main/java/net/minecraft/world/level/material/WaterFluid.java b/src/main/java/net/minecraft/world/level/material/WaterFluid.java +index 109f71401c65f476ccf6813137386fc9fef10254..9dcdb2f4001115db0c26fdbf86531dbe6098485d 100644 +--- a/src/main/java/net/minecraft/world/level/material/WaterFluid.java ++++ b/src/main/java/net/minecraft/world/level/material/WaterFluid.java +@@ -80,6 +80,13 @@ public abstract class WaterFluid extends FlowingFluid { + return world.getGameRules().getBoolean(GameRules.RULE_WATER_SOURCE_CONVERSION); + } + ++ // Purpur start ++ @Override ++ protected int getRequiredSources(Level level) { ++ return level.purpurConfig.waterInfiniteRequiredSources; ++ } ++ // Purpur end ++ + // Paper start - Add BlockBreakBlockEvent + @Override + protected void beforeDestroyingBlock(LevelAccessor world, BlockPos pos, BlockState state, BlockPos source) { +diff --git a/src/main/java/net/minecraft/world/level/pathfinder/PathFinder.java b/src/main/java/net/minecraft/world/level/pathfinder/PathFinder.java +index 18bbb3f8f99849333ff4bc020c8ce758a69312a5..404080976208c30e9e95e5bee47c2a749e709a45 100644 +--- a/src/main/java/net/minecraft/world/level/pathfinder/PathFinder.java ++++ b/src/main/java/net/minecraft/world/level/pathfinder/PathFinder.java +@@ -53,8 +53,8 @@ public class PathFinder { + @Nullable + // Paper start - Perf: remove streams and optimize collection + private Path findPath(ProfilerFiller profiler, Node startNode, List> positions, float followRange, int distance, float rangeMultiplier) { +- profiler.push("find_path"); +- profiler.markForCharting(MetricCategory.PATH_FINDING); ++ //profiler.push("find_path"); // Purpur ++ //profiler.markForCharting(MetricCategory.PATH_FINDING); // Purpur + // Set set = positions.keySet(); + startNode.g = 0.0F; + startNode.h = this.getBestH(startNode, positions); // Paper - optimize collection +@@ -122,7 +122,7 @@ public class PathFinder { + if (best == null || comparator.compare(path, best) < 0) + best = path; + } +- profiler.pop(); ++ //profiler.pop(); // Purpur + return best; + // Paper end - Perf: remove streams and optimize collection + } +diff --git a/src/main/java/net/minecraft/world/level/pathfinder/WalkNodeEvaluator.java b/src/main/java/net/minecraft/world/level/pathfinder/WalkNodeEvaluator.java +index d5004290e40a1ff5e0fcfe75f8da34ae15962359..f26383cf896785333dbd6f86348d5a5f67a6731f 100644 +--- a/src/main/java/net/minecraft/world/level/pathfinder/WalkNodeEvaluator.java ++++ b/src/main/java/net/minecraft/world/level/pathfinder/WalkNodeEvaluator.java +@@ -240,7 +240,7 @@ public class WalkNodeEvaluator extends NodeEvaluator { + if ((node == null || node.costMalus < 0.0F) + && maxYStep > 0 + && (pathType != PathType.FENCE || this.canWalkOverFences()) +- && pathType != PathType.UNPASSABLE_RAIL ++ && (this.mob.level().purpurConfig.mobsIgnoreRails || pathType != PathType.UNPASSABLE_RAIL) // Purpur + && pathType != PathType.TRAPDOOR + && pathType != PathType.POWDER_SNOW) { + node = this.tryJumpOn(x, y, z, maxYStep, prevFeetY, direction, nodeType, mutableBlockPos); +@@ -491,7 +491,7 @@ public class WalkNodeEvaluator extends NodeEvaluator { + return PathType.TRAPDOOR; + } else if (blockState.is(Blocks.POWDER_SNOW)) { + return PathType.POWDER_SNOW; +- } else if (blockState.is(Blocks.CACTUS) || blockState.is(Blocks.SWEET_BERRY_BUSH)) { ++ } else if (blockState.is(Blocks.CACTUS) || blockState.is(Blocks.SWEET_BERRY_BUSH) || blockState.is(Blocks.STONECUTTER)) { // Purpur + return PathType.DAMAGE_OTHER; + } else if (blockState.is(Blocks.HONEY_BLOCK)) { + return PathType.STICKY_HONEY; +diff --git a/src/main/java/net/minecraft/world/level/portal/PortalShape.java b/src/main/java/net/minecraft/world/level/portal/PortalShape.java +index af24467ee37cfc06f692b3b02e68f6cfbaaa8d59..afe6b2170846273b41b694aa53dca4c31bf78b3f 100644 +--- a/src/main/java/net/minecraft/world/level/portal/PortalShape.java ++++ b/src/main/java/net/minecraft/world/level/portal/PortalShape.java +@@ -33,7 +33,7 @@ public class PortalShape { + private static final int MIN_HEIGHT = 3; + public static final int MAX_HEIGHT = 21; + private static final BlockBehaviour.StatePredicate FRAME = (iblockdata, iblockaccess, blockposition) -> { +- return iblockdata.is(Blocks.OBSIDIAN); ++ return iblockdata.is(Blocks.OBSIDIAN) || (org.purpurmc.purpur.PurpurConfig.cryingObsidianValidForPortalFrame && iblockdata.is(Blocks.CRYING_OBSIDIAN)); // Purpur + }; + private static final float SAFE_TRAVEL_MAX_ENTITY_XY = 4.0F; + private static final double SAFE_TRAVEL_MAX_VERTICAL_DELTA = 1.0D; +diff --git a/src/main/java/net/minecraft/world/level/saveddata/maps/MapItemSavedData.java b/src/main/java/net/minecraft/world/level/saveddata/maps/MapItemSavedData.java +index cf8ae635fce7ea66d4e1ab1dc05575f035fa95ef..e26f6215ca42885cb0635a3183a8df93a924ba7f 100644 +--- a/src/main/java/net/minecraft/world/level/saveddata/maps/MapItemSavedData.java ++++ b/src/main/java/net/minecraft/world/level/saveddata/maps/MapItemSavedData.java +@@ -79,6 +79,7 @@ public class MapItemSavedData extends SavedData { + private final Map frameMarkers = Maps.newHashMap(); + private int trackedDecorationCount; + private org.bukkit.craftbukkit.map.RenderData vanillaRender = new org.bukkit.craftbukkit.map.RenderData(); // Paper ++ public boolean isExplorerMap; // Purpur + + // CraftBukkit start + public final CraftMapView mapView; +diff --git a/src/main/java/net/minecraft/world/level/storage/loot/functions/LootingEnchantFunction.java b/src/main/java/net/minecraft/world/level/storage/loot/functions/LootingEnchantFunction.java +index cfe953bc924f46b570e37395ac0f05ebcb82eb39..5500e7ada2dd783cc1317968a3e54696bdd73bf8 100644 +--- a/src/main/java/net/minecraft/world/level/storage/loot/functions/LootingEnchantFunction.java ++++ b/src/main/java/net/minecraft/world/level/storage/loot/functions/LootingEnchantFunction.java +@@ -57,6 +57,13 @@ public class LootingEnchantFunction extends LootItemConditionalFunction { + + if (entity instanceof LivingEntity) { + int i = EnchantmentHelper.getMobLooting((LivingEntity) entity); ++ // Purpur start ++ if (org.purpurmc.purpur.PurpurConfig.fixProjectileLootingTransfer && ++ context.getParamOrNull(LootContextParams.DIRECT_KILLER_ENTITY) ++ instanceof net.minecraft.world.entity.projectile.AbstractArrow arrow) { ++ i = arrow.lootingLevel; ++ } ++ // Purpur end + // CraftBukkit start - use lootingModifier if set by plugin + if (context.hasParam(LootContextParams.LOOTING_MOD)) { + i = context.getParamOrNull(LootContextParams.LOOTING_MOD); +diff --git a/src/main/java/net/minecraft/world/phys/AABB.java b/src/main/java/net/minecraft/world/phys/AABB.java +index 92394960fc76886f393cba02ac33c57739a4b383..494808b7bc2fb296b78e229ce138a937b7ac2c59 100644 +--- a/src/main/java/net/minecraft/world/phys/AABB.java ++++ b/src/main/java/net/minecraft/world/phys/AABB.java +@@ -502,4 +502,10 @@ public class AABB { + public static AABB ofSize(Vec3 center, double dx, double dy, double dz) { + return new AABB(center.x - dx / 2.0, center.y - dy / 2.0, center.z - dz / 2.0, center.x + dx / 2.0, center.y + dy / 2.0, center.z + dz / 2.0); + } ++ ++ // Purpur - tuinity added method ++ public final AABB offsetY(double dy) { ++ return new AABB(this.minX, this.minY + dy, this.minZ, this.maxX, this.maxY + dy, this.maxZ); ++ } ++ // Purpur + } +diff --git a/src/main/java/net/minecraft/world/ticks/LevelTicks.java b/src/main/java/net/minecraft/world/ticks/LevelTicks.java +index 7a69564572357a7acc043e35b9c113beeb738951..a6d62abd3102770652f914b9d697c6d3c2533cfc 100644 +--- a/src/main/java/net/minecraft/world/ticks/LevelTicks.java ++++ b/src/main/java/net/minecraft/world/ticks/LevelTicks.java +@@ -81,20 +81,20 @@ public class LevelTicks implements LevelTickAccess { + } + + public void tick(long time, int maxTicks, BiConsumer ticker) { +- ProfilerFiller profilerFiller = this.profiler.get(); +- profilerFiller.push("collect"); +- this.collectTicks(time, maxTicks, profilerFiller); +- profilerFiller.popPush("run"); +- profilerFiller.incrementCounter("ticksToRun", this.toRunThisTick.size()); ++ //ProfilerFiller profilerFiller = this.profiler.get(); // Purpur ++ //profilerFiller.push("collect"); // Purpur ++ this.collectTicks(time, maxTicks, null); // Purpur ++ //profilerFiller.popPush("run"); // Purpur ++ //profilerFiller.incrementCounter("ticksToRun", this.toRunThisTick.size()); // Purpur + this.runCollectedTicks(ticker); +- profilerFiller.popPush("cleanup"); ++ //profilerFiller.popPush("cleanup"); // Purpur + this.cleanupAfterTick(); +- profilerFiller.pop(); ++ //profilerFiller.pop(); // Purpur + } + + private void collectTicks(long time, int maxTicks, ProfilerFiller profiler) { + this.sortContainersToTick(time); +- profiler.incrementCounter("containersToTick", this.containersToTick.size()); ++ //profiler.incrementCounter("containersToTick", this.containersToTick.size()); // Purpur + this.drainContainers(time, maxTicks); + this.rescheduleLeftoverContainers(); + } +diff --git a/src/main/java/org/bukkit/craftbukkit/CraftOfflinePlayer.java b/src/main/java/org/bukkit/craftbukkit/CraftOfflinePlayer.java +index 9d93130f23addb18b97d7f5ec013faef17a74529..29d2fb87a65778926aea2cfc7a5b486cad596515 100644 +--- a/src/main/java/org/bukkit/craftbukkit/CraftOfflinePlayer.java ++++ b/src/main/java/org/bukkit/craftbukkit/CraftOfflinePlayer.java +@@ -335,14 +335,26 @@ public class CraftOfflinePlayer implements OfflinePlayer, ConfigurationSerializa + + @Override + public Location getLocation() { ++ // Purpur start ++ if (this.isOnline()) { ++ return this.getPlayer().getLocation(); ++ } ++ // Purpur end ++ + CompoundTag data = this.getData(); + if (data == null) { + return null; + } + +- if (data.contains("Pos") && data.contains("Rotation")) { +- ListTag position = (ListTag) data.get("Pos"); +- ListTag rotation = (ListTag) data.get("Rotation"); ++ // Purpur start - OfflinePlayer API ++ //if (data.contains("Pos") && data.contains("Rotation")) { ++ ListTag position = data.getList("Pos", net.minecraft.nbt.Tag.TAG_DOUBLE); ++ ListTag rotation = data.getList("Rotation", net.minecraft.nbt.Tag.TAG_FLOAT); ++ ++ if (position.isEmpty() && rotation.isEmpty()) { ++ return null; ++ } ++ // Purpur end - OfflinePlayer API + + UUID uuid = new UUID(data.getLong("WorldUUIDMost"), data.getLong("WorldUUIDLeast")); + +@@ -353,9 +365,9 @@ public class CraftOfflinePlayer implements OfflinePlayer, ConfigurationSerializa + rotation.getFloat(0), + rotation.getFloat(1) + ); +- } ++ //} // Purpur - OfflinePlayer API + +- return null; ++ //return null; // Purpur - OfflinePlayer API + } + + @Override +@@ -598,4 +610,191 @@ public class CraftOfflinePlayer implements OfflinePlayer, ConfigurationSerializa + manager.save(); + } + } ++ ++ // Purpur start - OfflinePlayer API ++ @Override ++ public boolean getAllowFlight() { ++ if (this.isOnline()) { ++ return this.getPlayer().getAllowFlight(); ++ } else { ++ CompoundTag data = this.getData(); ++ if (data == null) return false; ++ if (!data.contains("abilities")) return false; ++ CompoundTag abilities = data.getCompound("abilities"); ++ return abilities.getByte("mayfly") == (byte) 1; ++ } ++ } ++ ++ @Override ++ public void setAllowFlight(boolean flight) { ++ if (this.isOnline()) { ++ this.getPlayer().setAllowFlight(flight); ++ } else { ++ CompoundTag data = this.getData(); ++ if (data == null) return; ++ if (!data.contains("abilities")) return; ++ CompoundTag abilities = data.getCompound("abilities"); ++ abilities.putByte("mayfly", (byte) (flight ? 1 : 0)); ++ data.put("abilities", abilities); ++ save(data); ++ } ++ } ++ ++ @Override ++ public boolean isFlying() { ++ if (this.isOnline()) { ++ return this.isFlying(); ++ } else { ++ CompoundTag data = this.getData(); ++ if (data == null) return false; ++ if (!data.contains("abilities")) return false; ++ CompoundTag abilities = data.getCompound("abilities"); ++ return abilities.getByte("flying") == (byte) 1; ++ } ++ } ++ ++ @Override ++ public void setFlying(boolean value) { ++ if (this.isOnline()) { ++ this.getPlayer().setFlying(value); ++ } else { ++ CompoundTag data = this.getData(); ++ if (data == null) return; ++ if (!data.contains("abilities")) return; ++ CompoundTag abilities = data.getCompound("abilities"); ++ abilities.putByte("mayfly", (byte) (value ? 1 : 0)); ++ data.put("abilities", abilities); ++ save(data); ++ } ++ } ++ ++ @Override ++ public void setFlySpeed(float value) throws IllegalArgumentException { ++ if (value < -1f || value > 1f) throw new IllegalArgumentException("FlySpeed needs to be between -1 and 1"); ++ if (this.isOnline()) { ++ this.getPlayer().setFlySpeed(value); ++ } else { ++ CompoundTag data = this.getData(); ++ if (data == null) return; ++ if (!data.contains("abilities")) return; ++ CompoundTag abilities = data.getCompound("abilities"); ++ abilities.putFloat("flySpeed", value); ++ data.put("abilities", abilities); ++ save(data); ++ } ++ } ++ ++ @Override ++ public float getFlySpeed() { ++ if (this.isOnline()) { ++ return this.getPlayer().getFlySpeed(); ++ } else { ++ CompoundTag data = this.getData(); ++ if (data == null) return 0; ++ if (!data.contains("abilities")) return 0; ++ CompoundTag abilities = data.getCompound("abilities"); ++ return abilities.getFloat("flySpeed"); ++ } ++ } ++ ++ @Override ++ public void setWalkSpeed(float value) throws IllegalArgumentException { ++ if (value < -1f || value > 1f) throw new IllegalArgumentException("WalkSpeed needs to be between -1 and 1"); ++ if (this.isOnline()) { ++ this.getPlayer().setWalkSpeed(value); ++ } else { ++ CompoundTag data = this.getData(); ++ if (data == null) return; ++ if (!data.contains("abilities")) return; ++ CompoundTag abilities = data.getCompound("abilities"); ++ abilities.putFloat("walkSpeed", value); ++ data.put("abilities", abilities); ++ save(data); ++ } ++ } ++ ++ @Override ++ public float getWalkSpeed() { ++ if (this.isOnline()) { ++ return this.getPlayer().getWalkSpeed(); ++ } else { ++ CompoundTag data = this.getData(); ++ if (data == null) return 0; ++ if (!data.contains("abilities")) return 0; ++ CompoundTag abilities = data.getCompound("abilities"); ++ return abilities.getFloat("walkSpeed"); ++ } ++ } ++ ++ @Override ++ public boolean teleportOffline(Location destination) { ++ if (this.isOnline()) { ++ return this.getPlayer().teleport(destination); ++ } else { ++ return setLocation(destination); ++ } ++ } ++ ++ @Override ++ public boolean teleportOffline(Location destination, org.bukkit.event.player.PlayerTeleportEvent.TeleportCause cause){ ++ if (this.isOnline()) { ++ return this.getPlayer().teleport(destination, cause); ++ } else { ++ return setLocation(destination); ++ } ++ } ++ ++ @Override ++ public java.util.concurrent.CompletableFuture teleportOfflineAsync(Location destination) { ++ if (this.isOnline()) { ++ return this.getPlayer().teleportAsync(destination); ++ } else { ++ return java.util.concurrent.CompletableFuture.completedFuture(setLocation(destination)); ++ } ++ } ++ ++ @Override ++ public java.util.concurrent.CompletableFuture teleportOfflineAsync(Location destination, org.bukkit.event.player.PlayerTeleportEvent.TeleportCause cause) { ++ if (this.isOnline()) { ++ return this.getPlayer().teleportAsync(destination, cause); ++ } else { ++ return java.util.concurrent.CompletableFuture.completedFuture(setLocation(destination)); ++ } ++ } ++ ++ private boolean setLocation(Location location) { ++ CompoundTag data = this.getData(); ++ if (data == null) return false; ++ data.putLong("WorldUUIDMost", location.getWorld().getUID().getMostSignificantBits()); ++ data.putLong("WorldUUIDLeast", location.getWorld().getUID().getLeastSignificantBits()); ++ net.minecraft.nbt.ListTag position = new net.minecraft.nbt.ListTag(); ++ position.add(net.minecraft.nbt.DoubleTag.valueOf(location.getX())); ++ position.add(net.minecraft.nbt.DoubleTag.valueOf(location.getY())); ++ position.add(net.minecraft.nbt.DoubleTag.valueOf(location.getZ())); ++ data.put("Pos", position); ++ net.minecraft.nbt.ListTag rotation = new net.minecraft.nbt.ListTag(); ++ rotation.add(net.minecraft.nbt.FloatTag.valueOf(location.getYaw())); ++ rotation.add(net.minecraft.nbt.FloatTag.valueOf(location.getPitch())); ++ data.put("Rotation", rotation); ++ save(data); ++ return true; ++ } ++ ++ /** ++ * Safely replaces player's .dat file with provided CompoundTag ++ * @param compoundTag ++ */ ++ private void save(CompoundTag compoundTag) { ++ File playerDir = server.console.playerDataStorage.getPlayerDir(); ++ try { ++ File tempFile = File.createTempFile(this.getUniqueId()+"-", ".dat", playerDir); ++ net.minecraft.nbt.NbtIo.writeCompressed(compoundTag, tempFile.toPath()); ++ File playerDataFile = new File(playerDir, this.getUniqueId()+".dat"); ++ File playerDataFileOld = new File(playerDir, this.getUniqueId()+".dat_old"); ++ net.minecraft.Util.safeReplaceFile(playerDataFile.toPath(), tempFile.toPath(), playerDataFileOld.toPath()); ++ } catch (java.io.IOException e) { ++ e.printStackTrace(); ++ } ++ } ++ // Purpur end - OfflinePlayer API + } +diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java +index 05e304f9fc8d0291fa779da589bd060ef4165b49..8d754bf77cb88d8ddf964f3221183e4097f06d13 100644 +--- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java ++++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java +@@ -268,7 +268,7 @@ import javax.annotation.Nullable; // Paper + import javax.annotation.Nonnull; // Paper + + public final class CraftServer implements Server { +- private final String serverName = "Paper"; // Paper ++ private final String serverName = "Purpur"; // Paper // Pufferfish // Purpur + private final String serverVersion; + private final String bukkitVersion = Versioning.getBukkitVersion(); + private final Logger logger = Logger.getLogger("Minecraft"); +@@ -403,6 +403,20 @@ public final class CraftServer implements Server { + this.serverTickManager = new CraftServerTickManager(console.tickRateManager()); + + Bukkit.setServer(this); ++ // Purpur start ++ org.purpurmc.purpur.language.Language.setLanguage(new org.purpurmc.purpur.language.Language() { ++ private net.minecraft.locale.Language language = net.minecraft.locale.Language.getInstance(); ++ @Override ++ public boolean has(@org.jetbrains.annotations.NotNull String key) { ++ return language.has(key); ++ } ++ ++ @Override ++ public @org.jetbrains.annotations.NotNull String getOrDefault(@org.jetbrains.annotations.NotNull String key) { ++ return language.getOrDefault(key); ++ } ++ }); ++ // Purpur end + + CraftRegistry.setMinecraftRegistry(console.registryAccess()); + +@@ -1054,6 +1068,7 @@ public final class CraftServer implements Server { + + org.spigotmc.SpigotConfig.init((File) this.console.options.valueOf("spigot-settings")); // Spigot + this.console.paperConfigurations.reloadConfigs(this.console); ++ org.purpurmc.purpur.PurpurConfig.init((File) console.options.valueOf("purpur-settings")); // Purpur + for (ServerLevel world : this.console.getAllLevels()) { + // world.serverLevelData.setDifficulty(config.difficulty); // Paper - per level difficulty + world.setSpawnSettings(world.serverLevelData.getDifficulty() != Difficulty.PEACEFUL && config.spawnMonsters, config.spawnAnimals); // Paper - per level difficulty (from MinecraftServer#setDifficulty(ServerLevel, Difficulty, boolean)) +@@ -1069,6 +1084,7 @@ public final class CraftServer implements Server { + } + } + world.spigotConfig.init(); // Spigot ++ world.purpurConfig.init(); // Purpur + } + + Plugin[] pluginClone = pluginManager.getPlugins().clone(); // Paper +@@ -1084,6 +1100,7 @@ public final class CraftServer implements Server { + this.reloadData(); + org.spigotmc.SpigotConfig.registerCommands(); // Spigot + io.papermc.paper.command.PaperCommands.registerCommands(this.console); // Paper ++ org.purpurmc.purpur.PurpurConfig.registerCommands(); // Purpur + this.overrideAllCommandBlockCommands = this.commandsConfiguration.getStringList("command-block-overrides").contains("*"); + this.ignoreVanillaPermissions = this.commandsConfiguration.getBoolean("ignore-vanilla-permissions"); + +@@ -1586,6 +1603,55 @@ public final class CraftServer implements Server { + return true; + } + ++ // Purpur Start ++ @Override ++ public void addFuel(org.bukkit.Material material, int burnTime) { ++ Preconditions.checkArgument(burnTime > 0, "BurnTime must be greater than 0"); ++ net.minecraft.world.level.block.entity.AbstractFurnaceBlockEntity.addFuel(net.minecraft.world.item.ItemStack.fromBukkitCopy(new ItemStack(material)), burnTime); ++ } ++ ++ @Override ++ public void removeFuel(org.bukkit.Material material) { ++ net.minecraft.world.level.block.entity.AbstractFurnaceBlockEntity.removeFuel(net.minecraft.world.item.ItemStack.fromBukkitCopy(new ItemStack(material))); ++ } ++ ++ @Override ++ public void sendBlockHighlight(Location location, int duration) { ++ sendBlockHighlight(location, duration, "", 0x6400FF00); ++ } ++ ++ @Override ++ public void sendBlockHighlight(Location location, int duration, int argb) { ++ sendBlockHighlight(location, duration, "", argb); ++ } ++ ++ @Override ++ public void sendBlockHighlight(Location location, int duration, String text) { ++ sendBlockHighlight(location, duration, text, 0x6400FF00); ++ } ++ ++ @Override ++ public void sendBlockHighlight(Location location, int duration, String text, int argb) { ++ this.worlds.forEach((name, world) -> world.sendBlockHighlight(location, duration, text, argb)); ++ } ++ ++ @Override ++ public void sendBlockHighlight(Location location, int duration, org.bukkit.Color color, int transparency) { ++ sendBlockHighlight(location, duration, "", color, transparency); ++ } ++ ++ @Override ++ public void sendBlockHighlight(Location location, int duration, String text, org.bukkit.Color color, int transparency) { ++ if (transparency < 0 || transparency > 255) throw new IllegalArgumentException("transparency is outside of 0-255 range"); ++ sendBlockHighlight(location, duration, text, transparency << 24 | color.asRGB()); ++ } ++ ++ @Override ++ public void clearBlockHighlights() { ++ this.worlds.forEach((name, world) -> clearBlockHighlights()); ++ } ++ // Purpur End ++ + @Override + public List getRecipesFor(ItemStack result) { + Preconditions.checkArgument(result != null, "ItemStack cannot be null"); +@@ -3048,6 +3114,18 @@ public final class CraftServer implements Server { + return CraftServer.this.console.paperConfigurations.createLegacyObject(CraftServer.this.console); + } + ++ // Purpur start ++ @Override ++ public YamlConfiguration getPurpurConfig() { ++ return org.purpurmc.purpur.PurpurConfig.config; ++ } ++ ++ @Override ++ public java.util.Properties getServerProperties() { ++ return getProperties().properties; ++ } ++ // Purpur end ++ + @Override + public void restart() { + org.spigotmc.RestartCommand.restart(); +@@ -3077,6 +3155,7 @@ public final class CraftServer implements Server { + @Override + public double[] getTPS() { + return new double[] { ++ net.minecraft.server.MinecraftServer.getServer().tps5s.getAverage(), // Purpur + net.minecraft.server.MinecraftServer.getServer().tps1.getAverage(), + net.minecraft.server.MinecraftServer.getServer().tps5.getAverage(), + net.minecraft.server.MinecraftServer.getServer().tps15.getAverage() +@@ -3278,4 +3357,16 @@ public final class CraftServer implements Server { + return this.potionBrewer; + } + // Paper end ++ ++ // Purpur start ++ @Override ++ public String getServerName() { ++ return this.getProperties().serverName; ++ } ++ ++ @Override ++ public boolean isLagging() { ++ return getServer().lagging; ++ } ++ // Purpur end + } +diff --git a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java +index f2b20ed5063a293f0b464548f590d652170cd1d8..226ff7c6048b510be2e71ecc5d5ff3581092aa5e 100644 +--- a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java ++++ b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java +@@ -2424,6 +2424,48 @@ public class CraftWorld extends CraftRegionAccessor implements World { + return (this.getHandle().getDragonFight() == null) ? null : new CraftDragonBattle(this.getHandle().getDragonFight()); + } + ++ // Purpur start ++ public float getLocalDifficultyAt(Location location) { ++ return getHandle().getCurrentDifficultyAt(io.papermc.paper.util.MCUtil.toBlockPosition(location)).getEffectiveDifficulty(); ++ } ++ ++ @Override ++ public void sendBlockHighlight(Location location, int duration) { ++ sendBlockHighlight(location, duration, "", 0x6400FF00); ++ } ++ ++ @Override ++ public void sendBlockHighlight(Location location, int duration, int argb) { ++ sendBlockHighlight(location, duration, "", argb); ++ } ++ ++ @Override ++ public void sendBlockHighlight(Location location, int duration, String text) { ++ sendBlockHighlight(location, duration, text, 0x6400FF00); ++ } ++ ++ @Override ++ public void sendBlockHighlight(Location location, int duration, String text, int argb) { ++ net.minecraft.network.protocol.game.DebugPackets.sendGameTestAddMarker(getHandle(), io.papermc.paper.util.MCUtil.toBlockPosition(location), text, argb, duration); ++ } ++ ++ @Override ++ public void sendBlockHighlight(Location location, int duration, org.bukkit.Color color, int transparency) { ++ sendBlockHighlight(location, duration, "", color, transparency); ++ } ++ ++ @Override ++ public void sendBlockHighlight(Location location, int duration, String text, org.bukkit.Color color, int transparency) { ++ if (transparency < 0 || transparency > 255) throw new IllegalArgumentException("transparency is outside of 0-255 range"); ++ sendBlockHighlight(location, duration, text, transparency << 24 | color.asRGB()); ++ } ++ ++ @Override ++ public void clearBlockHighlights() { ++ net.minecraft.network.protocol.game.DebugPackets.sendGameTestClearPacket(getHandle()); ++ } ++ // Purpur end ++ + @Override + public Collection getStructures(int x, int z) { + return this.getStructures(x, z, struct -> true); +diff --git a/src/main/java/org/bukkit/craftbukkit/Main.java b/src/main/java/org/bukkit/craftbukkit/Main.java +index c097f5d5fbd51cbbc01bbd54101905c59b3f3a4c..222865a3cee62f244a566092a7d814efe478ee01 100644 +--- a/src/main/java/org/bukkit/craftbukkit/Main.java ++++ b/src/main/java/org/bukkit/craftbukkit/Main.java +@@ -175,6 +175,14 @@ public class Main { + .describedAs("Jar file"); + // Paper end + ++ // Purpur Start ++ acceptsAll(asList("purpur", "purpur-settings"), "File for purpur settings") ++ .withRequiredArg() ++ .ofType(File.class) ++ .defaultsTo(new File("purpur.yml")) ++ .describedAs("Yml file"); ++ // Purpur end ++ + // Paper start + acceptsAll(asList("server-name"), "Name of the server") + .withRequiredArg() +@@ -294,7 +302,7 @@ public class Main { + System.setProperty(net.minecrell.terminalconsole.TerminalConsoleAppender.JLINE_OVERRIDE_PROPERTY, "false"); // Paper + } + +- if (Main.class.getPackage().getImplementationVendor() != null && System.getProperty("IReallyKnowWhatIAmDoingISwear") == null) { ++ if (false && Main.class.getPackage().getImplementationVendor() != null && System.getProperty("IReallyKnowWhatIAmDoingISwear") == null) { // Purpur + Date buildDate = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z").parse(Main.class.getPackage().getImplementationVendor()); // Paper + + Calendar deadline = Calendar.getInstance(); +diff --git a/src/main/java/org/bukkit/craftbukkit/block/CraftBeehive.java b/src/main/java/org/bukkit/craftbukkit/block/CraftBeehive.java +index 1a2a05160ba51d9c75f1ae6ae61d944d81428722..3beb26ad2ef0fded49a8da8c5dec64f9508c1995 100644 +--- a/src/main/java/org/bukkit/craftbukkit/block/CraftBeehive.java ++++ b/src/main/java/org/bukkit/craftbukkit/block/CraftBeehive.java +@@ -16,8 +16,15 @@ import org.bukkit.entity.Bee; + + public class CraftBeehive extends CraftBlockEntityState implements Beehive { + ++ private final List> storage = new ArrayList<>(); // Purpur ++ + public CraftBeehive(World world, BeehiveBlockEntity tileEntity) { + super(world, tileEntity); ++ // Purpur start - load bees to be able to modify them individually ++ for(BeehiveBlockEntity.BeeData data : tileEntity.getStored()) { ++ storage.add(new org.purpurmc.purpur.entity.PurpurStoredBee(data, this)); ++ } ++ // Purpur end + } + + protected CraftBeehive(CraftBeehive state, Location location) { +@@ -75,15 +82,54 @@ public class CraftBeehive extends CraftBlockEntityState impl + bees.add((Bee) bee.getBukkitEntity()); + } + } +- ++ storage.clear(); // Purpur + return bees; + } + ++ // Purpur start ++ @Override ++ public Bee releaseEntity(org.purpurmc.purpur.entity.StoredEntity entity) { ++ ensureNoWorldGeneration(); ++ ++ if(!getEntities().contains(entity)) { ++ return null; ++ } ++ ++ if(isPlaced()) { ++ BeehiveBlockEntity beehive = ((BeehiveBlockEntity) this.getTileEntityFromWorld()); ++ BeehiveBlockEntity.BeeData data = ((org.purpurmc.purpur.entity.PurpurStoredBee) entity).getHandle(); ++ ++ List list = beehive.releaseBee(getHandle(), data, BeeReleaseStatus.BEE_RELEASED, true); ++ ++ if (list.size() == 1) { ++ storage.remove(entity); ++ ++ return (Bee) list.get(0).getBukkitEntity(); ++ } ++ } ++ ++ return null; ++ } ++ ++ @Override ++ public List> getEntities() { ++ return new ArrayList<>(storage); ++ } ++ // Purpur end ++ + @Override + public void addEntity(Bee entity) { + Preconditions.checkArgument(entity != null, "Entity must not be null"); + +- this.getSnapshot().addOccupant(((CraftBee) entity).getHandle()); ++ int length = this.getSnapshot().getStored().size(); // Purpur ++ getSnapshot().addOccupant(((CraftBee) entity).getHandle()); ++ ++ // Purpur start - check if new bee was added, and if yes, add to stored bees ++ List storedBeeData = this.getSnapshot().getStored(); ++ if(length < storedBeeData.size()) { ++ storage.add(new org.purpurmc.purpur.entity.PurpurStoredBee(storedBeeData.getLast(), this)); ++ } ++ // Purpur end + } + + @Override +@@ -100,6 +146,7 @@ public class CraftBeehive extends CraftBlockEntityState impl + @Override + public void clearEntities() { + getSnapshot().clearBees(); ++ storage.clear(); // Purpur + } + // Paper end + } +diff --git a/src/main/java/org/bukkit/craftbukkit/block/CraftConduit.java b/src/main/java/org/bukkit/craftbukkit/block/CraftConduit.java +index c1759aeb3e6ad0e4eb66cba3da1b120dd1dce812..1a91bc2e422db0eba65694ac046f1b362c6b0cd6 100644 +--- a/src/main/java/org/bukkit/craftbukkit/block/CraftConduit.java ++++ b/src/main/java/org/bukkit/craftbukkit/block/CraftConduit.java +@@ -73,7 +73,7 @@ public class CraftConduit extends CraftBlockEntityState impl + public int getRange() { + this.ensureNoWorldGeneration(); + ConduitBlockEntity conduit = (ConduitBlockEntity) this.getTileEntityFromWorld(); +- return (conduit != null) ? ConduitBlockEntity.getRange(conduit.effectBlocks) : 0; ++ return (conduit != null) ? ConduitBlockEntity.getRange(conduit.effectBlocks, this.world.getHandle()) : 0; // Purpur + } + + @Override +diff --git a/src/main/java/org/bukkit/craftbukkit/command/CraftConsoleCommandSender.java b/src/main/java/org/bukkit/craftbukkit/command/CraftConsoleCommandSender.java +index 4e56018b64d11f76c8da43fd8f85c6de72204e36..9607675e6c5bff2183c4420d11fc63eeb5747fb6 100644 +--- a/src/main/java/org/bukkit/craftbukkit/command/CraftConsoleCommandSender.java ++++ b/src/main/java/org/bukkit/craftbukkit/command/CraftConsoleCommandSender.java +@@ -21,7 +21,12 @@ public class CraftConsoleCommandSender extends ServerCommandSender implements Co + + @Override + public void sendMessage(String message) { +- this.sendRawMessage(message); ++ // Purpur start ++ String[] parts = message.split("\n"); ++ for (String part : parts) { ++ this.sendRawMessage(part); ++ } ++ // Purpur end + } + + @Override +@@ -91,7 +96,7 @@ public class CraftConsoleCommandSender extends ServerCommandSender implements Co + // Paper start + @Override + public void sendMessage(final net.kyori.adventure.identity.Identity identity, final net.kyori.adventure.text.Component message, final net.kyori.adventure.audience.MessageType type) { +- this.sendRawMessage(net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection().serialize(message)); ++ this.sendMessage(net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection().serialize(message)); // Purpur + } + + @Override +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftEndermite.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftEndermite.java +index d657fd2c507a5b215aeab0a5f3e9c2ee892a27c8..985e9ec21c60a1f47973bd5fc53b96a6f9b7d04a 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftEndermite.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftEndermite.java +@@ -21,12 +21,12 @@ public class CraftEndermite extends CraftMonster implements Endermite { + + @Override + public boolean isPlayerSpawned() { +- return false; ++ return getHandle().isPlayerSpawned(); // Purpur + } + + @Override + public void setPlayerSpawned(boolean playerSpawned) { +- // Nop ++ getHandle().setPlayerSpawned(playerSpawned); // Purpur + } + // Paper start + @Override +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java +index a2d336ceb52b63db5c03432ee7bc94dc6a742b82..0ed18542fd8e2a992dc56a5f421eaa840e0af193 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java +@@ -84,6 +84,21 @@ public abstract class CraftEntity implements org.bukkit.entity.Entity { + this.entityType = CraftEntityType.minecraftToBukkit(entity.getType()); + } + ++ @Override ++ public boolean isImmuneToFire() { ++ return getHandle().fireImmune(); ++ } ++ ++ @Override ++ public void setImmuneToFire(Boolean fireImmune) { ++ getHandle().immuneToFire = fireImmune; ++ } ++ ++ @Override ++ public boolean isInDaylight() { ++ return getHandle().isSunBurnTick(); ++ } ++ + public static CraftEntity getEntity(CraftServer server, T entity) { + Preconditions.checkArgument(entity != null, "Unknown entity"); + +@@ -253,6 +268,10 @@ public abstract class CraftEntity implements org.bukkit.entity.Entity { + // Paper end + + if ((!ignorePassengers && this.entity.isVehicle()) || this.entity.isRemoved()) { // Paper - Teleport passenger API ++ // Purpur start ++ if (!entity.isRemoved() && new org.purpurmc.purpur.event.entity.EntityTeleportHinderedEvent(entity.getBukkitEntity(), org.purpurmc.purpur.event.entity.EntityTeleportHinderedEvent.Reason.IS_VEHICLE, cause).callEvent()) ++ return teleport(location, cause); ++ // Purpur end + return false; + } + +@@ -1280,4 +1299,27 @@ public abstract class CraftEntity implements org.bukkit.entity.Entity { + return this.getHandle().getScoreboardName(); + } + // Paper end - entity scoreboard name ++ ++ // Purpur start ++ @Override ++ public org.bukkit.entity.Player getRider() { ++ net.minecraft.world.entity.player.Player rider = getHandle().getRider(); ++ return rider != null ? (org.bukkit.entity.Player) rider.getBukkitEntity() : null; ++ } ++ ++ @Override ++ public boolean hasRider() { ++ return getHandle().getRider() != null; ++ } ++ ++ @Override ++ public boolean isRidable() { ++ return getHandle().isRidable(); ++ } ++ ++ @Override ++ public boolean isRidableInWater() { ++ return !getHandle().dismountsUnderwater(); ++ } ++ // Purpur end + } +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftHumanEntity.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftHumanEntity.java +index 41f3cdec7deabf34358b8087df77169f85a5b919..90265b6f2acd43713b61e277799dd31311b6b7e2 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftHumanEntity.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftHumanEntity.java +@@ -265,6 +265,7 @@ public class CraftHumanEntity extends CraftLivingEntity implements HumanEntity { + @Override + public void recalculatePermissions() { + this.perm.recalculatePermissions(); ++ getHandle().canPortalInstant = hasPermission("purpur.portal.instant"); // Purpur + } + + @Override +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftIronGolem.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftIronGolem.java +index 63cae1a2e95d8da17c45c4404a8dd0ca6a413c39..966587c2788b5c93be83259ddc962a89cde7cbaa 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftIronGolem.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftIronGolem.java +@@ -27,4 +27,17 @@ public class CraftIronGolem extends CraftGolem implements IronGolem { + public void setPlayerCreated(boolean playerCreated) { + this.getHandle().setPlayerCreated(playerCreated); + } ++ ++ // Purpur start ++ @Override ++ @org.jetbrains.annotations.Nullable ++ public java.util.UUID getSummoner() { ++ return getHandle().getSummoner(); ++ } ++ ++ @Override ++ public void setSummoner(@org.jetbrains.annotations.Nullable java.util.UUID summoner) { ++ getHandle().setSummoner(summoner); ++ } ++ // Purpur end + } +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftItem.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftItem.java +index 30d62ee4d5cd2ddacb8783b5bbbf475d592b3e02..5c1cda88080850314dac196dbe71ff12e48a8aca 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftItem.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftItem.java +@@ -151,4 +151,51 @@ public class CraftItem extends CraftEntity implements Item { + public String toString() { + return "CraftItem"; + } ++ ++ // Purpur start ++ @Override ++ public void setImmuneToCactus(boolean immuneToCactus) { ++ this.getHandle().immuneToCactus = immuneToCactus; ++ } ++ ++ @Override ++ public boolean isImmuneToCactus() { ++ return this.getHandle().immuneToCactus; ++ } ++ ++ @Override ++ public void setImmuneToExplosion(boolean immuneToExplosion) { ++ this.getHandle().immuneToExplosion = immuneToExplosion; ++ } ++ ++ @Override ++ public boolean isImmuneToExplosion() { ++ return this.getHandle().immuneToExplosion; ++ } ++ ++ @Override ++ public void setImmuneToFire(@org.jetbrains.annotations.Nullable Boolean immuneToFire) { ++ this.getHandle().immuneToFire = (immuneToFire != null && immuneToFire); ++ } ++ ++ @Override ++ public void setImmuneToFire(boolean immuneToFire) { ++ this.setImmuneToFire((Boolean) immuneToFire); ++ } ++ ++ @Override ++ public boolean isImmuneToFire() { ++ return this.getHandle().immuneToFire; ++ } ++ ++ @Override ++ public void setImmuneToLightning(boolean immuneToLightning) { ++ this.getHandle().immuneToLightning = immuneToLightning; ++ } ++ ++ @Override ++ public boolean isImmuneToLightning() { ++ return this.getHandle().immuneToLightning; ++ } ++ // Purpur end + } +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftLivingEntity.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftLivingEntity.java +index aa351df679f300018367244c7ccb3e5a59e9276f..b452ebbe11145987fb5e66b39993898457322080 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftLivingEntity.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftLivingEntity.java +@@ -505,7 +505,7 @@ public class CraftLivingEntity extends CraftEntity implements LivingEntity { + net.minecraft.server.level.ServerPlayer entityPlayer = killer == null ? null : ((CraftPlayer) killer).getHandle(); + getHandle().lastHurtByPlayer = entityPlayer; + getHandle().lastHurtByMob = entityPlayer; +- getHandle().lastHurtByPlayerTime = entityPlayer == null ? 0 : 100; // 100 value taken from EntityLiving#damageEntity ++ getHandle().lastHurtByPlayerTime = entityPlayer == null ? 0 : getHandle().level().purpurConfig.mobLastHurtByPlayerTime; // 100 value taken from EntityLiving#damageEntity // Purpur + } + // Paper end + +@@ -1173,4 +1173,22 @@ public class CraftLivingEntity extends CraftEntity implements LivingEntity { + this.getHandle().setYBodyRot(bodyYaw); + } + // Paper end - body yaw API ++ ++ // Purpur start ++ @Override ++ public void broadcastItemBreak(org.bukkit.inventory.EquipmentSlot slot) { ++ if (slot == null) return; ++ getHandle().broadcastBreakEvent(org.bukkit.craftbukkit.CraftEquipmentSlot.getNMS(slot)); ++ } ++ ++ @Override ++ public boolean shouldBurnInDay() { ++ return getHandle().shouldBurnInDay(); ++ } ++ ++ @Override ++ public void setShouldBurnInDay(boolean shouldBurnInDay) { ++ getHandle().setShouldBurnInDay(shouldBurnInDay); ++ } ++ // Purpur end + } +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftLlama.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftLlama.java +index 0ad16ee7b33582d214dab41eeee378d52c8e38ed..16bd1294c219f15ada653ef810bc2d748222d0da 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftLlama.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftLlama.java +@@ -90,4 +90,16 @@ public class CraftLlama extends CraftChestedHorse implements Llama, com.destroys + return this.getHandle().caravanTail == null ? null : (Llama) this.getHandle().caravanTail.getBukkitEntity(); + } + // Paper end ++ ++ // Purpur start ++ @Override ++ public boolean shouldJoinCaravan() { ++ return getHandle().shouldJoinCaravan; ++ } ++ ++ @Override ++ public void setShouldJoinCaravan(boolean shouldJoinCaravan) { ++ getHandle().shouldJoinCaravan = shouldJoinCaravan; ++ } ++ // Purpur end + } +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java +index eddbbd0e9be3cb81d1030c0c9da829b9193ebc16..90338017ebcb2a690dff7dad57aa6fbb95e0ff93 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java +@@ -565,10 +565,15 @@ public class CraftPlayer extends CraftHumanEntity implements Player { + + @Override + public void setPlayerListName(String name) { ++ // Purpur start ++ setPlayerListName(name, false); ++ } ++ public void setPlayerListName(String name, boolean useMM) { ++ // Purpur end + if (name == null) { + name = this.getName(); + } +- this.getHandle().listName = name.equals(this.getName()) ? null : CraftChatMessage.fromStringOrNull(name); ++ this.getHandle().listName = name.equals(this.getName()) ? null : useMM ? io.papermc.paper.adventure.PaperAdventure.asVanilla(net.kyori.adventure.text.minimessage.MiniMessage.miniMessage().deserialize(name)) : CraftChatMessage.fromStringOrNull(name); // Purpur + if (this.getHandle().connection == null) return; // Paper - Updates are possible before the player has fully joined + for (ServerPlayer player : (List) this.server.getHandle().players) { + if (player.getBukkitEntity().canSee(this)) { +@@ -1429,6 +1434,10 @@ public class CraftPlayer extends CraftHumanEntity implements Player { + } + + if (entity.isVehicle() && !ignorePassengers) { // Paper - Teleport API ++ // Purpur start ++ if (new org.purpurmc.purpur.event.entity.EntityTeleportHinderedEvent(entity.getBukkitEntity(), org.purpurmc.purpur.event.entity.EntityTeleportHinderedEvent.Reason.IS_VEHICLE, cause).callEvent()) ++ return teleport(location, cause); ++ // Purpur end + return false; + } + +@@ -2727,6 +2736,28 @@ public class CraftPlayer extends CraftHumanEntity implements Player { + return this.getHandle().getAbilities().walkingSpeed * 2f; + } + ++ // Purpur start - OfflinePlayer API ++ @Override ++ public boolean teleportOffline(@NotNull Location destination) { ++ return this.teleport(destination); ++ } ++ ++ @Override ++ public boolean teleportOffline(Location destination, PlayerTeleportEvent.TeleportCause cause) { ++ return this.teleport(destination, cause); ++ } ++ ++ @Override ++ public java.util.concurrent.CompletableFuture teleportOfflineAsync(@NotNull Location destination) { ++ return this.teleportAsync(destination); ++ } ++ ++ @Override ++ public java.util.concurrent.CompletableFuture teleportOfflineAsync(@NotNull Location destination, PlayerTeleportEvent.TeleportCause cause) { ++ return this.teleportAsync(destination, cause); ++ } ++ // Purpur end - OfflinePlayer API ++ + private void validateSpeed(float value) { + Preconditions.checkArgument(value <= 1f && value >= -1f, "Speed value (%s) need to be between -1f and 1f", value); + } +@@ -3513,4 +3544,70 @@ public class CraftPlayer extends CraftHumanEntity implements Player { + public void setSendViewDistance(final int viewDistance) { + this.getHandle().setSendViewDistance(viewDistance); + } ++ ++ // Purpur start ++ @Override ++ public boolean usesPurpurClient() { ++ return getHandle().purpurClient; ++ } ++ ++ @Override ++ public boolean isAfk() { ++ return getHandle().isAfk(); ++ } ++ ++ @Override ++ public void setAfk(boolean setAfk) { ++ getHandle().setAfk(setAfk); ++ } ++ ++ @Override ++ public void resetIdleTimer() { ++ getHandle().resetLastActionTime(); ++ } ++ ++ @Override ++ public void sendBlockHighlight(Location location, int duration) { ++ sendBlockHighlight(location, duration, "", 0x6400FF00); ++ } ++ ++ @Override ++ public void sendBlockHighlight(Location location, int duration, int argb) { ++ sendBlockHighlight(location, duration, "", argb); ++ } ++ ++ @Override ++ public void sendBlockHighlight(Location location, int duration, String text) { ++ sendBlockHighlight(location, duration, text, 0x6400FF00); ++ } ++ ++ @Override ++ public void sendBlockHighlight(Location location, int duration, String text, int argb) { ++ if (this.getHandle().connection == null) return; ++ this.getHandle().connection.send(new net.minecraft.network.protocol.common.ClientboundCustomPayloadPacket(new net.minecraft.network.protocol.common.custom.GameTestAddMarkerDebugPayload(io.papermc.paper.util.MCUtil.toBlockPosition(location), argb, text, duration))); ++ } ++ ++ @Override ++ public void sendBlockHighlight(Location location, int duration, org.bukkit.Color color, int transparency) { ++ sendBlockHighlight(location, duration, "", color, transparency); ++ } ++ ++ @Override ++ public void sendBlockHighlight(Location location, int duration, String text, org.bukkit.Color color, int transparency) { ++ if (transparency < 0 || transparency > 255) throw new IllegalArgumentException("transparency is outside of 0-255 range"); ++ sendBlockHighlight(location, duration, text, transparency << 24 | color.asRGB()); ++ } ++ ++ @Override ++ public void clearBlockHighlights() { ++ if (this.getHandle().connection == null) return; ++ this.getHandle().connection.send(new net.minecraft.network.protocol.common.ClientboundCustomPayloadPacket(new net.minecraft.network.protocol.common.custom.GameTestClearMarkersDebugPayload())); ++ } ++ ++ @Override ++ public void sendDeathScreen(net.kyori.adventure.text.Component message) { ++ if (this.getHandle().connection == null) return; ++ this.getHandle().connection.send(new net.minecraft.network.protocol.game.ClientboundPlayerCombatKillPacket(getEntityId(), io.papermc.paper.adventure.PaperAdventure.asVanilla(message))); ++ } ++ // Purpur end + } +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftSnowman.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftSnowman.java +index 4ce2373ff71c3c1b8951646e057587a3ab09e145..4f7f6cf6ca24406570d2d29dc63dc89401119961 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftSnowman.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftSnowman.java +@@ -28,4 +28,17 @@ public class CraftSnowman extends CraftGolem implements Snowman, com.destroystok + public String toString() { + return "CraftSnowman"; + } ++ ++ // Purpur start ++ @Override ++ @org.jetbrains.annotations.Nullable ++ public java.util.UUID getSummoner() { ++ return getHandle().getSummoner(); ++ } ++ ++ @Override ++ public void setSummoner(@org.jetbrains.annotations.Nullable java.util.UUID summoner) { ++ getHandle().setSummoner(summoner); ++ } ++ // Purpur end + } +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftVillager.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftVillager.java +index 6c15d40979fd3e3d246a447c432b321fbf29ada3..6ace76a829c88e2e747dbbcce0a6582c615fc56d 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftVillager.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftVillager.java +@@ -252,4 +252,11 @@ public class CraftVillager extends CraftAbstractVillager implements Villager { + getHandle().getGossips().gossips.clear(); + } + // Paper end ++ ++ // Purpur start ++ @Override ++ public boolean isLobotomized() { ++ return getHandle().isLobotomized(); ++ } ++ // Purpur end + } +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftWither.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftWither.java +index 7a8ce6956db56061af93ba9761f5d1057a90bc49..6d286b23806666f7b00ac88c5922144649f8a041 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftWither.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftWither.java +@@ -99,4 +99,17 @@ public class CraftWither extends CraftMonster implements Wither, com.destroystok + this.getHandle().makeInvulnerable(); + } + // Paper end ++ ++ // Purpur start ++ @Override ++ @org.jetbrains.annotations.Nullable ++ public java.util.UUID getSummoner() { ++ return getHandle().getSummoner(); ++ } ++ ++ @Override ++ public void setSummoner(@org.jetbrains.annotations.Nullable java.util.UUID summoner) { ++ getHandle().setSummoner(summoner); ++ } ++ // Purpur end + } +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftWolf.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftWolf.java +index 86574da257731de7646a712ed73384955fe35aa3..e223234dd64b0e41441c3b9f649f0b64dc6d98c4 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftWolf.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftWolf.java +@@ -146,4 +146,16 @@ public class CraftWolf extends CraftTameableAnimal implements Wolf { + return this.getKey().hashCode(); + } + } ++ ++ // Purpur start ++ @Override ++ public boolean isRabid() { ++ return getHandle().isRabid(); ++ } ++ ++ @Override ++ public void setRabid(boolean isRabid) { ++ getHandle().setRabid(isRabid); ++ } ++ // Purpur end + } +diff --git a/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java b/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java +index 547ab158cd0cbf51da06ea97740cfce34bca651b..6fed586c9a778f7a57e1b4ca2e6f2dbc15c8769d 100644 +--- a/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java ++++ b/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java +@@ -592,6 +592,15 @@ public class CraftEventFactory { + // Paper end + craftServer.getPluginManager().callEvent(event); + ++ // Purpur start ++ if (who != null) { ++ switch (action) { ++ case LEFT_CLICK_BLOCK, LEFT_CLICK_AIR -> who.processClick(InteractionHand.MAIN_HAND); ++ case RIGHT_CLICK_BLOCK, RIGHT_CLICK_AIR -> who.processClick(InteractionHand.OFF_HAND); ++ } ++ } ++ // Purpur end ++ + return event; + } + +@@ -1121,7 +1130,7 @@ public class CraftEventFactory { + return CraftEventFactory.callEntityDamageEvent(source.getDirectBlock(), entity, DamageCause.LAVA, bukkitDamageSource, modifiers, modifierFunctions, cancelled); + } else if (source.getDirectBlock() != null) { + DamageCause cause; +- if (source.is(DamageTypes.CACTUS) || source.is(DamageTypes.SWEET_BERRY_BUSH) || source.is(DamageTypes.STALAGMITE) || source.is(DamageTypes.FALLING_STALACTITE) || source.is(DamageTypes.FALLING_ANVIL)) { ++ if (source.is(DamageTypes.CACTUS) || source.is(DamageTypes.SWEET_BERRY_BUSH) || source.is(DamageTypes.STALAGMITE) || source.is(DamageTypes.FALLING_STALACTITE) || source.is(DamageTypes.FALLING_ANVIL) || source.isStonecutter()) { // Purpur + cause = DamageCause.CONTACT; + } else if (source.is(DamageTypes.HOT_FLOOR)) { + cause = DamageCause.HOT_FLOOR; +@@ -1179,6 +1188,7 @@ public class CraftEventFactory { + EntityDamageEvent event; + if (damager != null) { + event = new EntityDamageByEntityEvent(damager.getBukkitEntity(), damagee.getBukkitEntity(), cause, bukkitDamageSource, modifiers, modifierFunctions, critical); ++ damager.processClick(InteractionHand.MAIN_HAND); // Purpur + } else { + event = new EntityDamageEvent(damagee.getBukkitEntity(), cause, bukkitDamageSource, modifiers, modifierFunctions); + } +diff --git a/src/main/java/org/bukkit/craftbukkit/inventory/CraftContainer.java b/src/main/java/org/bukkit/craftbukkit/inventory/CraftContainer.java +index 977b77547f7ba62cef3640cf8d4f1c8e7cded53a..beae43e9b6fe447e7515d878ac175f461968768a 100644 +--- a/src/main/java/org/bukkit/craftbukkit/inventory/CraftContainer.java ++++ b/src/main/java/org/bukkit/craftbukkit/inventory/CraftContainer.java +@@ -184,8 +184,19 @@ public class CraftContainer extends AbstractContainerMenu { + case PLAYER: + case CHEST: + case ENDER_CHEST: ++ // Purpur start ++ this.delegate = new ChestMenu(org.purpurmc.purpur.PurpurConfig.enderChestSixRows ? MenuType.GENERIC_9x6 : MenuType.GENERIC_9x3, windowId, bottom, top, top.getContainerSize() / 9); ++ break; + case BARREL: +- this.delegate = new ChestMenu(MenuType.GENERIC_9x3, windowId, bottom, top, top.getContainerSize() / 9); ++ this.delegate = new ChestMenu(switch (org.purpurmc.purpur.PurpurConfig.barrelRows) { ++ case 6 -> MenuType.GENERIC_9x6; ++ case 5 -> MenuType.GENERIC_9x5; ++ case 4 -> MenuType.GENERIC_9x4; ++ case 2 -> MenuType.GENERIC_9x2; ++ case 1 -> MenuType.GENERIC_9x1; ++ default -> MenuType.GENERIC_9x3; ++ }, windowId, bottom, top, top.getContainerSize() / 9); ++ // Purpur end + break; + case DISPENSER: + case DROPPER: +diff --git a/src/main/java/org/bukkit/craftbukkit/inventory/CraftInventory.java b/src/main/java/org/bukkit/craftbukkit/inventory/CraftInventory.java +index af1ae3dacb628da23f7d2988c6e76d3fb2d64103..4ee2d501f882279b48edb4b8bf0824587c276bf6 100644 +--- a/src/main/java/org/bukkit/craftbukkit/inventory/CraftInventory.java ++++ b/src/main/java/org/bukkit/craftbukkit/inventory/CraftInventory.java +@@ -84,7 +84,7 @@ public class CraftInventory implements Inventory { + + @Override + public void setContents(ItemStack[] items) { +- Preconditions.checkArgument(items.length <= this.getSize(), "Invalid inventory size (%s); expected %s or less", items.length, this.getSize()); ++ // Preconditions.checkArgument(items.length <= this.getSize(), "Invalid inventory size (%s); expected %s or less", items.length, this.getSize()); // Purpur + + for (int i = 0; i < this.getSize(); i++) { + if (i >= items.length) { +diff --git a/src/main/java/org/bukkit/craftbukkit/inventory/CraftInventoryAnvil.java b/src/main/java/org/bukkit/craftbukkit/inventory/CraftInventoryAnvil.java +index 9ee14589d63bbfc0880f2eee5e924fe946ee0035..0a5841fa26698e60bdeadbb58b9343fe1ff08a28 100644 +--- a/src/main/java/org/bukkit/craftbukkit/inventory/CraftInventoryAnvil.java ++++ b/src/main/java/org/bukkit/craftbukkit/inventory/CraftInventoryAnvil.java +@@ -9,7 +9,7 @@ import org.bukkit.inventory.AnvilInventory; + public class CraftInventoryAnvil extends CraftResultInventory implements AnvilInventory { + + private final Location location; +- private final AnvilMenu container; ++ public final AnvilMenu container; // Purpur - private -> public + + public CraftInventoryAnvil(Location location, Container inventory, Container resultInventory, AnvilMenu container) { + super(inventory, resultInventory); +@@ -57,4 +57,26 @@ public class CraftInventoryAnvil extends CraftResultInventory implements AnvilIn + Preconditions.checkArgument(levels >= 0, "Maximum repair cost must be positive (or 0)"); + this.container.maximumRepairCost = levels; + } ++ ++ // Purpur start ++ @Override ++ public boolean canBypassCost() { ++ return container.bypassCost; ++ } ++ ++ @Override ++ public void setBypassCost(boolean bypassCost) { ++ container.bypassCost = bypassCost; ++ } ++ ++ @Override ++ public boolean canDoUnsafeEnchants() { ++ return container.canDoUnsafeEnchants; ++ } ++ ++ @Override ++ public void setDoUnsafeEnchants(boolean canDoUnsafeEnchants) { ++ container.canDoUnsafeEnchants = canDoUnsafeEnchants; ++ } ++ // Purpur end + } +diff --git a/src/main/java/org/bukkit/craftbukkit/inventory/CraftRecipe.java b/src/main/java/org/bukkit/craftbukkit/inventory/CraftRecipe.java +index 6ba29875d78ede4aa7978ff689e588f7fed11528..4afec4387971612f62b825e9e56c2ead7424a7c2 100644 +--- a/src/main/java/org/bukkit/craftbukkit/inventory/CraftRecipe.java ++++ b/src/main/java/org/bukkit/craftbukkit/inventory/CraftRecipe.java +@@ -29,6 +29,7 @@ public interface CraftRecipe extends Recipe { + } else if (bukkit instanceof RecipeChoice.ExactChoice) { + stack = new Ingredient(((RecipeChoice.ExactChoice) bukkit).getChoices().stream().map((mat) -> new net.minecraft.world.item.crafting.Ingredient.ItemValue(CraftItemStack.asNMSCopy(mat)))); + stack.exact = true; ++ stack.predicate = ((RecipeChoice.ExactChoice) bukkit).getPredicate(); // Purpur + } else { + throw new IllegalArgumentException("Unknown recipe stack instance " + bukkit); + } +diff --git a/src/main/java/org/bukkit/craftbukkit/legacy/CraftLegacy.java b/src/main/java/org/bukkit/craftbukkit/legacy/CraftLegacy.java +index 71d057dc8c7362f8e7aaca5e31c9f02b2bf3f281..9d9405af0db28c0f3ffff2881b54f1dc84675a9d 100644 +--- a/src/main/java/org/bukkit/craftbukkit/legacy/CraftLegacy.java ++++ b/src/main/java/org/bukkit/craftbukkit/legacy/CraftLegacy.java +@@ -256,6 +256,7 @@ public final class CraftLegacy { + } + + static { ++ if (!org.purpurmc.purpur.PurpurConfig.loggerSuppressInitLegacyMaterialError) // Purpur + LOGGER.warn("Initializing Legacy Material Support. Unless you have legacy plugins and/or data this is a bug!"); // Paper - Improve logging and errors; doesn't need to be an error + if (MinecraftServer.getServer() != null && MinecraftServer.getServer().isDebugging()) { + new Exception().printStackTrace(); +diff --git a/src/main/java/org/bukkit/craftbukkit/map/CraftMapRenderer.java b/src/main/java/org/bukkit/craftbukkit/map/CraftMapRenderer.java +index 0cbbd915631904fe8c6effefb92895422b33eff6..aef19cfbecb4ddfc8dc71c4f3b2a011364c12dc2 100644 +--- a/src/main/java/org/bukkit/craftbukkit/map/CraftMapRenderer.java ++++ b/src/main/java/org/bukkit/craftbukkit/map/CraftMapRenderer.java +@@ -47,4 +47,10 @@ public class CraftMapRenderer extends MapRenderer { + } + } + ++ // Purpur - start ++ @Override ++ public boolean isExplorerMap() { ++ return this.worldMap.isExplorerMap; ++ } ++ // Purpur - end + } +diff --git a/src/main/java/org/bukkit/craftbukkit/scheduler/CraftScheduler.java b/src/main/java/org/bukkit/craftbukkit/scheduler/CraftScheduler.java +index e85b9bb3f9c225d289a4959921970b9963881199..ca8ae8e1c51b937dac916e0b0dc94b5e2e61efeb 100644 +--- a/src/main/java/org/bukkit/craftbukkit/scheduler/CraftScheduler.java ++++ b/src/main/java/org/bukkit/craftbukkit/scheduler/CraftScheduler.java +@@ -503,7 +503,7 @@ public class CraftScheduler implements BukkitScheduler { + this.parsePending(); + } else { + // this.debugTail = this.debugTail.setNext(new CraftAsyncDebugger(currentTick + CraftScheduler.RECENT_TICKS, task.getOwner(), task.getTaskClass())); // Paper +- task.getOwner().getLogger().log(Level.SEVERE, "Unexpected Async Task in the Sync Scheduler. Report this to Paper"); // Paper ++ task.getOwner().getLogger().log(Level.SEVERE, "Unexpected Async Task in the Sync Scheduler. Report this to Purpur"); // Paper // Purpur + // We don't need to parse pending + // (async tasks must live with race-conditions if they attempt to cancel between these few lines of code) + } +@@ -515,10 +515,10 @@ public class CraftScheduler implements BukkitScheduler { + this.runners.remove(task.getTaskId()); + } + } +- MinecraftTimings.bukkitSchedulerFinishTimer.startTiming(); // Paper ++ //MinecraftTimings.bukkitSchedulerFinishTimer.startTiming(); // Paper // Purpur + this.pending.addAll(temp); + temp.clear(); +- MinecraftTimings.bukkitSchedulerFinishTimer.stopTiming(); // Paper ++ //MinecraftTimings.bukkitSchedulerFinishTimer.stopTiming(); // Paper // Purpur + //this.debugHead = this.debugHead.getNextHead(currentTick); // Paper + } + +@@ -561,7 +561,7 @@ public class CraftScheduler implements BukkitScheduler { + } + + void parsePending() { // Paper +- if (!this.isAsyncScheduler) MinecraftTimings.bukkitSchedulerPendingTimer.startTiming(); // Paper ++ //if (!this.isAsyncScheduler) MinecraftTimings.bukkitSchedulerPendingTimer.startTiming(); // Paper // Purpur + CraftTask head = this.head; + CraftTask task = head.getNext(); + CraftTask lastTask = head; +@@ -580,7 +580,7 @@ public class CraftScheduler implements BukkitScheduler { + task.setNext(null); + } + this.head = lastTask; +- if (!this.isAsyncScheduler) MinecraftTimings.bukkitSchedulerPendingTimer.stopTiming(); // Paper ++ //if (!this.isAsyncScheduler) MinecraftTimings.bukkitSchedulerPendingTimer.stopTiming(); // Paper // Purpur + } + + private boolean isReady(final int currentTick) { +diff --git a/src/main/java/org/bukkit/craftbukkit/scheduler/CraftTask.java b/src/main/java/org/bukkit/craftbukkit/scheduler/CraftTask.java +index ea26d9464644b5217879b8c21b4da28e57708dcb..5835dc236b3f5291a804f7fb14a12eb466d4e0ba 100644 +--- a/src/main/java/org/bukkit/craftbukkit/scheduler/CraftTask.java ++++ b/src/main/java/org/bukkit/craftbukkit/scheduler/CraftTask.java +@@ -96,13 +96,13 @@ public class CraftTask implements BukkitTask, Runnable { // Spigot + + @Override + public void run() { +- try (Timing ignored = timings.startTiming()) { // Paper ++ //try (Timing ignored = timings.startTiming()) { // Paper // Purpur + if (this.rTask != null) { + this.rTask.run(); + } else { + this.cTask.accept(this); + } +- } // Paper ++ //} // Paper // Purpur + } + + long getCreatedAt() { +diff --git a/src/main/java/org/bukkit/craftbukkit/scoreboard/CraftScoreboardManager.java b/src/main/java/org/bukkit/craftbukkit/scoreboard/CraftScoreboardManager.java +index b3e1adeb932da9b3bed16acd94e2f16da48a7c72..d3ec817e95628f1fc8be4a29c9a0f13c7d5fd552 100644 +--- a/src/main/java/org/bukkit/craftbukkit/scoreboard/CraftScoreboardManager.java ++++ b/src/main/java/org/bukkit/craftbukkit/scoreboard/CraftScoreboardManager.java +@@ -115,7 +115,7 @@ public final class CraftScoreboardManager implements ScoreboardManager { + public void forAllObjectives(ObjectiveCriteria criteria, ScoreHolder holder, Consumer consumer) { + // Paper start - add timings for scoreboard search + // plugins leaking scoreboards will make this very expensive, let server owners debug it easily +- co.aikar.timings.MinecraftTimings.scoreboardScoreSearch.startTimingIfSync(); ++ //co.aikar.timings.MinecraftTimings.scoreboardScoreSearch.startTimingIfSync(); // Purpur + try { + // Paper end - add timings for scoreboard search + for (CraftScoreboard scoreboard : this.scoreboards) { +@@ -123,7 +123,7 @@ public final class CraftScoreboardManager implements ScoreboardManager { + board.forAllObjectives(criteria, holder, (score) -> consumer.accept(score)); + } + } finally { // Paper start - add timings for scoreboard search +- co.aikar.timings.MinecraftTimings.scoreboardScoreSearch.stopTimingIfSync(); ++ //co.aikar.timings.MinecraftTimings.scoreboardScoreSearch.stopTimingIfSync(); // Purpur + } + // Paper end - add timings for scoreboard search + } +diff --git a/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java b/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java +index a1c9989df460d7ae3666fffe7968750832a30b85..ad7f21566271260270db452e2f15c32f8a829d28 100644 +--- a/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java ++++ b/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java +@@ -507,7 +507,7 @@ public final class CraftMagicNumbers implements UnsafeValues { + + @Override + public com.destroystokyo.paper.util.VersionFetcher getVersionFetcher() { +- return new com.destroystokyo.paper.PaperVersionFetcher(); ++ return new com.destroystokyo.paper.PaperVersionFetcher(); // Purpur - TODO: Pufferfish + } + + @Override +diff --git a/src/main/java/org/bukkit/craftbukkit/util/Versioning.java b/src/main/java/org/bukkit/craftbukkit/util/Versioning.java +index 774556a62eb240da42e84db4502e2ed43495be17..99597258e8e88cd9e2c901c4ac3ff7faeeabee2b 100644 +--- a/src/main/java/org/bukkit/craftbukkit/util/Versioning.java ++++ b/src/main/java/org/bukkit/craftbukkit/util/Versioning.java +@@ -11,7 +11,7 @@ public final class Versioning { + public static String getBukkitVersion() { + String result = "Unknown-Version"; + +- InputStream stream = Bukkit.class.getClassLoader().getResourceAsStream("META-INF/maven/io.papermc.paper/paper-api/pom.properties"); ++ InputStream stream = Bukkit.class.getClassLoader().getResourceAsStream("META-INF/maven/org.purpurmc.purpur/purpur-api/pom.properties"); // Pufferfish // Purpur + Properties properties = new Properties(); + + if (stream != null) { +diff --git a/src/main/java/org/bukkit/craftbukkit/util/permissions/CommandPermissions.java b/src/main/java/org/bukkit/craftbukkit/util/permissions/CommandPermissions.java +index 52649f82351ab4f675c3cc3cd6640956b0f76b91..eb51c88c7a0658190d3a8bfd5d18dca79d85fba0 100644 +--- a/src/main/java/org/bukkit/craftbukkit/util/permissions/CommandPermissions.java ++++ b/src/main/java/org/bukkit/craftbukkit/util/permissions/CommandPermissions.java +@@ -23,7 +23,15 @@ public final class CommandPermissions { + DefaultPermissions.registerPermission(CommandPermissions.PREFIX + "kick", "Allows the user to kick players", PermissionDefault.OP, commands); + DefaultPermissions.registerPermission(CommandPermissions.PREFIX + "stop", "Allows the user to stop the server", PermissionDefault.OP, commands); + DefaultPermissions.registerPermission(CommandPermissions.PREFIX + "list", "Allows the user to list all online players", PermissionDefault.OP, commands); +- DefaultPermissions.registerPermission(CommandPermissions.PREFIX + "gamemode", "Allows the user to change the gamemode of another player", PermissionDefault.OP, commands); ++ // Purpur start ++ Permission gamemodeVanilla = DefaultPermissions.registerPermission(PREFIX + "gamemode", "Allows the user to change the gamemode", PermissionDefault.OP, commands); ++ for (net.minecraft.world.level.GameType gametype : net.minecraft.world.level.GameType.values()) { ++ Permission gamemodeSelf = DefaultPermissions.registerPermission(PREFIX + "gamemode." + gametype.getName(), "Allows the user to set " + gametype.getName() + " gamemode for self", PermissionDefault.OP); ++ Permission gamemodeOther = DefaultPermissions.registerPermission(PREFIX + "gamemode." + gametype.getName() + ".other", "Allows the user to set " + gametype.getName() + " gamemode for other players", PermissionDefault.OP); ++ gamemodeSelf.addParent(gamemodeOther, true); ++ gamemodeVanilla.addParent(gamemodeSelf, true); ++ } ++ // Purpur end + DefaultPermissions.registerPermission(CommandPermissions.PREFIX + "experience", "Allows the user to give themselves or others arbitrary values of experience", PermissionDefault.OP, commands); // Paper - wrong permission; redirects are de-redirected and the root literal name is used, so xp -> experience + DefaultPermissions.registerPermission(CommandPermissions.PREFIX + "defaultgamemode", "Allows the user to change the default gamemode of the server", PermissionDefault.OP, commands); + DefaultPermissions.registerPermission(CommandPermissions.PREFIX + "seed", "Allows the user to view the seed of the world", PermissionDefault.OP, commands); +diff --git a/src/main/java/org/purpurmc/purpur/PurpurConfig.java b/src/main/java/org/purpurmc/purpur/PurpurConfig.java +new file mode 100644 +index 0000000000000000000000000000000000000000..046304d9149472eaffb3ff5f4fa22a230969de86 +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/PurpurConfig.java +@@ -0,0 +1,589 @@ ++package org.purpurmc.purpur; ++ ++import com.google.common.base.Throwables; ++import com.google.common.collect.ImmutableMap; ++import com.mojang.datafixers.util.Pair; ++import net.kyori.adventure.bossbar.BossBar; ++import net.kyori.adventure.text.minimessage.MiniMessage; ++import net.minecraft.core.registries.BuiltInRegistries; ++import net.minecraft.resources.ResourceLocation; ++import net.minecraft.server.MinecraftServer; ++import net.minecraft.world.effect.MobEffect; ++import net.minecraft.world.effect.MobEffectInstance; ++import net.minecraft.world.entity.EntityDimensions; ++import net.minecraft.world.entity.EntityType; ++import net.minecraft.world.food.FoodProperties; ++import net.minecraft.world.food.Foods; ++import net.minecraft.world.item.enchantment.Enchantment; ++import net.minecraft.world.level.block.Block; ++import net.minecraft.world.level.block.Blocks; ++import org.bukkit.Bukkit; ++import org.bukkit.command.Command; ++import org.bukkit.configuration.ConfigurationSection; ++import org.bukkit.configuration.InvalidConfigurationException; ++import org.bukkit.configuration.file.YamlConfiguration; ++import org.purpurmc.purpur.command.PurpurCommand; ++import org.purpurmc.purpur.task.TPSBarTask; ++ ++import java.io.File; ++import java.io.IOException; ++import java.lang.reflect.InvocationTargetException; ++import java.lang.reflect.Method; ++import java.lang.reflect.Modifier; ++import java.util.ArrayList; ++import java.util.Collections; ++import java.util.HashMap; ++import java.util.HashSet; ++import java.util.List; ++import java.util.Map; ++import java.util.Set; ++import java.util.logging.Level; ++ ++@SuppressWarnings("unused") ++public class PurpurConfig { ++ private static final String HEADER = "This is the main configuration file for Purpur.\n" ++ + "As you can see, there's tons to configure. Some options may impact gameplay, so use\n" ++ + "with caution, and make sure you know what each option does before configuring.\n" ++ + "\n" ++ + "If you need help with the configuration or have any questions related to Purpur,\n" ++ + "join us in our Discord guild.\n" ++ + "\n" ++ + "Website: https://purpurmc.org \n" ++ + "Docs: https://purpurmc.org/docs \n"; ++ private static File CONFIG_FILE; ++ public static YamlConfiguration config; ++ ++ private static Map commands; ++ ++ public static int version; ++ static boolean verbose; ++ ++ public static void init(File configFile) { ++ CONFIG_FILE = configFile; ++ config = new YamlConfiguration(); ++ try { ++ config.load(CONFIG_FILE); ++ } catch (IOException ignore) { ++ } catch (InvalidConfigurationException ex) { ++ Bukkit.getLogger().log(Level.SEVERE, "Could not load purpur.yml, please correct your syntax errors", ex); ++ throw Throwables.propagate(ex); ++ } ++ config.options().header(HEADER); ++ config.options().copyDefaults(true); ++ verbose = getBoolean("verbose", false); ++ ++ commands = new HashMap<>(); ++ commands.put("purpur", new PurpurCommand("purpur")); ++ ++ version = getInt("config-version", 34); ++ set("config-version", 34); ++ ++ readConfig(PurpurConfig.class, null); ++ ++ Blocks.rebuildCache(); ++ } ++ ++ protected static void log(String s) { ++ if (verbose) { ++ log(Level.INFO, s); ++ } ++ } ++ ++ protected static void log(Level level, String s) { ++ Bukkit.getLogger().log(level, s); ++ } ++ ++ public static void registerCommands() { ++ for (Map.Entry entry : commands.entrySet()) { ++ MinecraftServer.getServer().server.getCommandMap().register(entry.getKey(), "Purpur", entry.getValue()); ++ } ++ } ++ ++ static void readConfig(Class clazz, Object instance) { ++ for (Method method : clazz.getDeclaredMethods()) { ++ if (Modifier.isPrivate(method.getModifiers())) { ++ if (method.getParameterTypes().length == 0 && method.getReturnType() == Void.TYPE) { ++ try { ++ method.setAccessible(true); ++ method.invoke(instance); ++ } catch (InvocationTargetException ex) { ++ throw Throwables.propagate(ex.getCause()); ++ } catch (Exception ex) { ++ Bukkit.getLogger().log(Level.SEVERE, "Error invoking " + method, ex); ++ } ++ } ++ } ++ } ++ ++ try { ++ config.save(CONFIG_FILE); ++ } catch (IOException ex) { ++ Bukkit.getLogger().log(Level.SEVERE, "Could not save " + CONFIG_FILE, ex); ++ } ++ } ++ ++ private static void set(String path, Object val) { ++ config.addDefault(path, val); ++ config.set(path, val); ++ } ++ ++ private static String getString(String path, String def) { ++ config.addDefault(path, def); ++ return config.getString(path, config.getString(path)); ++ } ++ ++ private static boolean getBoolean(String path, boolean def) { ++ config.addDefault(path, def); ++ return config.getBoolean(path, config.getBoolean(path)); ++ } ++ ++ private static double getDouble(String path, double def) { ++ config.addDefault(path, def); ++ return config.getDouble(path, config.getDouble(path)); ++ } ++ ++ private static int getInt(String path, int def) { ++ config.addDefault(path, def); ++ return config.getInt(path, config.getInt(path)); ++ } ++ ++ private static List getList(String path, T def) { ++ config.addDefault(path, def); ++ return config.getList(path, config.getList(path)); ++ } ++ ++ static Map getMap(String path, Map def) { ++ if (def != null && config.getConfigurationSection(path) == null) { ++ config.addDefault(path, def); ++ return def; ++ } ++ return toMap(config.getConfigurationSection(path)); ++ } ++ ++ private static Map toMap(ConfigurationSection section) { ++ ImmutableMap.Builder builder = ImmutableMap.builder(); ++ if (section != null) { ++ for (String key : section.getKeys(false)) { ++ Object obj = section.get(key); ++ if (obj != null) { ++ builder.put(key, obj instanceof ConfigurationSection val ? toMap(val) : obj); ++ } ++ } ++ } ++ return builder.build(); ++ } ++ ++ public static String cannotRideMob = "You cannot mount that mob"; ++ public static String afkBroadcastAway = "%s is now AFK"; ++ public static String afkBroadcastBack = "%s is no longer AFK"; ++ public static boolean afkBroadcastUseDisplayName = false; ++ public static String afkTabListPrefix = "[AFK] "; ++ public static String afkTabListSuffix = ""; ++ public static String creditsCommandOutput = "%s has been shown the end credits"; ++ public static String demoCommandOutput = "%s has been shown the demo screen"; ++ public static String pingCommandOutput = "%s's ping is %sms"; ++ public static String ramCommandOutput = "Ram Usage: / ()"; ++ public static String rambarCommandOutput = "Rambar toggled for "; ++ public static String tpsbarCommandOutput = "Tpsbar toggled for "; ++ public static String dontRunWithScissors = "Don't run with scissors!"; ++ public static String uptimeCommandOutput = "Server uptime is "; ++ public static String unverifiedUsername = "default"; ++ public static String sleepSkippingNight = "default"; ++ public static String sleepingPlayersPercent = "default"; ++ public static String sleepNotPossible = "default"; ++ private static void messages() { ++ cannotRideMob = getString("settings.messages.cannot-ride-mob", cannotRideMob); ++ afkBroadcastAway = getString("settings.messages.afk-broadcast-away", afkBroadcastAway); ++ afkBroadcastBack = getString("settings.messages.afk-broadcast-back", afkBroadcastBack); ++ afkBroadcastUseDisplayName = getBoolean("settings.messages.afk-broadcast-use-display-name", afkBroadcastUseDisplayName); ++ afkTabListPrefix = MiniMessage.miniMessage().serialize(MiniMessage.miniMessage().deserialize(getString("settings.messages.afk-tab-list-prefix", afkTabListPrefix))); ++ afkTabListSuffix = MiniMessage.miniMessage().serialize(MiniMessage.miniMessage().deserialize(getString("settings.messages.afk-tab-list-suffix", afkTabListSuffix))); ++ creditsCommandOutput = getString("settings.messages.credits-command-output", creditsCommandOutput); ++ demoCommandOutput = getString("settings.messages.demo-command-output", demoCommandOutput); ++ pingCommandOutput = getString("settings.messages.ping-command-output", pingCommandOutput); ++ ramCommandOutput = getString("settings.messages.ram-command-output", ramCommandOutput); ++ rambarCommandOutput = getString("settings.messages.rambar-command-output", rambarCommandOutput); ++ tpsbarCommandOutput = getString("settings.messages.tpsbar-command-output", tpsbarCommandOutput); ++ dontRunWithScissors = getString("settings.messages.dont-run-with-scissors", dontRunWithScissors); ++ uptimeCommandOutput = getString("settings.messages.uptime-command-output", uptimeCommandOutput); ++ unverifiedUsername = getString("settings.messages.unverified-username", unverifiedUsername); ++ sleepSkippingNight = getString("settings.messages.sleep-skipping-night", sleepSkippingNight); ++ sleepingPlayersPercent = getString("settings.messages.sleeping-players-percent", sleepingPlayersPercent); ++ sleepNotPossible = getString("settings.messages.sleep-not-possible", sleepNotPossible); ++ } ++ ++ public static String deathMsgRunWithScissors = " slipped and fell on their shears"; ++ public static String deathMsgStonecutter = " has sawed themself in half"; ++ private static void deathMessages() { ++ deathMsgRunWithScissors = getString("settings.messages.death-message.run-with-scissors", deathMsgRunWithScissors); ++ deathMsgStonecutter = getString("settings.messages.death-message.stonecutter", deathMsgStonecutter); ++ } ++ ++ public static boolean advancementOnlyBroadcastToAffectedPlayer = false; ++ public static boolean deathMessageOnlyBroadcastToAffectedPlayer = false; ++ private static void broadcastSettings() { ++ if (version < 13) { ++ boolean oldValue = getBoolean("settings.advancement.only-broadcast-to-affected-player", false); ++ set("settings.broadcasts.advancement.only-broadcast-to-affected-player", oldValue); ++ set("settings.advancement.only-broadcast-to-affected-player", null); ++ } ++ advancementOnlyBroadcastToAffectedPlayer = getBoolean("settings.broadcasts.advancement.only-broadcast-to-affected-player", advancementOnlyBroadcastToAffectedPlayer); ++ deathMessageOnlyBroadcastToAffectedPlayer = getBoolean("settings.broadcasts.death.only-broadcast-to-affected-player", deathMessageOnlyBroadcastToAffectedPlayer); ++ } ++ ++ public static String serverModName = "Purpur"; ++ private static void serverModName() { ++ serverModName = getString("settings.server-mod-name", serverModName); ++ } ++ ++ public static double laggingThreshold = 19.0D; ++ private static void tickLoopSettings() { ++ laggingThreshold = getDouble("settings.lagging-threshold", laggingThreshold); ++ } ++ ++ public static boolean useAlternateKeepAlive = false; ++ private static void useAlternateKeepAlive() { ++ useAlternateKeepAlive = getBoolean("settings.use-alternate-keepalive", useAlternateKeepAlive); ++ } ++ ++ public static boolean disableGiveCommandDrops = false; ++ private static void disableGiveCommandDrops() { ++ disableGiveCommandDrops = getBoolean("settings.disable-give-dropping", disableGiveCommandDrops); ++ } ++ ++ public static String commandRamBarTitle = "Ram: / ()"; ++ public static BossBar.Overlay commandRamBarProgressOverlay = BossBar.Overlay.NOTCHED_20; ++ public static BossBar.Color commandRamBarProgressColorGood = BossBar.Color.GREEN; ++ public static BossBar.Color commandRamBarProgressColorMedium = BossBar.Color.YELLOW; ++ public static BossBar.Color commandRamBarProgressColorLow = BossBar.Color.RED; ++ public static String commandRamBarTextColorGood = ""; ++ public static String commandRamBarTextColorMedium = ""; ++ public static String commandRamBarTextColorLow = ""; ++ public static int commandRamBarTickInterval = 20; ++ public static String commandTPSBarTitle = "TPS: MSPT: Ping: ms"; ++ public static BossBar.Overlay commandTPSBarProgressOverlay = BossBar.Overlay.NOTCHED_20; ++ public static TPSBarTask.FillMode commandTPSBarProgressFillMode = TPSBarTask.FillMode.MSPT; ++ public static BossBar.Color commandTPSBarProgressColorGood = BossBar.Color.GREEN; ++ public static BossBar.Color commandTPSBarProgressColorMedium = BossBar.Color.YELLOW; ++ public static BossBar.Color commandTPSBarProgressColorLow = BossBar.Color.RED; ++ public static String commandTPSBarTextColorGood = ""; ++ public static String commandTPSBarTextColorMedium = ""; ++ public static String commandTPSBarTextColorLow = ""; ++ public static int commandTPSBarTickInterval = 20; ++ public static String commandCompassBarTitle = "S \u00B7 \u25C8 \u00B7 \u25C8 \u00B7 \u25C8 \u00B7 SW \u00B7 \u25C8 \u00B7 \u25C8 \u00B7 \u25C8 \u00B7 W \u00B7 \u25C8 \u00B7 \u25C8 \u00B7 \u25C8 \u00B7 NW \u00B7 \u25C8 \u00B7 \u25C8 \u00B7 \u25C8 \u00B7 N \u00B7 \u25C8 \u00B7 \u25C8 \u00B7 \u25C8 \u00B7 NE \u00B7 \u25C8 \u00B7 \u25C8 \u00B7 \u25C8 \u00B7 E \u00B7 \u25C8 \u00B7 \u25C8 \u00B7 \u25C8 \u00B7 SE \u00B7 \u25C8 \u00B7 \u25C8 \u00B7 \u25C8 \u00B7 S \u00B7 \u25C8 \u00B7 \u25C8 \u00B7 \u25C8 \u00B7 SW \u00B7 \u25C8 \u00B7 \u25C8 \u00B7 \u25C8 \u00B7 W \u00B7 \u25C8 \u00B7 \u25C8 \u00B7 \u25C8 \u00B7 NW \u00B7 \u25C8 \u00B7 \u25C8 \u00B7 \u25C8 \u00B7 N \u00B7 \u25C8 \u00B7 \u25C8 \u00B7 \u25C8 \u00B7 NE \u00B7 \u25C8 \u00B7 \u25C8 \u00B7 \u25C8 \u00B7 E \u00B7 \u25C8 \u00B7 \u25C8 \u00B7 \u25C8 \u00B7 SE \u00B7 \u25C8 \u00B7 \u25C8 \u00B7 \u25C8 \u00B7 "; ++ public static BossBar.Overlay commandCompassBarProgressOverlay = BossBar.Overlay.PROGRESS; ++ public static BossBar.Color commandCompassBarProgressColor = BossBar.Color.BLUE; ++ public static float commandCompassBarProgressPercent = 1.0F; ++ public static int commandCompassBarTickInterval = 5; ++ public static boolean commandGamemodeRequiresPermission = false; ++ public static boolean hideHiddenPlayersFromEntitySelector = false; ++ public static String uptimeFormat = ""; ++ public static String uptimeDay = "%02d day, "; ++ public static String uptimeDays = "%02d days, "; ++ public static String uptimeHour = "%02d hour, "; ++ public static String uptimeHours = "%02d hours, "; ++ public static String uptimeMinute = "%02d minute, and "; ++ public static String uptimeMinutes = "%02d minutes, and "; ++ public static String uptimeSecond = "%02d second"; ++ public static String uptimeSeconds = "%02d seconds"; ++ private static void commandSettings() { ++ commandRamBarTitle = getString("settings.command.rambar.title", commandRamBarTitle); ++ commandRamBarProgressOverlay = BossBar.Overlay.valueOf(getString("settings.command.rambar.overlay", commandRamBarProgressOverlay.name())); ++ commandRamBarProgressColorGood = BossBar.Color.valueOf(getString("settings.command.rambar.progress-color.good", commandRamBarProgressColorGood.name())); ++ commandRamBarProgressColorMedium = BossBar.Color.valueOf(getString("settings.command.rambar.progress-color.medium", commandRamBarProgressColorMedium.name())); ++ commandRamBarProgressColorLow = BossBar.Color.valueOf(getString("settings.command.rambar.progress-color.low", commandRamBarProgressColorLow.name())); ++ commandRamBarTextColorGood = getString("settings.command.rambar.text-color.good", commandRamBarTextColorGood); ++ commandRamBarTextColorMedium = getString("settings.command.rambar.text-color.medium", commandRamBarTextColorMedium); ++ commandRamBarTextColorLow = getString("settings.command.rambar.text-color.low", commandRamBarTextColorLow); ++ commandRamBarTickInterval = getInt("settings.command.rambar.tick-interval", commandRamBarTickInterval); ++ ++ commandTPSBarTitle = getString("settings.command.tpsbar.title", commandTPSBarTitle); ++ commandTPSBarProgressOverlay = BossBar.Overlay.valueOf(getString("settings.command.tpsbar.overlay", commandTPSBarProgressOverlay.name())); ++ commandTPSBarProgressFillMode = TPSBarTask.FillMode.valueOf(getString("settings.command.tpsbar.fill-mode", commandTPSBarProgressFillMode.name())); ++ commandTPSBarProgressColorGood = BossBar.Color.valueOf(getString("settings.command.tpsbar.progress-color.good", commandTPSBarProgressColorGood.name())); ++ commandTPSBarProgressColorMedium = BossBar.Color.valueOf(getString("settings.command.tpsbar.progress-color.medium", commandTPSBarProgressColorMedium.name())); ++ commandTPSBarProgressColorLow = BossBar.Color.valueOf(getString("settings.command.tpsbar.progress-color.low", commandTPSBarProgressColorLow.name())); ++ commandTPSBarTextColorGood = getString("settings.command.tpsbar.text-color.good", commandTPSBarTextColorGood); ++ commandTPSBarTextColorMedium = getString("settings.command.tpsbar.text-color.medium", commandTPSBarTextColorMedium); ++ commandTPSBarTextColorLow = getString("settings.command.tpsbar.text-color.low", commandTPSBarTextColorLow); ++ commandTPSBarTickInterval = getInt("settings.command.tpsbar.tick-interval", commandTPSBarTickInterval); ++ ++ commandCompassBarTitle = getString("settings.command.compass.title", commandCompassBarTitle); ++ commandCompassBarProgressOverlay = BossBar.Overlay.valueOf(getString("settings.command.compass.overlay", commandCompassBarProgressOverlay.name())); ++ commandCompassBarProgressColor = BossBar.Color.valueOf(getString("settings.command.compass.progress-color", commandCompassBarProgressColor.name())); ++ commandCompassBarProgressPercent = (float) getDouble("settings.command.compass.percent", commandCompassBarProgressPercent); ++ commandCompassBarTickInterval = getInt("settings.command.compass.tick-interval", commandCompassBarTickInterval); ++ ++ commandGamemodeRequiresPermission = getBoolean("settings.command.gamemode.requires-specific-permission", commandGamemodeRequiresPermission); ++ hideHiddenPlayersFromEntitySelector = getBoolean("settings.command.hide-hidden-players-from-entity-selector", hideHiddenPlayersFromEntitySelector); ++ uptimeFormat = getString("settings.command.uptime.format", uptimeFormat); ++ uptimeDay = getString("settings.command.uptime.day", uptimeDay); ++ uptimeDays = getString("settings.command.uptime.days", uptimeDays); ++ uptimeHour = getString("settings.command.uptime.hour", uptimeHour); ++ uptimeHours = getString("settings.command.uptime.hours", uptimeHours); ++ uptimeMinute = getString("settings.command.uptime.minute", uptimeMinute); ++ uptimeMinutes = getString("settings.command.uptime.minutes", uptimeMinutes); ++ uptimeSecond = getString("settings.command.uptime.second", uptimeSecond); ++ uptimeSeconds = getString("settings.command.uptime.seconds", uptimeSeconds); ++ } ++ ++ public static int barrelRows = 3; ++ public static boolean enderChestSixRows = false; ++ public static boolean enderChestPermissionRows = false; ++ public static boolean cryingObsidianValidForPortalFrame = false; ++ public static int beeInsideBeeHive = 3; ++ public static boolean anvilCumulativeCost = true; ++ public static int lightningRodRange = 128; ++ public static Set grindstoneIgnoredEnchants = new HashSet<>(); ++ public static boolean grindstoneRemoveAttributes = false; ++ public static boolean grindstoneRemoveDisplay = false; ++ public static int caveVinesMaxGrowthAge = 25; ++ public static int kelpMaxGrowthAge = 25; ++ public static int twistingVinesMaxGrowthAge = 25; ++ public static int weepingVinesMaxGrowthAge = 25; ++ private static void blockSettings() { ++ if (version < 3) { ++ boolean oldValue = getBoolean("settings.barrel.packed-barrels", true); ++ set("settings.blocks.barrel.six-rows", oldValue); ++ set("settings.packed-barrels", null); ++ oldValue = getBoolean("settings.large-ender-chests", true); ++ set("settings.blocks.ender_chest.six-rows", oldValue); ++ set("settings.large-ender-chests", null); ++ } ++ if (version < 20) { ++ boolean oldValue = getBoolean("settings.blocks.barrel.six-rows", false); ++ set("settings.blocks.barrel.rows", oldValue ? 6 : 3); ++ set("settings.blocks.barrel.six-rows", null); ++ } ++ barrelRows = getInt("settings.blocks.barrel.rows", barrelRows); ++ if (barrelRows < 1 || barrelRows > 6) { ++ Bukkit.getLogger().severe("settings.blocks.barrel.rows must be 1-6, resetting to default"); ++ barrelRows = 3; ++ } ++ org.bukkit.event.inventory.InventoryType.BARREL.setDefaultSize(switch (barrelRows) { ++ case 6 -> 54; ++ case 5 -> 45; ++ case 4 -> 36; ++ case 2 -> 18; ++ case 1 -> 9; ++ default -> 27; ++ }); ++ enderChestSixRows = getBoolean("settings.blocks.ender_chest.six-rows", enderChestSixRows); ++ org.bukkit.event.inventory.InventoryType.ENDER_CHEST.setDefaultSize(enderChestSixRows ? 54 : 27); ++ enderChestPermissionRows = getBoolean("settings.blocks.ender_chest.use-permissions-for-rows", enderChestPermissionRows); ++ cryingObsidianValidForPortalFrame = getBoolean("settings.blocks.crying_obsidian.valid-for-portal-frame", cryingObsidianValidForPortalFrame); ++ beeInsideBeeHive = getInt("settings.blocks.beehive.max-bees-inside", beeInsideBeeHive); ++ anvilCumulativeCost = getBoolean("settings.blocks.anvil.cumulative-cost", anvilCumulativeCost); ++ lightningRodRange = getInt("settings.blocks.lightning_rod.range", lightningRodRange); ++ ArrayList defaultCurses = new ArrayList<>(){{ ++ add("minecraft:binding_curse"); ++ add("minecraft:vanishing_curse"); ++ }}; ++ if (version < 24 && !getBoolean("settings.blocks.grindstone.ignore-curses", true)) { ++ defaultCurses.clear(); ++ } ++ getList("settings.blocks.grindstone.ignored-enchants", defaultCurses).forEach(key -> { ++ Enchantment enchantment = BuiltInRegistries.ENCHANTMENT.get(new ResourceLocation(key.toString())); ++ grindstoneIgnoredEnchants.add(enchantment); ++ }); ++ grindstoneRemoveAttributes = getBoolean("settings.blocks.grindstone.remove-attributes", grindstoneRemoveAttributes); ++ grindstoneRemoveDisplay = getBoolean("settings.blocks.grindstone.remove-name-and-lore", grindstoneRemoveDisplay); ++ caveVinesMaxGrowthAge = getInt("settings.blocks.cave_vines.max-growth-age", caveVinesMaxGrowthAge); ++ if (caveVinesMaxGrowthAge > 25) { ++ caveVinesMaxGrowthAge = 25; ++ log(Level.WARNING, "blocks.cave_vines.max-growth-age is set to above maximum allowed value of 25"); ++ log(Level.WARNING, "Using value of 25 to prevent issues"); ++ } ++ kelpMaxGrowthAge = getInt("settings.blocks.kelp.max-growth-age", kelpMaxGrowthAge); ++ if (kelpMaxGrowthAge > 25) { ++ kelpMaxGrowthAge = 25; ++ log(Level.WARNING, "blocks.kelp.max-growth-age is set to above maximum allowed value of 25"); ++ log(Level.WARNING, "Using value of 25 to prevent issues"); ++ } ++ twistingVinesMaxGrowthAge = getInt("settings.blocks.twisting_vines.max-growth-age", twistingVinesMaxGrowthAge); ++ if (twistingVinesMaxGrowthAge > 25) { ++ twistingVinesMaxGrowthAge = 25; ++ log(Level.WARNING, "blocks.twisting_vines.max-growth-age is set to above maximum allowed value of 25"); ++ log(Level.WARNING, "Using value of 25 to prevent issues"); ++ } ++ weepingVinesMaxGrowthAge = getInt("settings.blocks.weeping_vines.max-growth-age", weepingVinesMaxGrowthAge); ++ if (weepingVinesMaxGrowthAge > 25) { ++ weepingVinesMaxGrowthAge = 25; ++ log(Level.WARNING, "blocks.weeping_vines.max-growth-age is set to above maximum allowed value of 25"); ++ log(Level.WARNING, "Using value of 25 to prevent issues"); ++ } ++ } ++ ++ public static boolean allowInfinityMending = false; ++ public static boolean allowCrossbowInfinity = true; ++ public static boolean allowShearsLooting = false; ++ public static boolean allowUnsafeEnchants = false; ++ public static boolean allowInapplicableEnchants = true; ++ public static boolean allowIncompatibleEnchants = true; ++ public static boolean allowHigherEnchantsLevels = true; ++ public static boolean allowUnsafeEnchantCommand = false; ++ public static boolean replaceIncompatibleEnchants = false; ++ public static boolean clampEnchantLevels = true; ++ private static void enchantmentSettings() { ++ if (version < 5) { ++ boolean oldValue = getBoolean("settings.enchantment.allow-infinite-and-mending-together", false); ++ set("settings.enchantment.allow-infinity-and-mending-together", oldValue); ++ set("settings.enchantment.allow-infinite-and-mending-together", null); ++ } ++ if (version < 30) { ++ boolean oldValue = getBoolean("settings.enchantment.allow-unsafe-enchants", false); ++ set("settings.enchantment.anvil.allow-unsafe-enchants", oldValue); ++ set("settings.enchantment.anvil.allow-inapplicable-enchants", true); ++ set("settings.enchantment.anvil.allow-incompatible-enchants", true); ++ set("settings.enchantment.anvil.allow-higher-enchants-levels", true); ++ set("settings.enchantment.allow-unsafe-enchants", null); ++ } ++ allowInfinityMending = getBoolean("settings.enchantment.allow-infinity-and-mending-together", allowInfinityMending); ++ allowCrossbowInfinity = getBoolean("settings.enchantment.allow-infinity-on-crossbow", allowCrossbowInfinity); ++ allowShearsLooting = getBoolean("settings.enchantment.allow-looting-on-shears", allowShearsLooting); ++ allowUnsafeEnchants = getBoolean("settings.enchantment.anvil.allow-unsafe-enchants", allowUnsafeEnchants); ++ allowInapplicableEnchants = getBoolean("settings.enchantment.anvil.allow-inapplicable-enchants", allowInapplicableEnchants); ++ allowIncompatibleEnchants = getBoolean("settings.enchantment.anvil.allow-incompatible-enchants", allowIncompatibleEnchants); ++ allowHigherEnchantsLevels = getBoolean("settings.enchantment.anvil.allow-higher-enchants-levels", allowHigherEnchantsLevels); ++ allowUnsafeEnchantCommand = getBoolean("settings.enchantment.allow-unsafe-enchant-command", allowUnsafeEnchants); // allowUnsafeEnchants as default for backwards compatability ++ replaceIncompatibleEnchants = getBoolean("settings.enchantment.anvil.replace-incompatible-enchants", replaceIncompatibleEnchants); ++ clampEnchantLevels = getBoolean("settings.enchantment.clamp-levels", clampEnchantLevels); ++ } ++ ++ public static boolean endermanShortHeight = false; ++ private static void entitySettings() { ++ endermanShortHeight = getBoolean("settings.entity.enderman.short-height", endermanShortHeight); ++ if (endermanShortHeight) EntityType.ENDERMAN.setDimensions(EntityDimensions.scalable(0.6F, 1.9F)); ++ } ++ ++ public static boolean allowWaterPlacementInTheEnd = true; ++ private static void allowWaterPlacementInEnd() { ++ allowWaterPlacementInTheEnd = getBoolean("settings.allow-water-placement-in-the-end", allowWaterPlacementInTheEnd); ++ } ++ ++ public static boolean beeCountPayload = false; ++ private static void beeCountPayload() { ++ beeCountPayload = getBoolean("settings.bee-count-payload", beeCountPayload); ++ } ++ ++ public static boolean loggerSuppressInitLegacyMaterialError = false; ++ public static boolean loggerSuppressIgnoredAdvancementWarnings = false; ++ public static boolean loggerSuppressUnrecognizedRecipeErrors = false; ++ public static boolean loggerSuppressSetBlockFarChunk = false; ++ public static boolean loggerSuppressLibraryLoader = false; ++ private static void loggerSettings() { ++ loggerSuppressInitLegacyMaterialError = getBoolean("settings.logger.suppress-init-legacy-material-errors", loggerSuppressInitLegacyMaterialError); ++ loggerSuppressIgnoredAdvancementWarnings = getBoolean("settings.logger.suppress-ignored-advancement-warnings", loggerSuppressIgnoredAdvancementWarnings); ++ loggerSuppressUnrecognizedRecipeErrors = getBoolean("settings.logger.suppress-unrecognized-recipe-errors", loggerSuppressUnrecognizedRecipeErrors); ++ loggerSuppressSetBlockFarChunk = getBoolean("settings.logger.suppress-setblock-in-far-chunk-errors", loggerSuppressSetBlockFarChunk); ++ loggerSuppressLibraryLoader = getBoolean("settings.logger.suppress-library-loader", loggerSuppressLibraryLoader); ++ org.bukkit.plugin.java.JavaPluginLoader.SuppressLibraryLoaderLogger = loggerSuppressLibraryLoader; ++ } ++ ++ public static boolean tpsCatchup = true; ++ private static void tpsCatchup() { ++ tpsCatchup = getBoolean("settings.tps-catchup", tpsCatchup); ++ } ++ ++ public static boolean useUPnP = false; ++ public static boolean maxJoinsPerSecond = false; ++ public static boolean kickForOutOfOrderChat = true; ++ private static void networkSettings() { ++ useUPnP = getBoolean("settings.network.upnp-port-forwarding", useUPnP); ++ maxJoinsPerSecond = getBoolean("settings.network.max-joins-per-second", maxJoinsPerSecond); ++ kickForOutOfOrderChat = getBoolean("settings.network.kick-for-out-of-order-chat", kickForOutOfOrderChat); ++ } ++ ++ public static java.util.regex.Pattern usernameValidCharactersPattern; ++ private static void usernameValidationSettings() { ++ String defaultPattern = "^[a-zA-Z0-9_.]*$"; ++ String setPattern = getString("settings.username-valid-characters", defaultPattern); ++ usernameValidCharactersPattern = java.util.regex.Pattern.compile(setPattern == null || setPattern.isBlank() ? defaultPattern : setPattern); ++ } ++ ++ public static boolean fixProjectileLootingTransfer = false; ++ private static void fixProjectileLootingTransfer() { ++ fixProjectileLootingTransfer = getBoolean("settings.fix-projectile-looting-transfer", fixProjectileLootingTransfer); ++ } ++ ++ public static boolean clampAttributes = true; ++ private static void clampAttributes() { ++ clampAttributes = getBoolean("settings.clamp-attributes", clampAttributes); ++ } ++ ++ public static boolean limitArmor = true; ++ private static void limitArmor() { ++ limitArmor = getBoolean("settings.limit-armor", limitArmor); ++ } ++ ++ private static void blastResistanceSettings() { ++ getMap("settings.blast-resistance-overrides", Collections.emptyMap()).forEach((blockId, value) -> { ++ Block block = BuiltInRegistries.BLOCK.get(new ResourceLocation(blockId)); ++ if (block == Blocks.AIR) { ++ log(Level.SEVERE, "Invalid block for `settings.blast-resistance-overrides`: " + blockId); ++ return; ++ } ++ if (!(value instanceof Number blastResistance)) { ++ log(Level.SEVERE, "Invalid blast resistance for `settings.blast-resistance-overrides." + blockId + "`: " + value); ++ return; ++ } ++ block.explosionResistance = blastResistance.floatValue(); ++ }); ++ } ++ private static void blockFallMultiplierSettings() { ++ getMap("settings.block-fall-multipliers", Map.ofEntries( ++ Map.entry("minecraft:hay_block", Map.of("damage", 0.2F)), ++ Map.entry("minecraft:white_bed", Map.of("distance", 0.5F)), ++ Map.entry("minecraft:light_gray_bed", Map.of("distance", 0.5F)), ++ Map.entry("minecraft:gray_bed", Map.of("distance", 0.5F)), ++ Map.entry("minecraft:black_bed", Map.of("distance", 0.5F)), ++ Map.entry("minecraft:brown_bed", Map.of("distance", 0.5F)), ++ Map.entry("minecraft:pink_bed", Map.of("distance", 0.5F)), ++ Map.entry("minecraft:red_bed", Map.of("distance", 0.5F)), ++ Map.entry("minecraft:orange_bed", Map.of("distance", 0.5F)), ++ Map.entry("minecraft:yellow_bed", Map.of("distance", 0.5F)), ++ Map.entry("minecraft:green_bed", Map.of("distance", 0.5F)), ++ Map.entry("minecraft:lime_bed", Map.of("distance", 0.5F)), ++ Map.entry("minecraft:cyan_bed", Map.of("distance", 0.5F)), ++ Map.entry("minecraft:light_blue_bed", Map.of("distance", 0.5F)), ++ Map.entry("minecraft:blue_bed", Map.of("distance", 0.5F)), ++ Map.entry("minecraft:purple_bed", Map.of("distance", 0.5F)), ++ Map.entry("minecraft:magenta_bed", Map.of("distance", 0.5F)) ++ )).forEach((blockId, value) -> { ++ Block block = BuiltInRegistries.BLOCK.get(new ResourceLocation(blockId)); ++ if (block == Blocks.AIR) { ++ log(Level.SEVERE, "Invalid block for `settings.block-fall-multipliers`: " + blockId); ++ return; ++ } ++ if (!(value instanceof Map map)) { ++ log(Level.SEVERE, "Invalid fall multiplier for `settings.block-fall-multipliers." + blockId + "`: " + value ++ + ", expected a map with keys `damage` and `distance` to floats."); ++ return; ++ } ++ Object rawFallDamageMultiplier = map.get("damage"); ++ if (rawFallDamageMultiplier == null) rawFallDamageMultiplier = 1F; ++ if (!(rawFallDamageMultiplier instanceof Number fallDamageMultiplier)) { ++ log(Level.SEVERE, "Invalid multiplier for `settings.block-fall-multipliers." + blockId + ".damage`: " + map.get("damage")); ++ return; ++ } ++ Object rawFallDistanceMultiplier = map.get("distance"); ++ if (rawFallDistanceMultiplier == null) rawFallDistanceMultiplier = 1F; ++ if (!(rawFallDistanceMultiplier instanceof Number fallDistanceMultiplier)) { ++ log(Level.SEVERE, "Invalid multiplier for `settings.block-fall-multipliers." + blockId + ".distance`: " + map.get("distance")); ++ return; ++ } ++ block.fallDamageMultiplier = fallDamageMultiplier.floatValue(); ++ block.fallDistanceMultiplier = fallDistanceMultiplier.floatValue(); ++ }); ++ } ++ ++ public static boolean playerDeathsAlwaysShowItem = false; ++ private static void playerDeathsAlwaysShowItem() { ++ playerDeathsAlwaysShowItem = getBoolean("settings.player-deaths-always-show-item", playerDeathsAlwaysShowItem); ++ } ++ ++ public static boolean registerMinecraftDebugCommands = false; ++ private static void registerMinecraftDebugCommands() { ++ registerMinecraftDebugCommands = getBoolean("settings.register-minecraft-debug-commands", registerMinecraftDebugCommands); ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/PurpurWorldConfig.java b/src/main/java/org/purpurmc/purpur/PurpurWorldConfig.java +new file mode 100644 +index 0000000000000000000000000000000000000000..8704cc621937beda692bf484cf5ef11b2d7d7e4c +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/PurpurWorldConfig.java +@@ -0,0 +1,3289 @@ ++package org.purpurmc.purpur; ++ ++import net.minecraft.core.registries.BuiltInRegistries; ++import net.minecraft.resources.ResourceLocation; ++import net.minecraft.util.Mth; ++import net.minecraft.world.entity.Entity; ++import net.minecraft.world.entity.EntityType; ++import net.minecraft.world.item.DyeColor; ++import net.minecraft.world.item.Item; ++import net.minecraft.world.item.Items; ++import net.minecraft.world.level.block.Block; ++import net.minecraft.world.level.block.Blocks; ++import net.minecraft.world.level.block.state.properties.Tilt; ++import org.purpurmc.purpur.tool.Flattenable; ++import org.purpurmc.purpur.tool.Strippable; ++import org.purpurmc.purpur.tool.Tillable; ++import org.purpurmc.purpur.tool.Waxable; ++import org.purpurmc.purpur.tool.Weatherable; ++import org.apache.commons.lang.BooleanUtils; ++import org.bukkit.ChatColor; ++import org.bukkit.World; ++import org.bukkit.configuration.ConfigurationSection; ++import java.util.ArrayList; ++import java.util.HashMap; ++import java.util.List; ++import java.util.Map; ++import java.util.function.Predicate; ++import java.util.logging.Level; ++import static org.purpurmc.purpur.PurpurConfig.log; ++ ++@SuppressWarnings("unused") ++public class PurpurWorldConfig { ++ ++ private final String worldName; ++ private final World.Environment environment; ++ ++ public PurpurWorldConfig(String worldName, World.Environment environment) { ++ this.worldName = worldName; ++ this.environment = environment; ++ init(); ++ } ++ ++ public void init() { ++ log("-------- World Settings For [" + worldName + "] --------"); ++ PurpurConfig.readConfig(PurpurWorldConfig.class, this); ++ } ++ ++ private void set(String path, Object val) { ++ PurpurConfig.config.addDefault("world-settings.default." + path, val); ++ PurpurConfig.config.set("world-settings.default." + path, val); ++ if (PurpurConfig.config.get("world-settings." + worldName + "." + path) != null) { ++ PurpurConfig.config.addDefault("world-settings." + worldName + "." + path, val); ++ PurpurConfig.config.set("world-settings." + worldName + "." + path, val); ++ } ++ } ++ ++ private ConfigurationSection getConfigurationSection(String path) { ++ ConfigurationSection section = PurpurConfig.config.getConfigurationSection("world-settings." + worldName + "." + path); ++ return section != null ? section : PurpurConfig.config.getConfigurationSection("world-settings.default." + path); ++ } ++ ++ private String getString(String path, String def) { ++ PurpurConfig.config.addDefault("world-settings.default." + path, def); ++ return PurpurConfig.config.getString("world-settings." + worldName + "." + path, PurpurConfig.config.getString("world-settings.default." + path)); ++ } ++ ++ private boolean getBoolean(String path, boolean def) { ++ PurpurConfig.config.addDefault("world-settings.default." + path, def); ++ return PurpurConfig.config.getBoolean("world-settings." + worldName + "." + path, PurpurConfig.config.getBoolean("world-settings.default." + path)); ++ } ++ ++ private boolean getBoolean(String path, Predicate predicate) { ++ String val = getString(path, "default").toLowerCase(); ++ Boolean bool = BooleanUtils.toBooleanObject(val, "true", "false", "default"); ++ return predicate.test(bool); ++ } ++ ++ private double getDouble(String path, double def) { ++ PurpurConfig.config.addDefault("world-settings.default." + path, def); ++ return PurpurConfig.config.getDouble("world-settings." + worldName + "." + path, PurpurConfig.config.getDouble("world-settings.default." + path)); ++ } ++ ++ private int getInt(String path, int def) { ++ PurpurConfig.config.addDefault("world-settings.default." + path, def); ++ return PurpurConfig.config.getInt("world-settings." + worldName + "." + path, PurpurConfig.config.getInt("world-settings.default." + path)); ++ } ++ ++ private List getList(String path, T def) { ++ PurpurConfig.config.addDefault("world-settings.default." + path, def); ++ return PurpurConfig.config.getList("world-settings." + worldName + "." + path, PurpurConfig.config.getList("world-settings.default." + path)); ++ } ++ ++ private Map getMap(String path, Map def) { ++ final Map fallback = PurpurConfig.getMap("world-settings.default." + path, def); ++ final Map value = PurpurConfig.getMap("world-settings." + worldName + "." + path, null); ++ return value.isEmpty() ? fallback : value; ++ } ++ ++ public float armorstandStepHeight = 0.0F; ++ public boolean armorstandSetNameVisible = true; ++ public boolean armorstandFixNametags = false; ++ public boolean armorstandMovement = true; ++ public boolean armorstandWaterMovement = true; ++ public boolean armorstandWaterFence = true; ++ public boolean armorstandPlaceWithArms = false; ++ private void armorstandSettings() { ++ armorstandStepHeight = (float) getDouble("gameplay-mechanics.armorstand.step-height", armorstandStepHeight); ++ armorstandSetNameVisible = getBoolean("gameplay-mechanics.armorstand.set-name-visible-when-placing-with-custom-name", armorstandSetNameVisible); ++ armorstandFixNametags = getBoolean("gameplay-mechanics.armorstand.fix-nametags", armorstandFixNametags); ++ armorstandMovement = getBoolean("gameplay-mechanics.armorstand.can-movement-tick", armorstandMovement); ++ armorstandWaterMovement = getBoolean("gameplay-mechanics.armorstand.can-move-in-water", armorstandWaterMovement); ++ armorstandWaterFence = getBoolean("gameplay-mechanics.armorstand.can-move-in-water-over-fence", armorstandWaterFence); ++ armorstandPlaceWithArms = getBoolean("gameplay-mechanics.armorstand.place-with-arms-visible", armorstandPlaceWithArms); ++ } ++ ++ public boolean arrowMovementResetsDespawnCounter = true; ++ private void arrowSettings() { ++ arrowMovementResetsDespawnCounter = getBoolean("gameplay-mechanics.arrow.movement-resets-despawn-counter", arrowMovementResetsDespawnCounter); ++ } ++ ++ public boolean useBetterMending = false; ++ public double mendingMultiplier = 1.0; ++ public boolean alwaysTameInCreative = false; ++ public boolean boatEjectPlayersOnLand = false; ++ public boolean boatsDoFallDamage = false; ++ public boolean disableDropsOnCrammingDeath = false; ++ public boolean entitiesCanUsePortals = true; ++ public boolean entitiesPickUpLootBypassMobGriefing = false; ++ public boolean fireballsBypassMobGriefing = false; ++ public boolean imposeTeleportRestrictionsOnGateways = false; ++ public boolean milkCuresBadOmen = true; ++ public boolean milkClearsBeneficialEffects = true; ++ public boolean noteBlockIgnoreAbove = false; ++ public boolean persistentDroppableEntityDisplayNames = true; ++ public boolean projectilesBypassMobGriefing = false; ++ public boolean tickFluids = true; ++ public double mobsBlindnessMultiplier = 1; ++ public double tridentLoyaltyVoidReturnHeight = 0.0D; ++ public double voidDamageHeight = -64.0D; ++ public double voidDamageDealt = 4.0D; ++ public int raidCooldownSeconds = 0; ++ public int animalBreedingCooldownSeconds = 0; ++ public boolean mobsIgnoreRails = false; ++ public boolean rainStopsAfterSleep = true; ++ public boolean thunderStopsAfterSleep = true; ++ public int mobLastHurtByPlayerTime = 100; ++ public boolean disableOxidationProximityPenalty = false; ++ private void miscGameplayMechanicsSettings() { ++ useBetterMending = getBoolean("gameplay-mechanics.use-better-mending", useBetterMending); ++ mendingMultiplier = getDouble("gameplay-mechanics.mending-multiplier", mendingMultiplier); ++ alwaysTameInCreative = getBoolean("gameplay-mechanics.always-tame-in-creative", alwaysTameInCreative); ++ boatEjectPlayersOnLand = getBoolean("gameplay-mechanics.boat.eject-players-on-land", boatEjectPlayersOnLand); ++ boatsDoFallDamage = getBoolean("gameplay-mechanics.boat.do-fall-damage", boatsDoFallDamage); ++ disableDropsOnCrammingDeath = getBoolean("gameplay-mechanics.disable-drops-on-cramming-death", disableDropsOnCrammingDeath); ++ entitiesCanUsePortals = getBoolean("gameplay-mechanics.entities-can-use-portals", entitiesCanUsePortals); ++ entitiesPickUpLootBypassMobGriefing = getBoolean("gameplay-mechanics.entities-pick-up-loot-bypass-mob-griefing", entitiesPickUpLootBypassMobGriefing); ++ fireballsBypassMobGriefing = getBoolean("gameplay-mechanics.fireballs-bypass-mob-griefing", fireballsBypassMobGriefing); ++ imposeTeleportRestrictionsOnGateways = getBoolean("gameplay-mechanics.impose-teleport-restrictions-on-gateways", imposeTeleportRestrictionsOnGateways); ++ milkCuresBadOmen = getBoolean("gameplay-mechanics.milk-cures-bad-omen", milkCuresBadOmen); ++ milkClearsBeneficialEffects = getBoolean("gameplay-mechanics.milk-clears-beneficial-effects", milkClearsBeneficialEffects); ++ noteBlockIgnoreAbove = getBoolean("gameplay-mechanics.note-block-ignore-above", noteBlockIgnoreAbove); ++ persistentDroppableEntityDisplayNames = getBoolean("gameplay-mechanics.persistent-droppable-entity-display-names", persistentDroppableEntityDisplayNames); ++ projectilesBypassMobGriefing = getBoolean("gameplay-mechanics.projectiles-bypass-mob-griefing", projectilesBypassMobGriefing); ++ tickFluids = getBoolean("gameplay-mechanics.tick-fluids", tickFluids); ++ mobsBlindnessMultiplier = getDouble("gameplay-mechanics.entity-blindness-multiplier", mobsBlindnessMultiplier); ++ tridentLoyaltyVoidReturnHeight = getDouble("gameplay-mechanics.trident-loyalty-void-return-height", tridentLoyaltyVoidReturnHeight); ++ voidDamageHeight = getDouble("gameplay-mechanics.void-damage-height", voidDamageHeight); ++ voidDamageDealt = getDouble("gameplay-mechanics.void-damage-dealt", voidDamageDealt); ++ raidCooldownSeconds = getInt("gameplay-mechanics.raid-cooldown-seconds", raidCooldownSeconds); ++ animalBreedingCooldownSeconds = getInt("gameplay-mechanics.animal-breeding-cooldown-seconds", animalBreedingCooldownSeconds); ++ mobsIgnoreRails = getBoolean("gameplay-mechanics.mobs-ignore-rails", mobsIgnoreRails); ++ rainStopsAfterSleep = getBoolean("gameplay-mechanics.rain-stops-after-sleep", rainStopsAfterSleep); ++ thunderStopsAfterSleep = getBoolean("gameplay-mechanics.thunder-stops-after-sleep", thunderStopsAfterSleep); ++ mobLastHurtByPlayerTime = getInt("gameplay-mechanics.mob-last-hurt-by-player-time", mobLastHurtByPlayerTime); ++ disableOxidationProximityPenalty = getBoolean("gameplay-mechanics.disable-oxidation-proximity-penalty", disableOxidationProximityPenalty); ++ } ++ ++ public int daytimeTicks = 12000; ++ public int nighttimeTicks = 12000; ++ private void daytimeCycleSettings() { ++ daytimeTicks = getInt("gameplay-mechanics.daylight-cycle-ticks.daytime", daytimeTicks); ++ nighttimeTicks = getInt("gameplay-mechanics.daylight-cycle-ticks.nighttime", nighttimeTicks); ++ } ++ ++ public int drowningAirTicks = 300; ++ public int drowningDamageInterval = 20; ++ public double damageFromDrowning = 2.0F; ++ private void drowningSettings() { ++ drowningAirTicks = getInt("gameplay-mechanics.drowning.air-ticks", drowningAirTicks); ++ drowningDamageInterval = getInt("gameplay-mechanics.drowning.ticks-per-damage", drowningDamageInterval); ++ damageFromDrowning = getDouble("gameplay-mechanics.drowning.damage-from-drowning", damageFromDrowning); ++ } ++ ++ public int elytraDamagePerSecond = 1; ++ public double elytraDamageMultiplyBySpeed = 0; ++ public boolean elytraIgnoreUnbreaking = false; ++ public int elytraDamagePerFireworkBoost = 0; ++ public int elytraDamagePerTridentBoost = 0; ++ public boolean elytraKineticDamage = true; ++ private void elytraSettings() { ++ elytraDamagePerSecond = getInt("gameplay-mechanics.elytra.damage-per-second", elytraDamagePerSecond); ++ elytraDamageMultiplyBySpeed = getDouble("gameplay-mechanics.elytra.damage-multiplied-by-speed", elytraDamageMultiplyBySpeed); ++ elytraIgnoreUnbreaking = getBoolean("gameplay-mechanics.elytra.ignore-unbreaking", elytraIgnoreUnbreaking); ++ elytraDamagePerFireworkBoost = getInt("gameplay-mechanics.elytra.damage-per-boost.firework", elytraDamagePerFireworkBoost); ++ elytraDamagePerTridentBoost = getInt("gameplay-mechanics.elytra.damage-per-boost.trident", elytraDamagePerTridentBoost); ++ elytraKineticDamage = getBoolean("gameplay-mechanics.elytra.kinetic-damage", elytraKineticDamage); ++ } ++ ++ public int entityLifeSpan = 0; ++ public float entityLeftHandedChance = 0.05f; ++ public boolean entitySharedRandom = true; ++ private void entitySettings() { ++ entityLifeSpan = getInt("gameplay-mechanics.entity-lifespan", entityLifeSpan); ++ entityLeftHandedChance = (float) getDouble("gameplay-mechanics.entity-left-handed-chance", entityLeftHandedChance); ++ entitySharedRandom = getBoolean("settings.entity.shared-random", entitySharedRandom); ++ } ++ ++ public boolean explosionClampRadius = true; ++ private void explosionSettings() { ++ explosionClampRadius = getBoolean("gameplay-mechanics.clamp-explosion-radius", explosionClampRadius); ++ } ++ ++ public boolean infinityWorksWithoutArrows = false; ++ public boolean infinityWorksWithNormalArrows = true; ++ public boolean infinityWorksWithSpectralArrows = false; ++ public boolean infinityWorksWithTippedArrows = false; ++ private void infinityArrowsSettings() { ++ infinityWorksWithoutArrows = getBoolean("gameplay-mechanics.infinity-bow.works-without-arrows", infinityWorksWithoutArrows); ++ infinityWorksWithNormalArrows = getBoolean("gameplay-mechanics.infinity-bow.normal-arrows", infinityWorksWithNormalArrows); ++ infinityWorksWithSpectralArrows = getBoolean("gameplay-mechanics.infinity-bow.spectral-arrows", infinityWorksWithSpectralArrows); ++ infinityWorksWithTippedArrows = getBoolean("gameplay-mechanics.infinity-bow.tipped-arrows", infinityWorksWithTippedArrows); ++ } ++ ++ public List itemImmuneToCactus = new ArrayList<>(); ++ public List itemImmuneToExplosion = new ArrayList<>(); ++ public List itemImmuneToFire = new ArrayList<>(); ++ public List itemImmuneToLightning = new ArrayList<>(); ++ public boolean dontRunWithScissors = false; ++ public boolean ignoreScissorsInWater = false; ++ public boolean ignoreScissorsInLava = false; ++ public double scissorsRunningDamage = 1D; ++ public float enderPearlDamage = 5.0F; ++ public int enderPearlCooldown = 20; ++ public int enderPearlCooldownCreative = 20; ++ public float enderPearlEndermiteChance = 0.05F; ++ public int glowBerriesEatGlowDuration = 0; ++ public boolean shulkerBoxItemDropContentsWhenDestroyed = true; ++ public boolean compassItemShowsBossBar = false; ++ public boolean snowballExtinguishesFire = false; ++ public boolean snowballExtinguishesCandles = false; ++ public boolean snowballExtinguishesCampfires = false; ++ private void itemSettings() { ++ itemImmuneToCactus.clear(); ++ getList("gameplay-mechanics.item.immune.cactus", new ArrayList<>()).forEach(key -> { ++ if (key.toString().equals("*")) { ++ BuiltInRegistries.ITEM.stream().filter(item -> item != Items.AIR).forEach((item) -> itemImmuneToCactus.add(item)); ++ return; ++ } ++ Item item = BuiltInRegistries.ITEM.get(new ResourceLocation(key.toString())); ++ if (item != Items.AIR) itemImmuneToCactus.add(item); ++ }); ++ itemImmuneToExplosion.clear(); ++ getList("gameplay-mechanics.item.immune.explosion", new ArrayList<>()).forEach(key -> { ++ if (key.toString().equals("*")) { ++ BuiltInRegistries.ITEM.stream().filter(item -> item != Items.AIR).forEach((item) -> itemImmuneToExplosion.add(item)); ++ return; ++ } ++ Item item = BuiltInRegistries.ITEM.get(new ResourceLocation(key.toString())); ++ if (item != Items.AIR) itemImmuneToExplosion.add(item); ++ }); ++ itemImmuneToFire.clear(); ++ getList("gameplay-mechanics.item.immune.fire", new ArrayList<>()).forEach(key -> { ++ if (key.toString().equals("*")) { ++ BuiltInRegistries.ITEM.stream().filter(item -> item != Items.AIR).forEach((item) -> itemImmuneToFire.add(item)); ++ return; ++ } ++ Item item = BuiltInRegistries.ITEM.get(new ResourceLocation(key.toString())); ++ if (item != Items.AIR) itemImmuneToFire.add(item); ++ }); ++ itemImmuneToLightning.clear(); ++ getList("gameplay-mechanics.item.immune.lightning", new ArrayList<>()).forEach(key -> { ++ if (key.toString().equals("*")) { ++ BuiltInRegistries.ITEM.stream().filter(item -> item != Items.AIR).forEach((item) -> itemImmuneToLightning.add(item)); ++ return; ++ } ++ Item item = BuiltInRegistries.ITEM.get(new ResourceLocation(key.toString())); ++ if (item != Items.AIR) itemImmuneToLightning.add(item); ++ }); ++ dontRunWithScissors = getBoolean("gameplay-mechanics.item.shears.damage-if-sprinting", dontRunWithScissors); ++ ignoreScissorsInWater = getBoolean("gameplay-mechanics.item.shears.ignore-in-water", ignoreScissorsInWater); ++ ignoreScissorsInLava = getBoolean("gameplay-mechanics.item.shears.ignore-in-lava", ignoreScissorsInLava); ++ scissorsRunningDamage = getDouble("gameplay-mechanics.item.shears.sprinting-damage", scissorsRunningDamage); ++ enderPearlDamage = (float) getDouble("gameplay-mechanics.item.ender-pearl.damage", enderPearlDamage); ++ enderPearlCooldown = getInt("gameplay-mechanics.item.ender-pearl.cooldown", enderPearlCooldown); ++ enderPearlCooldownCreative = getInt("gameplay-mechanics.item.ender-pearl.creative-cooldown", enderPearlCooldownCreative); ++ enderPearlEndermiteChance = (float) getDouble("gameplay-mechanics.item.ender-pearl.endermite-spawn-chance", enderPearlEndermiteChance); ++ glowBerriesEatGlowDuration = getInt("gameplay-mechanics.item.glow_berries.eat-glow-duration", glowBerriesEatGlowDuration); ++ shulkerBoxItemDropContentsWhenDestroyed = getBoolean("gameplay-mechanics.item.shulker_box.drop-contents-when-destroyed", shulkerBoxItemDropContentsWhenDestroyed); ++ compassItemShowsBossBar = getBoolean("gameplay-mechanics.item.compass.holding-shows-bossbar", compassItemShowsBossBar); ++ snowballExtinguishesFire = getBoolean("gameplay-mechanics.item.snowball.extinguish.fire", snowballExtinguishesFire); ++ snowballExtinguishesCandles = getBoolean("gameplay-mechanics.item.snowball.extinguish.candles", snowballExtinguishesCandles); ++ snowballExtinguishesCampfires = getBoolean("gameplay-mechanics.item.snowball.extinguish.campfires", snowballExtinguishesCampfires); ++ } ++ ++ public double minecartMaxSpeed = 0.4D; ++ public boolean minecartPlaceAnywhere = false; ++ public boolean minecartControllable = false; ++ public float minecartControllableStepHeight = 1.0F; ++ public double minecartControllableHopBoost = 0.5D; ++ public boolean minecartControllableFallDamage = true; ++ public double minecartControllableBaseSpeed = 0.1D; ++ public Map minecartControllableBlockSpeeds = new HashMap<>(); ++ public double poweredRailBoostModifier = 0.06; ++ private void minecartSettings() { ++ if (PurpurConfig.version < 12) { ++ boolean oldBool = getBoolean("gameplay-mechanics.controllable-minecarts.place-anywhere", minecartPlaceAnywhere); ++ set("gameplay-mechanics.controllable-minecarts.place-anywhere", null); ++ set("gameplay-mechanics.minecart.place-anywhere", oldBool); ++ oldBool = getBoolean("gameplay-mechanics.controllable-minecarts.enabled", minecartControllable); ++ set("gameplay-mechanics.controllable-minecarts.enabled", null); ++ set("gameplay-mechanics.minecart.controllable.enabled", oldBool); ++ double oldDouble = getDouble("gameplay-mechanics.controllable-minecarts.step-height", minecartControllableStepHeight); ++ set("gameplay-mechanics.controllable-minecarts.step-height", null); ++ set("gameplay-mechanics.minecart.controllable.step-height", oldDouble); ++ oldDouble = getDouble("gameplay-mechanics.controllable-minecarts.hop-boost", minecartControllableHopBoost); ++ set("gameplay-mechanics.controllable-minecarts.hop-boost", null); ++ set("gameplay-mechanics.minecart.controllable.hop-boost", oldDouble); ++ oldBool = getBoolean("gameplay-mechanics.controllable-minecarts.fall-damage", minecartControllableFallDamage); ++ set("gameplay-mechanics.controllable-minecarts.fall-damage", null); ++ set("gameplay-mechanics.minecart.controllable.fall-damage", oldBool); ++ oldDouble = getDouble("gameplay-mechanics.controllable-minecarts.base-speed", minecartControllableBaseSpeed); ++ set("gameplay-mechanics.controllable-minecarts.base-speed", null); ++ set("gameplay-mechanics.minecart.controllable.base-speed", oldDouble); ++ ConfigurationSection section = getConfigurationSection("gameplay-mechanics.controllable-minecarts.block-speed"); ++ if (section != null) { ++ for (String key : section.getKeys(false)) { ++ if ("grass-block".equals(key)) key = "grass_block"; // oopsie ++ oldDouble = section.getDouble(key, minecartControllableBaseSpeed); ++ set("gameplay-mechanics.controllable-minecarts.block-speed." + key, null); ++ set("gameplay-mechanics.minecart.controllable.block-speed." + key, oldDouble); ++ } ++ set("gameplay-mechanics.controllable-minecarts.block-speed", null); ++ } ++ set("gameplay-mechanics.controllable-minecarts", null); ++ } ++ ++ minecartMaxSpeed = getDouble("gameplay-mechanics.minecart.max-speed", minecartMaxSpeed); ++ minecartPlaceAnywhere = getBoolean("gameplay-mechanics.minecart.place-anywhere", minecartPlaceAnywhere); ++ minecartControllable = getBoolean("gameplay-mechanics.minecart.controllable.enabled", minecartControllable); ++ minecartControllableStepHeight = (float) getDouble("gameplay-mechanics.minecart.controllable.step-height", minecartControllableStepHeight); ++ minecartControllableHopBoost = getDouble("gameplay-mechanics.minecart.controllable.hop-boost", minecartControllableHopBoost); ++ minecartControllableFallDamage = getBoolean("gameplay-mechanics.minecart.controllable.fall-damage", minecartControllableFallDamage); ++ minecartControllableBaseSpeed = getDouble("gameplay-mechanics.minecart.controllable.base-speed", minecartControllableBaseSpeed); ++ ConfigurationSection section = getConfigurationSection("gameplay-mechanics.minecart.controllable.block-speed"); ++ if (section != null) { ++ for (String key : section.getKeys(false)) { ++ Block block = BuiltInRegistries.BLOCK.get(new ResourceLocation(key)); ++ if (block != Blocks.AIR) { ++ minecartControllableBlockSpeeds.put(block, section.getDouble(key, minecartControllableBaseSpeed)); ++ } ++ } ++ } else { ++ set("gameplay-mechanics.minecart.controllable.block-speed.grass_block", 0.3D); ++ set("gameplay-mechanics.minecart.controllable.block-speed.stone", 0.5D); ++ } ++ poweredRailBoostModifier = getDouble("gameplay-mechanics.minecart.powered-rail.boost-modifier", poweredRailBoostModifier); ++ } ++ ++ public float entityHealthRegenAmount = 1.0F; ++ public float entityMinimalHealthPoison = 1.0F; ++ public float entityPoisonDegenerationAmount = 1.0F; ++ public float entityWitherDegenerationAmount = 1.0F; ++ public float humanHungerExhaustionAmount = 0.005F; ++ public float humanSaturationRegenAmount = 1.0F; ++ private void mobEffectSettings() { ++ entityHealthRegenAmount = (float) getDouble("gameplay-mechanics.mob-effects.health-regen-amount", entityHealthRegenAmount); ++ entityMinimalHealthPoison = (float) getDouble("gameplay-mechanics.mob-effects.minimal-health-poison-amount", entityMinimalHealthPoison); ++ entityPoisonDegenerationAmount = (float) getDouble("gameplay-mechanics.mob-effects.poison-degeneration-amount", entityPoisonDegenerationAmount); ++ entityWitherDegenerationAmount = (float) getDouble("gameplay-mechanics.mob-effects.wither-degeneration-amount", entityWitherDegenerationAmount); ++ humanHungerExhaustionAmount = (float) getDouble("gameplay-mechanics.mob-effects.hunger-exhaustion-amount", humanHungerExhaustionAmount); ++ humanSaturationRegenAmount = (float) getDouble("gameplay-mechanics.mob-effects.saturation-regen-amount", humanSaturationRegenAmount); ++ } ++ ++ public boolean catSpawning; ++ public boolean patrolSpawning; ++ public boolean phantomSpawning; ++ public boolean villagerTraderSpawning; ++ public boolean villageSiegeSpawning; ++ public boolean mobSpawningIgnoreCreativePlayers = false; ++ private void mobSpawnerSettings() { ++ // values of "default" or null will default to true only if the world environment is normal (aka overworld) ++ Predicate predicate = (bool) -> (bool != null && bool) || (bool == null && environment == World.Environment.NORMAL); ++ catSpawning = getBoolean("gameplay-mechanics.mob-spawning.village-cats", predicate); ++ patrolSpawning = getBoolean("gameplay-mechanics.mob-spawning.raid-patrols", predicate); ++ phantomSpawning = getBoolean("gameplay-mechanics.mob-spawning.phantoms", predicate); ++ villagerTraderSpawning = getBoolean("gameplay-mechanics.mob-spawning.wandering-traders", predicate); ++ villageSiegeSpawning = getBoolean("gameplay-mechanics.mob-spawning.village-sieges", predicate); ++ mobSpawningIgnoreCreativePlayers = getBoolean("gameplay-mechanics.mob-spawning.ignore-creative-players", mobSpawningIgnoreCreativePlayers); ++ } ++ ++ public boolean disableObserverClocks = false; ++ private void observerSettings() { ++ disableObserverClocks = getBoolean("blocks.observer.disable-clock", disableObserverClocks); ++ } ++ ++ public int playerNetheriteFireResistanceDuration = 0; ++ public int playerNetheriteFireResistanceAmplifier = 0; ++ public boolean playerNetheriteFireResistanceAmbient = false; ++ public boolean playerNetheriteFireResistanceShowParticles = false; ++ public boolean playerNetheriteFireResistanceShowIcon = true; ++ private void playerNetheriteFireResistance() { ++ playerNetheriteFireResistanceDuration = getInt("gameplay-mechanics.player.netherite-fire-resistance.duration", playerNetheriteFireResistanceDuration); ++ playerNetheriteFireResistanceAmplifier = getInt("gameplay-mechanics.player.netherite-fire-resistance.amplifier", playerNetheriteFireResistanceAmplifier); ++ playerNetheriteFireResistanceAmbient = getBoolean("gameplay-mechanics.player.netherite-fire-resistance.ambient", playerNetheriteFireResistanceAmbient); ++ playerNetheriteFireResistanceShowParticles = getBoolean("gameplay-mechanics.player.netherite-fire-resistance.show-particles", playerNetheriteFireResistanceShowParticles); ++ playerNetheriteFireResistanceShowIcon = getBoolean("gameplay-mechanics.player.netherite-fire-resistance.show-icon", playerNetheriteFireResistanceShowIcon); ++ } ++ ++ public boolean idleTimeoutKick = true; ++ public boolean idleTimeoutTickNearbyEntities = true; ++ public boolean idleTimeoutCountAsSleeping = false; ++ public boolean idleTimeoutUpdateTabList = false; ++ public boolean idleTimeoutTargetPlayer = true; ++ public String playerDeathExpDropEquation = "expLevel * 7"; ++ public int playerDeathExpDropMax = 100; ++ public boolean teleportIfOutsideBorder = false; ++ public boolean teleportOnNetherCeilingDamage = false; ++ public boolean totemOfUndyingWorksInInventory = false; ++ public boolean playerFixStuckPortal = false; ++ public boolean creativeOnePunch = false; ++ public boolean playerSleepNearMonsters = false; ++ public boolean playersSkipNight = true; ++ public double playerCriticalDamageMultiplier = 1.5D; ++ public int playerBurpDelay = 10; ++ public boolean playerBurpWhenFull = false; ++ public boolean playerRidableInWater = false; ++ public boolean playerRemoveBindingWithWeakness = false; ++ public int shiftRightClickRepairsMendingPoints = 0; ++ public int playerExpPickupDelay = 2; ++ public boolean playerVoidTrading = false; ++ private void playerSettings() { ++ if (PurpurConfig.version < 19) { ++ boolean oldVal = getBoolean("gameplay-mechanics.player.idle-timeout.mods-target", idleTimeoutTargetPlayer); ++ set("gameplay-mechanics.player.idle-timeout.mods-target", null); ++ set("gameplay-mechanics.player.idle-timeout.mobs-target", oldVal); ++ } ++ idleTimeoutKick = System.getenv("PURPUR_FORCE_IDLE_KICK") == null ? getBoolean("gameplay-mechanics.player.idle-timeout.kick-if-idle", idleTimeoutKick) : Boolean.parseBoolean(System.getenv("PURPUR_FORCE_IDLE_KICK")); ++ idleTimeoutTickNearbyEntities = getBoolean("gameplay-mechanics.player.idle-timeout.tick-nearby-entities", idleTimeoutTickNearbyEntities); ++ idleTimeoutCountAsSleeping = getBoolean("gameplay-mechanics.player.idle-timeout.count-as-sleeping", idleTimeoutCountAsSleeping); ++ idleTimeoutUpdateTabList = getBoolean("gameplay-mechanics.player.idle-timeout.update-tab-list", idleTimeoutUpdateTabList); ++ idleTimeoutTargetPlayer = getBoolean("gameplay-mechanics.player.idle-timeout.mobs-target", idleTimeoutTargetPlayer); ++ playerDeathExpDropEquation = getString("gameplay-mechanics.player.exp-dropped-on-death.equation", playerDeathExpDropEquation); ++ playerDeathExpDropMax = getInt("gameplay-mechanics.player.exp-dropped-on-death.maximum", playerDeathExpDropMax); ++ teleportIfOutsideBorder = getBoolean("gameplay-mechanics.player.teleport-if-outside-border", teleportIfOutsideBorder); ++ teleportOnNetherCeilingDamage = getBoolean("gameplay-mechanics.player.teleport-on-nether-ceiling-damage", teleportOnNetherCeilingDamage); ++ totemOfUndyingWorksInInventory = getBoolean("gameplay-mechanics.player.totem-of-undying-works-in-inventory", totemOfUndyingWorksInInventory); ++ playerFixStuckPortal = getBoolean("gameplay-mechanics.player.fix-stuck-in-portal", playerFixStuckPortal); ++ creativeOnePunch = getBoolean("gameplay-mechanics.player.one-punch-in-creative", creativeOnePunch); ++ playerSleepNearMonsters = getBoolean("gameplay-mechanics.player.sleep-ignore-nearby-mobs", playerSleepNearMonsters); ++ playersSkipNight = getBoolean("gameplay-mechanics.player.can-skip-night", playersSkipNight); ++ playerCriticalDamageMultiplier = getDouble("gameplay-mechanics.player.critical-damage-multiplier", playerCriticalDamageMultiplier); ++ playerBurpDelay = getInt("gameplay-mechanics.player.burp-delay", playerBurpDelay); ++ playerBurpWhenFull = getBoolean("gameplay-mechanics.player.burp-when-full", playerBurpWhenFull); ++ playerRidableInWater = getBoolean("gameplay-mechanics.player.ridable-in-water", playerRidableInWater); ++ playerRemoveBindingWithWeakness = getBoolean("gameplay-mechanics.player.curse-of-binding.remove-with-weakness", playerRemoveBindingWithWeakness); ++ shiftRightClickRepairsMendingPoints = getInt("gameplay-mechanics.player.shift-right-click-repairs-mending-points", shiftRightClickRepairsMendingPoints); ++ playerExpPickupDelay = getInt("gameplay-mechanics.player.exp-pickup-delay-ticks", playerExpPickupDelay); ++ playerVoidTrading = getBoolean("gameplay-mechanics.player.allow-void-trading", playerVoidTrading); ++ } ++ ++ public boolean silkTouchEnabled = false; ++ public String silkTouchSpawnerName = "Monster Spawner"; ++ public List silkTouchSpawnerLore = new ArrayList<>(); ++ public List silkTouchTools = new ArrayList<>(); ++ public int minimumSilkTouchSpawnerRequire = 1; ++ private void silkTouchSettings() { ++ if (PurpurConfig.version < 21) { ++ String oldName = getString("gameplay-mechanics.silk-touch.spawner-name", silkTouchSpawnerName); ++ set("gameplay-mechanics.silk-touch.spawner-name", "" + ChatColor.toMM(oldName.replace("{mob}", ""))); ++ List list = new ArrayList<>(); ++ getList("gameplay-mechanics.silk-touch.spawner-lore", List.of("Spawns a ")) ++ .forEach(line -> list.add("" + ChatColor.toMM(line.toString().replace("{mob}", "")))); ++ set("gameplay-mechanics.silk-touch.spawner-lore", list); ++ } ++ silkTouchEnabled = getBoolean("gameplay-mechanics.silk-touch.enabled", silkTouchEnabled); ++ silkTouchSpawnerName = getString("gameplay-mechanics.silk-touch.spawner-name", silkTouchSpawnerName); ++ minimumSilkTouchSpawnerRequire = getInt("gameplay-mechanics.silk-touch.minimal-level", minimumSilkTouchSpawnerRequire); ++ silkTouchSpawnerLore.clear(); ++ getList("gameplay-mechanics.silk-touch.spawner-lore", List.of("Spawns a ")) ++ .forEach(line -> silkTouchSpawnerLore.add(line.toString())); ++ silkTouchTools.clear(); ++ getList("gameplay-mechanics.silk-touch.tools", List.of( ++ "minecraft:iron_pickaxe", ++ "minecraft:golden_pickaxe", ++ "minecraft:diamond_pickaxe", ++ "minecraft:netherite_pickaxe" ++ )).forEach(key -> { ++ Item item = BuiltInRegistries.ITEM.get(new ResourceLocation(key.toString())); ++ if (item != Items.AIR) silkTouchTools.add(item); ++ }); ++ } ++ ++ private static boolean projectileDespawnRateSettingsMigrated = false; ++ private void projectileDespawnRateSettings() { ++ if (PurpurConfig.version < 28 && !projectileDespawnRateSettingsMigrated) { ++ migrateProjectileDespawnRateSettings(EntityType.DRAGON_FIREBALL); ++ migrateProjectileDespawnRateSettings(EntityType.EGG); ++ migrateProjectileDespawnRateSettings(EntityType.ENDER_PEARL); ++ migrateProjectileDespawnRateSettings(EntityType.EXPERIENCE_BOTTLE); ++ migrateProjectileDespawnRateSettings(EntityType.FIREWORK_ROCKET); ++ migrateProjectileDespawnRateSettings(EntityType.FISHING_BOBBER); ++ migrateProjectileDespawnRateSettings(EntityType.FIREBALL); ++ migrateProjectileDespawnRateSettings(EntityType.LLAMA_SPIT); ++ migrateProjectileDespawnRateSettings(EntityType.POTION); ++ migrateProjectileDespawnRateSettings(EntityType.SHULKER_BULLET); ++ migrateProjectileDespawnRateSettings(EntityType.SMALL_FIREBALL); ++ migrateProjectileDespawnRateSettings(EntityType.SNOWBALL); ++ migrateProjectileDespawnRateSettings(EntityType.WITHER_SKULL); ++ //PufferfishConfig.save(); // TODO: Pufferfish ++ set("gameplay-mechanics.projectile-despawn-rates", null); ++ // pufferfish's entity_timeout is a global config ++ // we only want to migrate values from the ++ // default world (first world loaded) ++ projectileDespawnRateSettingsMigrated = true; ++ } ++ } ++ private void migrateProjectileDespawnRateSettings(EntityType type) { ++ // TODO: Pufferfish ++ //String pufferName = "entity_timeouts." + type.id.toUpperCase(Locale.ROOT); ++ //int value = getInt("gameplay-mechanics.projectile-despawn-rates." + type.id, -1); ++ //if (value != -1 && PufferfishConfig.getRawInt(pufferName, -1) == -1) { ++ // PufferfishConfig.setInt(pufferName, value); ++ // type.ttl = value; ++ //} ++ } ++ ++ public double bowProjectileOffset = 1.0D; ++ public double crossbowProjectileOffset = 1.0D; ++ public double eggProjectileOffset = 1.0D; ++ public double enderPearlProjectileOffset = 1.0D; ++ public double throwablePotionProjectileOffset = 1.0D; ++ public double tridentProjectileOffset = 1.0D; ++ public double snowballProjectileOffset = 1.0D; ++ private void projectileOffsetSettings() { ++ bowProjectileOffset = getDouble("gameplay-mechanics.projectile-offset.bow", bowProjectileOffset); ++ crossbowProjectileOffset = getDouble("gameplay-mechanics.projectile-offset.crossbow", crossbowProjectileOffset); ++ eggProjectileOffset = getDouble("gameplay-mechanics.projectile-offset.egg", eggProjectileOffset); ++ enderPearlProjectileOffset = getDouble("gameplay-mechanics.projectile-offset.ender-pearl", enderPearlProjectileOffset); ++ throwablePotionProjectileOffset = getDouble("gameplay-mechanics.projectile-offset.throwable-potion", throwablePotionProjectileOffset); ++ tridentProjectileOffset = getDouble("gameplay-mechanics.projectile-offset.trident", tridentProjectileOffset); ++ snowballProjectileOffset = getDouble("gameplay-mechanics.projectile-offset.snowball", snowballProjectileOffset); ++ } ++ ++ public int snowballDamage = -1; ++ private void snowballSettings() { ++ snowballDamage = getInt("gameplay-mechanics.projectile-damage.snowball", snowballDamage); ++ } ++ ++ public Map axeStrippables = new HashMap<>(); ++ public Map axeWaxables = new HashMap<>(); ++ public Map axeWeatherables = new HashMap<>(); ++ public Map hoeTillables = new HashMap<>(); ++ public Map shovelFlattenables = new HashMap<>(); ++ public boolean hoeReplantsCrops = false; ++ public boolean hoeReplantsNetherWarts = false; ++ private void toolSettings() { ++ axeStrippables.clear(); ++ axeWaxables.clear(); ++ axeWeatherables.clear(); ++ hoeTillables.clear(); ++ shovelFlattenables.clear(); ++ if (PurpurConfig.version < 18) { ++ ConfigurationSection section = PurpurConfig.config.getConfigurationSection("world-settings." + worldName + ".tools.hoe.tilling"); ++ if (section != null) { ++ PurpurConfig.config.set("world-settings." + worldName + ".tools.hoe.tillables", section); ++ PurpurConfig.config.set("world-settings." + worldName + ".tools.hoe.tilling", null); ++ } ++ section = PurpurConfig.config.getConfigurationSection("world-settings.default.tools.hoe.tilling"); ++ if (section != null) { ++ PurpurConfig.config.set("world-settings.default.tools.hoe.tillables", section); ++ PurpurConfig.config.set("world-settings.default.tools.hoe.tilling", null); ++ } ++ } ++ if (PurpurConfig.version < 29) { ++ PurpurConfig.config.set("world-settings.default.tools.axe.strippables.minecraft:mangrove_log", Map.of("into", "minecraft:stripped_mangrove_log", "drops", new HashMap())); ++ PurpurConfig.config.set("world-settings.default.tools.axe.strippables.minecraft:mangrove_wood", Map.of("into", "minecraft:stripped_mangrove_wood", "drops", new HashMap())); ++ } ++ if (PurpurConfig.version < 32) { ++ PurpurConfig.config.set("world-settings.default.tools.axe.strippables.minecraft:cherry_log", Map.of("into", "minecraft:stripped_cherry_log", "drops", new HashMap())); ++ PurpurConfig.config.set("world-settings.default.tools.axe.strippables.minecraft:cherry_wood", Map.of("into", "minecraft:stripped_cherry_wood", "drops", new HashMap())); ++ PurpurConfig.config.set("world-settings.default.tools.axe.strippables.minecraft:bamboo_block", Map.of("into", "minecraft:stripped_bamboo_block", "drops", new HashMap())); ++ } ++ if (PurpurConfig.version < 33) { ++ getList("gameplay-mechanics.shovel-turns-block-to-grass-path", new ArrayList(){{ ++ add("minecraft:coarse_dirt"); ++ add("minecraft:dirt"); ++ add("minecraft:grass_block"); ++ add("minecraft:mycelium"); ++ add("minecraft:podzol"); ++ add("minecraft:rooted_dirt"); ++ }}).forEach(key -> { ++ PurpurConfig.config.set("world-settings.default.tools.shovel.flattenables." + key.toString(), Map.of("into", "minecraft:dirt_path", "drops", new HashMap())); ++ }); ++ set("gameplay-mechanics.shovel-turns-block-to-grass-path", null); ++ } ++ if (PurpurConfig.version < 34) { ++ PurpurConfig.config.set("world-settings.default.tools.axe.waxables.minecraft:waxed_chiseled_copper", Map.of("into", "minecraft:chiseled_copper", "drops", new HashMap())); ++ PurpurConfig.config.set("world-settings.default.tools.axe.waxables.minecraft:waxed_exposed_chiseled_copper", Map.of("into", "minecraft:exposed_chiseled_copper", "drops", new HashMap())); ++ PurpurConfig.config.set("world-settings.default.tools.axe.waxables.minecraft:waxed_weathered_chiseled_copper", Map.of("into", "minecraft:weathered_chiseled_copper", "drops", new HashMap())); ++ PurpurConfig.config.set("world-settings.default.tools.axe.waxables.minecraft:waxed_oxidized_chiseled_copper", Map.of("into", "minecraft:oxidized_chiseled_copper", "drops", new HashMap())); ++ PurpurConfig.config.set("world-settings.default.tools.axe.waxables.minecraft:waxed_copper_door", Map.of("into", "minecraft:copper_door", "drops", new HashMap())); ++ PurpurConfig.config.set("world-settings.default.tools.axe.waxables.minecraft:waxed_exposed_copper_door", Map.of("into", "minecraft:exposed_copper_door", "drops", new HashMap())); ++ PurpurConfig.config.set("world-settings.default.tools.axe.waxables.minecraft:waxed_weathered_copper_door", Map.of("into", "minecraft:weathered_copper_door", "drops", new HashMap())); ++ PurpurConfig.config.set("world-settings.default.tools.axe.waxables.minecraft:waxed_oxidized_copper_door", Map.of("into", "minecraft:oxidized_copper_door", "drops", new HashMap())); ++ PurpurConfig.config.set("world-settings.default.tools.axe.waxables.minecraft:waxed_copper_trapdoor", Map.of("into", "minecraft:copper_trapdoor", "drops", new HashMap())); ++ PurpurConfig.config.set("world-settings.default.tools.axe.waxables.minecraft:waxed_exposed_copper_trapdoor", Map.of("into", "minecraft:exposed_copper_trapdoor", "drops", new HashMap())); ++ PurpurConfig.config.set("world-settings.default.tools.axe.waxables.minecraft:waxed_weathered_copper_trapdoor", Map.of("into", "minecraft:weathered_copper_trapdoor", "drops", new HashMap())); ++ PurpurConfig.config.set("world-settings.default.tools.axe.waxables.minecraft:waxed_oxidized_copper_trapdoor", Map.of("into", "minecraft:oxidized_copper_trapdoor", "drops", new HashMap())); ++ PurpurConfig.config.set("world-settings.default.tools.axe.waxables.minecraft:waxed_copper_grate", Map.of("into", "minecraft:copper_grate", "drops", new HashMap())); ++ PurpurConfig.config.set("world-settings.default.tools.axe.waxables.minecraft:waxed_exposed_copper_grate", Map.of("into", "minecraft:exposed_copper_grate", "drops", new HashMap())); ++ PurpurConfig.config.set("world-settings.default.tools.axe.waxables.minecraft:waxed_weathered_copper_grate", Map.of("into", "minecraft:weathered_copper_grate", "drops", new HashMap())); ++ PurpurConfig.config.set("world-settings.default.tools.axe.waxables.minecraft:waxed_oxidized_copper_grate", Map.of("into", "minecraft:oxidized_copper_grate", "drops", new HashMap())); ++ PurpurConfig.config.set("world-settings.default.tools.axe.waxables.minecraft:waxed_copper_bulb", Map.of("into", "minecraft:copper_bulb", "drops", new HashMap())); ++ PurpurConfig.config.set("world-settings.default.tools.axe.waxables.minecraft:waxed_exposed_copper_bulb", Map.of("into", "minecraft:exposed_copper_bulb", "drops", new HashMap())); ++ PurpurConfig.config.set("world-settings.default.tools.axe.waxables.minecraft:waxed_weathered_copper_bulb", Map.of("into", "minecraft:weathered_copper_bulb", "drops", new HashMap())); ++ PurpurConfig.config.set("world-settings.default.tools.axe.waxables.minecraft:waxed_oxidized_copper_bulb", Map.of("into", "minecraft:oxidized_copper_bulb", "drops", new HashMap())); ++ ++ PurpurConfig.config.set("world-settings.default.tools.axe.weatherables.minecraft:exposed_chiseled_copper", Map.of("into", "minecraft:chiseled_copper", "drops", new HashMap())); ++ PurpurConfig.config.set("world-settings.default.tools.axe.weatherables.minecraft:weathered_chiseled_copper", Map.of("into", "minecraft:exposed_chiseled_copper", "drops", new HashMap())); ++ PurpurConfig.config.set("world-settings.default.tools.axe.weatherables.minecraft:oxidized_chiseled_copper", Map.of("into", "minecraft:weathered_chiseled_copper", "drops", new HashMap())); ++ PurpurConfig.config.set("world-settings.default.tools.axe.weatherables.minecraft:oxidized_cut_copper_stairs", Map.of("into", "minecraft:weathered_cut_copper_stairs", "drops", new HashMap())); ++ PurpurConfig.config.set("world-settings.default.tools.axe.weatherables.minecraft:exposed_copper_door", Map.of("into", "minecraft:copper_door", "drops", new HashMap())); ++ PurpurConfig.config.set("world-settings.default.tools.axe.weatherables.minecraft:weathered_copper_door", Map.of("into", "minecraft:exposed_copper_door", "drops", new HashMap())); ++ PurpurConfig.config.set("world-settings.default.tools.axe.weatherables.minecraft:oxidized_copper_door", Map.of("into", "minecraft:weathered_copper_door", "drops", new HashMap())); ++ PurpurConfig.config.set("world-settings.default.tools.axe.weatherables.minecraft:exposed_copper_trapdoor", Map.of("into", "minecraft:copper_trapdoor", "drops", new HashMap())); ++ PurpurConfig.config.set("world-settings.default.tools.axe.weatherables.minecraft:weathered_copper_trapdoor", Map.of("into", "minecraft:exposed_copper_trapdoor", "drops", new HashMap())); ++ PurpurConfig.config.set("world-settings.default.tools.axe.weatherables.minecraft:oxidized_copper_trapdoor", Map.of("into", "minecraft:weathered_copper_trapdoor", "drops", new HashMap())); ++ PurpurConfig.config.set("world-settings.default.tools.axe.weatherables.minecraft:exposed_copper_grate", Map.of("into", "minecraft:copper_grate", "drops", new HashMap())); ++ PurpurConfig.config.set("world-settings.default.tools.axe.weatherables.minecraft:weathered_copper_grate", Map.of("into", "minecraft:exposed_copper_grate", "drops", new HashMap())); ++ PurpurConfig.config.set("world-settings.default.tools.axe.weatherables.minecraft:oxidized_copper_grate", Map.of("into", "minecraft:weathered_copper_grate", "drops", new HashMap())); ++ PurpurConfig.config.set("world-settings.default.tools.axe.weatherables.minecraft:exposed_copper_bulb", Map.of("into", "minecraft:copper_bulb", "drops", new HashMap())); ++ PurpurConfig.config.set("world-settings.default.tools.axe.weatherables.minecraft:weathered_copper_bulb", Map.of("into", "minecraft:exposed_copper_bulb", "drops", new HashMap())); ++ PurpurConfig.config.set("world-settings.default.tools.axe.weatherables.minecraft:oxidized_copper_bulb", Map.of("into", "minecraft:weathered_copper_bulb", "drops", new HashMap())); ++ } ++ getMap("tools.axe.strippables", Map.ofEntries( ++ Map.entry("minecraft:oak_wood", Map.of("into", "minecraft:stripped_oak_wood", "drops", new HashMap())), ++ Map.entry("minecraft:oak_log", Map.of("into", "minecraft:stripped_oak_log", "drops", new HashMap())), ++ Map.entry("minecraft:dark_oak_wood", Map.of("into", "minecraft:stripped_dark_oak_wood", "drops", new HashMap())), ++ Map.entry("minecraft:dark_oak_log", Map.of("into", "minecraft:stripped_dark_oak_log", "drops", new HashMap())), ++ Map.entry("minecraft:acacia_wood", Map.of("into", "minecraft:stripped_acacia_wood", "drops", new HashMap())), ++ Map.entry("minecraft:acacia_log", Map.of("into", "minecraft:stripped_acacia_log", "drops", new HashMap())), ++ Map.entry("minecraft:birch_wood", Map.of("into", "minecraft:stripped_birch_wood", "drops", new HashMap())), ++ Map.entry("minecraft:birch_log", Map.of("into", "minecraft:stripped_birch_log", "drops", new HashMap())), ++ Map.entry("minecraft:jungle_wood", Map.of("into", "minecraft:stripped_jungle_wood", "drops", new HashMap())), ++ Map.entry("minecraft:jungle_log", Map.of("into", "minecraft:stripped_jungle_log", "drops", new HashMap())), ++ Map.entry("minecraft:spruce_wood", Map.of("into", "minecraft:stripped_spruce_wood", "drops", new HashMap())), ++ Map.entry("minecraft:spruce_log", Map.of("into", "minecraft:stripped_spruce_log", "drops", new HashMap())), ++ Map.entry("minecraft:mangrove_wood", Map.of("into", "minecraft:stripped_mangrove_wood", "drops", new HashMap())), ++ Map.entry("minecraft:mangrove_log", Map.of("into", "minecraft:stripped_mangrove_log", "drops", new HashMap())), ++ Map.entry("minecraft:cherry_log", Map.of("into", "minecraft:stripped_cherry_log", "drops", new HashMap())), ++ Map.entry("minecraft:cherry_wood", Map.of("into", "minecraft:stripped_cherry_wood", "drops", new HashMap())), ++ Map.entry("minecraft:bamboo_block", Map.of("into", "minecraft:stripped_bamboo_block", "drops", new HashMap())), ++ Map.entry("minecraft:warped_stem", Map.of("into", "minecraft:stripped_warped_stem", "drops", new HashMap())), ++ Map.entry("minecraft:warped_hyphae", Map.of("into", "minecraft:stripped_warped_hyphae", "drops", new HashMap())), ++ Map.entry("minecraft:crimson_stem", Map.of("into", "minecraft:stripped_crimson_stem", "drops", new HashMap())), ++ Map.entry("minecraft:crimson_hyphae", Map.of("into", "minecraft:stripped_crimson_hyphae", "drops", new HashMap()))) ++ ).forEach((blockId, obj) -> { ++ Block block = BuiltInRegistries.BLOCK.get(new ResourceLocation(blockId)); ++ if (block == Blocks.AIR) { PurpurConfig.log(Level.SEVERE, "Invalid block for `tools.axe.strippables`: " + blockId); return; } ++ if (!(obj instanceof Map map)) { PurpurConfig.log(Level.SEVERE, "Invalid yaml for `tools.axe.strippables." + blockId + "`"); return; } ++ String intoId = (String) map.get("into"); ++ Block into = BuiltInRegistries.BLOCK.get(new ResourceLocation(intoId)); ++ if (into == Blocks.AIR) { PurpurConfig.log(Level.SEVERE, "Invalid block for `tools.axe.strippables." + blockId + ".into`: " + intoId); return; } ++ Object dropsObj = map.get("drops"); ++ if (!(dropsObj instanceof Map dropsMap)) { PurpurConfig.log(Level.SEVERE, "Invalid yaml for `tools.axe.strippables." + blockId + ".drops`"); return; } ++ Map drops = new HashMap<>(); ++ dropsMap.forEach((itemId, chance) -> { ++ Item item = BuiltInRegistries.ITEM.get(new ResourceLocation(itemId.toString())); ++ if (item == Items.AIR) { PurpurConfig.log(Level.SEVERE, "Invalid item for `tools.axe.strippables." + blockId + ".drops`: " + itemId); return; } ++ drops.put(item, (double) chance); ++ }); ++ axeStrippables.put(block, new Strippable(into, drops)); ++ }); ++ getMap("tools.axe.waxables", Map.ofEntries( ++ Map.entry("minecraft:waxed_copper_block", Map.of("into", "minecraft:copper_block", "drops", new HashMap())), ++ Map.entry("minecraft:waxed_exposed_copper", Map.of("into", "minecraft:exposed_copper", "drops", new HashMap())), ++ Map.entry("minecraft:waxed_weathered_copper", Map.of("into", "minecraft:weathered_copper", "drops", new HashMap())), ++ Map.entry("minecraft:waxed_oxidized_copper", Map.of("into", "minecraft:oxidized_copper", "drops", new HashMap())), ++ Map.entry("minecraft:waxed_cut_copper", Map.of("into", "minecraft:cut_copper", "drops", new HashMap())), ++ Map.entry("minecraft:waxed_exposed_cut_copper", Map.of("into", "minecraft:exposed_cut_copper", "drops", new HashMap())), ++ Map.entry("minecraft:waxed_weathered_cut_copper", Map.of("into", "minecraft:weathered_cut_copper", "drops", new HashMap())), ++ Map.entry("minecraft:waxed_oxidized_cut_copper", Map.of("into", "minecraft:oxidized_cut_copper", "drops", new HashMap())), ++ Map.entry("minecraft:waxed_cut_copper_slab", Map.of("into", "minecraft:cut_copper_slab", "drops", new HashMap())), ++ Map.entry("minecraft:waxed_exposed_cut_copper_slab", Map.of("into", "minecraft:exposed_cut_copper_slab", "drops", new HashMap())), ++ Map.entry("minecraft:waxed_weathered_cut_copper_slab", Map.of("into", "minecraft:weathered_cut_copper_slab", "drops", new HashMap())), ++ Map.entry("minecraft:waxed_oxidized_cut_copper_slab", Map.of("into", "minecraft:oxidized_cut_copper_slab", "drops", new HashMap())), ++ Map.entry("minecraft:waxed_cut_copper_stairs", Map.of("into", "minecraft:cut_copper_stairs", "drops", new HashMap())), ++ Map.entry("minecraft:waxed_exposed_cut_copper_stairs", Map.of("into", "minecraft:exposed_cut_copper_stairs", "drops", new HashMap())), ++ Map.entry("minecraft:waxed_weathered_cut_copper_stairs", Map.of("into", "minecraft:weathered_cut_copper_stairs", "drops", new HashMap())), ++ Map.entry("minecraft:waxed_oxidized_cut_copper_stairs", Map.of("into", "minecraft:oxidized_cut_copper_stairs", "drops", new HashMap())), ++ Map.entry("minecraft:waxed_chiseled_copper", Map.of("into", "minecraft:chiseled_copper", "drops", new HashMap())), ++ Map.entry("minecraft:waxed_exposed_chiseled_copper", Map.of("into", "minecraft:exposed_chiseled_copper", "drops", new HashMap())), ++ Map.entry("minecraft:waxed_weathered_chiseled_copper", Map.of("into", "minecraft:weathered_chiseled_copper", "drops", new HashMap())), ++ Map.entry("minecraft:waxed_oxidized_chiseled_copper", Map.of("into", "minecraft:oxidized_chiseled_copper", "drops", new HashMap())), ++ Map.entry("minecraft:waxed_copper_door", Map.of("into", "minecraft:copper_door", "drops", new HashMap())), ++ Map.entry("minecraft:waxed_exposed_copper_door", Map.of("into", "minecraft:exposed_copper_door", "drops", new HashMap())), ++ Map.entry("minecraft:waxed_weathered_copper_door", Map.of("into", "minecraft:weathered_copper_door", "drops", new HashMap())), ++ Map.entry("minecraft:waxed_oxidized_copper_door", Map.of("into", "minecraft:oxidized_copper_door", "drops", new HashMap())), ++ Map.entry("minecraft:waxed_copper_trapdoor", Map.of("into", "minecraft:copper_trapdoor", "drops", new HashMap())), ++ Map.entry("minecraft:waxed_exposed_copper_trapdoor", Map.of("into", "minecraft:exposed_copper_trapdoor", "drops", new HashMap())), ++ Map.entry("minecraft:waxed_weathered_copper_trapdoor", Map.of("into", "minecraft:weathered_copper_trapdoor", "drops", new HashMap())), ++ Map.entry("minecraft:waxed_oxidized_copper_trapdoor", Map.of("into", "minecraft:oxidized_copper_trapdoor", "drops", new HashMap())), ++ Map.entry("minecraft:waxed_copper_grate", Map.of("into", "minecraft:copper_grate", "drops", new HashMap())), ++ Map.entry("minecraft:waxed_exposed_copper_grate", Map.of("into", "minecraft:exposed_copper_grate", "drops", new HashMap())), ++ Map.entry("minecraft:waxed_weathered_copper_grate", Map.of("into", "minecraft:weathered_copper_grate", "drops", new HashMap())), ++ Map.entry("minecraft:waxed_oxidized_copper_grate", Map.of("into", "minecraft:oxidized_copper_grate", "drops", new HashMap())), ++ Map.entry("minecraft:waxed_copper_bulb", Map.of("into", "minecraft:copper_bulb", "drops", new HashMap())), ++ Map.entry("minecraft:waxed_exposed_copper_bulb", Map.of("into", "minecraft:exposed_copper_bulb", "drops", new HashMap())), ++ Map.entry("minecraft:waxed_weathered_copper_bulb", Map.of("into", "minecraft:weathered_copper_bulb", "drops", new HashMap())), ++ Map.entry("minecraft:waxed_oxidized_copper_bulb", Map.of("into", "minecraft:oxidized_copper_bulb", "drops", new HashMap()))) ++ ).forEach((blockId, obj) -> { ++ Block block = BuiltInRegistries.BLOCK.get(new ResourceLocation(blockId)); ++ if (block == Blocks.AIR) { PurpurConfig.log(Level.SEVERE, "Invalid block for `tools.axe.waxables`: " + blockId); return; } ++ if (!(obj instanceof Map map)) { PurpurConfig.log(Level.SEVERE, "Invalid yaml for `tools.axe.waxables." + blockId + "`"); return; } ++ String intoId = (String) map.get("into"); ++ Block into = BuiltInRegistries.BLOCK.get(new ResourceLocation(intoId)); ++ if (into == Blocks.AIR) { PurpurConfig.log(Level.SEVERE, "Invalid block for `tools.axe.waxables." + blockId + ".into`: " + intoId); return; } ++ Object dropsObj = map.get("drops"); ++ if (!(dropsObj instanceof Map dropsMap)) { PurpurConfig.log(Level.SEVERE, "Invalid yaml for `tools.axe.waxables." + blockId + ".drops`"); return; } ++ Map drops = new HashMap<>(); ++ dropsMap.forEach((itemId, chance) -> { ++ Item item = BuiltInRegistries.ITEM.get(new ResourceLocation(itemId.toString())); ++ if (item == Items.AIR) { PurpurConfig.log(Level.SEVERE, "Invalid item for `tools.axe.waxables." + blockId + ".drops`: " + itemId); return; } ++ drops.put(item, (double) chance); ++ }); ++ axeWaxables.put(block, new Waxable(into, drops)); ++ }); ++ getMap("tools.axe.weatherables", Map.ofEntries( ++ Map.entry("minecraft:exposed_copper", Map.of("into", "minecraft:copper_block", "drops", new HashMap())), ++ Map.entry("minecraft:weathered_copper", Map.of("into", "minecraft:exposed_copper", "drops", new HashMap())), ++ Map.entry("minecraft:oxidized_copper", Map.of("into", "minecraft:weathered_copper", "drops", new HashMap())), ++ Map.entry("minecraft:exposed_cut_copper", Map.of("into", "minecraft:cut_copper", "drops", new HashMap())), ++ Map.entry("minecraft:weathered_cut_copper", Map.of("into", "minecraft:exposed_cut_copper", "drops", new HashMap())), ++ Map.entry("minecraft:oxidized_cut_copper", Map.of("into", "minecraft:weathered_cut_copper", "drops", new HashMap())), ++ Map.entry("minecraft:exposed_chiseled_copper", Map.of("into", "minecraft:chiseled_copper", "drops", new HashMap())), ++ Map.entry("minecraft:weathered_chiseled_copper", Map.of("into", "minecraft:exposed_chiseled_copper", "drops", new HashMap())), ++ Map.entry("minecraft:oxidized_chiseled_copper", Map.of("into", "minecraft:weathered_chiseled_copper", "drops", new HashMap())), ++ Map.entry("minecraft:exposed_cut_copper_slab", Map.of("into", "minecraft:cut_copper_slab", "drops", new HashMap())), ++ Map.entry("minecraft:weathered_cut_copper_slab", Map.of("into", "minecraft:exposed_cut_copper_slab", "drops", new HashMap())), ++ Map.entry("minecraft:oxidized_cut_copper_slab", Map.of("into", "minecraft:weathered_cut_copper_slab", "drops", new HashMap())), ++ Map.entry("minecraft:exposed_cut_copper_stairs", Map.of("into", "minecraft:cut_copper_stairs", "drops", new HashMap())), ++ Map.entry("minecraft:weathered_cut_copper_stairs", Map.of("into", "minecraft:exposed_cut_copper_stairs", "drops", new HashMap())), ++ Map.entry("minecraft:oxidized_cut_copper_stairs", Map.of("into", "minecraft:weathered_cut_copper_stairs", "drops", new HashMap())), ++ Map.entry("minecraft:exposed_copper_door", Map.of("into", "minecraft:copper_door", "drops", new HashMap())), ++ Map.entry("minecraft:weathered_copper_door", Map.of("into", "minecraft:exposed_copper_door", "drops", new HashMap())), ++ Map.entry("minecraft:oxidized_copper_door", Map.of("into", "minecraft:weathered_copper_door", "drops", new HashMap())), ++ Map.entry("minecraft:exposed_copper_trapdoor", Map.of("into", "minecraft:copper_trapdoor", "drops", new HashMap())), ++ Map.entry("minecraft:weathered_copper_trapdoor", Map.of("into", "minecraft:exposed_copper_trapdoor", "drops", new HashMap())), ++ Map.entry("minecraft:oxidized_copper_trapdoor", Map.of("into", "minecraft:weathered_copper_trapdoor", "drops", new HashMap())), ++ Map.entry("minecraft:exposed_copper_grate", Map.of("into", "minecraft:copper_grate", "drops", new HashMap())), ++ Map.entry("minecraft:weathered_copper_grate", Map.of("into", "minecraft:exposed_copper_grate", "drops", new HashMap())), ++ Map.entry("minecraft:oxidized_copper_grate", Map.of("into", "minecraft:weathered_copper_grate", "drops", new HashMap())), ++ Map.entry("minecraft:exposed_copper_bulb", Map.of("into", "minecraft:copper_bulb", "drops", new HashMap())), ++ Map.entry("minecraft:weathered_copper_bulb", Map.of("into", "minecraft:exposed_copper_bulb", "drops", new HashMap())), ++ Map.entry("minecraft:oxidized_copper_bulb", Map.of("into", "minecraft:weathered_copper_bulb", "drops", new HashMap()))) ++ ).forEach((blockId, obj) -> { ++ Block block = BuiltInRegistries.BLOCK.get(new ResourceLocation(blockId)); ++ if (block == Blocks.AIR) { PurpurConfig.log(Level.SEVERE, "Invalid block for `tools.axe.weatherables`: " + blockId); return; } ++ if (!(obj instanceof Map map)) { PurpurConfig.log(Level.SEVERE, "Invalid yaml for `tools.axe.weatherables." + blockId + "`"); return; } ++ String intoId = (String) map.get("into"); ++ Block into = BuiltInRegistries.BLOCK.get(new ResourceLocation(intoId)); ++ if (into == Blocks.AIR) { PurpurConfig.log(Level.SEVERE, "Invalid block for `tools.axe.weatherables." + blockId + ".into`: " + intoId); return; } ++ Object dropsObj = map.get("drops"); ++ if (!(dropsObj instanceof Map dropsMap)) { PurpurConfig.log(Level.SEVERE, "Invalid yaml for `tools.axe.weatherables." + blockId + ".drops`"); return; } ++ Map drops = new HashMap<>(); ++ dropsMap.forEach((itemId, chance) -> { ++ Item item = BuiltInRegistries.ITEM.get(new ResourceLocation(itemId.toString())); ++ if (item == Items.AIR) { PurpurConfig.log(Level.SEVERE, "Invalid item for `tools.axe.weatherables." + blockId + ".drops`: " + itemId); return; } ++ drops.put(item, (double) chance); ++ }); ++ axeWeatherables.put(block, new Weatherable(into, drops)); ++ }); ++ getMap("tools.hoe.tillables", Map.ofEntries( ++ Map.entry("minecraft:grass_block", Map.of("condition", "air_above", "into", "minecraft:farmland", "drops", new HashMap())), ++ Map.entry("minecraft:dirt_path", Map.of("condition", "air_above", "into", "minecraft:farmland", "drops", new HashMap())), ++ Map.entry("minecraft:dirt", Map.of("condition", "air_above", "into", "minecraft:farmland", "drops", new HashMap())), ++ Map.entry("minecraft:coarse_dirt", Map.of("condition", "air_above", "into", "minecraft:dirt", "drops", new HashMap())), ++ Map.entry("minecraft:rooted_dirt", Map.of("condition", "always", "into", "minecraft:dirt", "drops", Map.of("minecraft:hanging_roots", 1.0D)))) ++ ).forEach((blockId, obj) -> { ++ Block block = BuiltInRegistries.BLOCK.get(new ResourceLocation(blockId)); ++ if (block == Blocks.AIR) { PurpurConfig.log(Level.SEVERE, "Invalid block for `tools.hoe.tillables`: " + blockId); return; } ++ if (!(obj instanceof Map map)) { PurpurConfig.log(Level.SEVERE, "Invalid yaml for `tools.hoe.tillables." + blockId + "`"); return; } ++ String conditionId = (String) map.get("condition"); ++ Tillable.Condition condition = Tillable.Condition.get(conditionId); ++ if (condition == null) { PurpurConfig.log(Level.SEVERE, "Invalid condition for `tools.hoe.tillables." + blockId + ".condition`: " + conditionId); return; } ++ String intoId = (String) map.get("into"); ++ Block into = BuiltInRegistries.BLOCK.get(new ResourceLocation(intoId)); ++ if (into == Blocks.AIR) { PurpurConfig.log(Level.SEVERE, "Invalid block for `tools.hoe.tillables." + blockId + ".into`: " + intoId); return; } ++ Object dropsObj = map.get("drops"); ++ if (!(dropsObj instanceof Map dropsMap)) { PurpurConfig.log(Level.SEVERE, "Invalid yaml for `tools.hoe.tillables." + blockId + ".drops`"); return; } ++ Map drops = new HashMap<>(); ++ dropsMap.forEach((itemId, chance) -> { ++ Item item = BuiltInRegistries.ITEM.get(new ResourceLocation(itemId.toString())); ++ if (item == Items.AIR) { PurpurConfig.log(Level.SEVERE, "Invalid item for `tools.hoe.tillables." + blockId + ".drops`: " + itemId); return; } ++ drops.put(item, (double) chance); ++ }); ++ hoeTillables.put(block, new Tillable(condition, into, drops)); ++ }); ++ getMap("tools.shovel.flattenables", Map.ofEntries( ++ Map.entry("minecraft:grass_block", Map.of("into", "minecraft:dirt_path", "drops", new HashMap())), ++ Map.entry("minecraft:dirt", Map.of("into", "minecraft:dirt_path", "drops", new HashMap())), ++ Map.entry("minecraft:podzol", Map.of("into", "minecraft:dirt_path", "drops", new HashMap())), ++ Map.entry("minecraft:coarse_dirt", Map.of("into", "minecraft:dirt_path", "drops", new HashMap())), ++ Map.entry("minecraft:mycelium", Map.of("into", "minecraft:dirt_path", "drops", new HashMap())), ++ Map.entry("minecraft:rooted_dirt", Map.of("into", "minecraft:dirt_path", "drops", new HashMap()))) ++ ).forEach((blockId, obj) -> { ++ Block block = BuiltInRegistries.BLOCK.get(new ResourceLocation(blockId)); ++ if (block == Blocks.AIR) { PurpurConfig.log(Level.SEVERE, "Invalid block for `tools.shovel.flattenables`: " + blockId); return; } ++ if (!(obj instanceof Map map)) { PurpurConfig.log(Level.SEVERE, "Invalid yaml for `tools.shovel.flattenables." + blockId + "`"); return; } ++ String intoId = (String) map.get("into"); ++ Block into = BuiltInRegistries.BLOCK.get(new ResourceLocation(intoId)); ++ if (into == Blocks.AIR) { PurpurConfig.log(Level.SEVERE, "Invalid block for `tools.shovel.flattenables." + blockId + ".into`: " + intoId); return; } ++ Object dropsObj = map.get("drops"); ++ if (!(dropsObj instanceof Map dropsMap)) { PurpurConfig.log(Level.SEVERE, "Invalid yaml for `tools.shovel.flattenables." + blockId + ".drops`"); return; } ++ Map drops = new HashMap<>(); ++ dropsMap.forEach((itemId, chance) -> { ++ Item item = BuiltInRegistries.ITEM.get(new ResourceLocation(itemId.toString())); ++ if (item == Items.AIR) { PurpurConfig.log(Level.SEVERE, "Invalid item for `tools.shovel.flattenables." + blockId + ".drops`: " + itemId); return; } ++ drops.put(item, (double) chance); ++ }); ++ shovelFlattenables.put(block, new Flattenable(into, drops)); ++ }); ++ hoeReplantsCrops = getBoolean("tools.hoe.replant-crops", hoeReplantsCrops); ++ hoeReplantsNetherWarts = getBoolean("tools.hoe.replant-nether-warts", hoeReplantsNetherWarts); ++ } ++ ++ public boolean anvilAllowColors = false; ++ public boolean anvilColorsUseMiniMessage; ++ public int anvilRepairIngotsAmount = 0; ++ public int anvilDamageObsidianAmount = 0; ++ private void anvilSettings() { ++ anvilAllowColors = getBoolean("blocks.anvil.allow-colors", anvilAllowColors); ++ anvilColorsUseMiniMessage = getBoolean("blocks.anvil.use-mini-message", anvilColorsUseMiniMessage); ++ anvilRepairIngotsAmount = getInt("blocks.anvil.iron-ingots-used-for-repair", anvilRepairIngotsAmount); ++ anvilDamageObsidianAmount = getInt("blocks.anvil.obsidian-used-for-damage", anvilDamageObsidianAmount); ++ } ++ ++ public double azaleaGrowthChance = 0.0D; ++ private void azaleaSettings() { ++ azaleaGrowthChance = getDouble("blocks.azalea.growth-chance", azaleaGrowthChance); ++ } ++ ++ public int beaconLevelOne = 20; ++ public int beaconLevelTwo = 30; ++ public int beaconLevelThree = 40; ++ public int beaconLevelFour = 50; ++ public boolean beaconAllowEffectsWithTintedGlass = false; ++ private void beaconSettings() { ++ beaconLevelOne = getInt("blocks.beacon.effect-range.level-1", beaconLevelOne); ++ beaconLevelTwo = getInt("blocks.beacon.effect-range.level-2", beaconLevelTwo); ++ beaconLevelThree = getInt("blocks.beacon.effect-range.level-3", beaconLevelThree); ++ beaconLevelFour = getInt("blocks.beacon.effect-range.level-4", beaconLevelFour); ++ beaconAllowEffectsWithTintedGlass = getBoolean("blocks.beacon.allow-effects-with-tinted-glass", beaconAllowEffectsWithTintedGlass); ++ } ++ ++ public boolean bedExplode = true; ++ public boolean bedExplodeOnVillagerSleep = false; ++ public double bedExplosionPower = 5.0D; ++ public boolean bedExplosionFire = true; ++ public net.minecraft.world.level.Level.ExplosionInteraction bedExplosionEffect = net.minecraft.world.level.Level.ExplosionInteraction.BLOCK; ++ private void bedSettings() { ++ if (PurpurConfig.version < 31) { ++ if ("DESTROY".equals(getString("blocks.bed.explosion-effect", bedExplosionEffect.name()))) { ++ set("blocks.bed.explosion-effect", "BLOCK"); ++ } ++ } ++ bedExplode = getBoolean("blocks.bed.explode", bedExplode); ++ bedExplodeOnVillagerSleep = getBoolean("blocks.bed.explode-on-villager-sleep", bedExplodeOnVillagerSleep); ++ bedExplosionPower = getDouble("blocks.bed.explosion-power", bedExplosionPower); ++ bedExplosionFire = getBoolean("blocks.bed.explosion-fire", bedExplosionFire); ++ try { ++ bedExplosionEffect = net.minecraft.world.level.Level.ExplosionInteraction.valueOf(getString("blocks.bed.explosion-effect", bedExplosionEffect.name())); ++ } catch (IllegalArgumentException e) { ++ log(Level.SEVERE, "Unknown value for `blocks.bed.explosion-effect`! Using default of `BLOCK`"); ++ bedExplosionEffect = net.minecraft.world.level.Level.ExplosionInteraction.BLOCK; ++ } ++ } ++ ++ public Map bigDripleafTiltDelay = new HashMap<>(); ++ private void bigDripleafSettings() { ++ bigDripleafTiltDelay.clear(); ++ getMap("blocks.big_dripleaf.tilt-delay", Map.ofEntries( ++ Map.entry("UNSTABLE", 10), ++ Map.entry("PARTIAL", 10), ++ Map.entry("FULL", 100)) ++ ).forEach((tilt, delay) -> { ++ try { ++ bigDripleafTiltDelay.put(Tilt.valueOf(tilt), (int) delay); ++ } catch (IllegalArgumentException e) { ++ PurpurConfig.log(Level.SEVERE, "Invalid big_dripleaf tilt key: " + tilt); ++ } ++ }); ++ } ++ ++ public boolean cactusBreaksFromSolidNeighbors = true; ++ public boolean cactusAffectedByBonemeal = false; ++ private void cactusSettings() { ++ cactusBreaksFromSolidNeighbors = getBoolean("blocks.cactus.breaks-from-solid-neighbors", cactusBreaksFromSolidNeighbors); ++ cactusAffectedByBonemeal = getBoolean("blocks.cactus.affected-by-bonemeal", cactusAffectedByBonemeal); ++ } ++ ++ public boolean sugarCanAffectedByBonemeal = false; ++ private void sugarCaneSettings() { ++ sugarCanAffectedByBonemeal = getBoolean("blocks.sugar_cane.affected-by-bonemeal", sugarCanAffectedByBonemeal); ++ } ++ ++ public boolean netherWartAffectedByBonemeal = false; ++ private void netherWartSettings() { ++ netherWartAffectedByBonemeal = getBoolean("blocks.nether_wart.affected-by-bonemeal", netherWartAffectedByBonemeal); ++ } ++ ++ public boolean campFireLitWhenPlaced = true; ++ private void campFireSettings() { ++ campFireLitWhenPlaced = getBoolean("blocks.campfire.lit-when-placed", campFireLitWhenPlaced); ++ } ++ ++ public boolean chestOpenWithBlockOnTop = false; ++ private void chestSettings() { ++ chestOpenWithBlockOnTop = getBoolean("blocks.chest.open-with-solid-block-on-top", chestOpenWithBlockOnTop); ++ } ++ ++ public boolean composterBulkProcess = false; ++ private void composterSettings() { ++ composterBulkProcess = getBoolean("blocks.composter.sneak-to-bulk-process", composterBulkProcess); ++ } ++ ++ public boolean coralDieOutsideWater = true; ++ private void coralSettings() { ++ coralDieOutsideWater = getBoolean("blocks.coral.die-outside-water", coralDieOutsideWater); ++ } ++ ++ public boolean dispenserApplyCursedArmor = true; ++ public boolean dispenserPlaceAnvils = false; ++ private void dispenserSettings() { ++ dispenserApplyCursedArmor = getBoolean("blocks.dispenser.apply-cursed-to-armor-slots", dispenserApplyCursedArmor); ++ dispenserPlaceAnvils = getBoolean("blocks.dispenser.place-anvils", dispenserPlaceAnvils); ++ } ++ ++ public List doorRequiresRedstone = new ArrayList<>(); ++ private void doorSettings() { ++ getList("blocks.door.requires-redstone", new ArrayList()).forEach(key -> { ++ Block block = BuiltInRegistries.BLOCK.get(new ResourceLocation(key.toString())); ++ if (!block.defaultBlockState().isAir()) { ++ doorRequiresRedstone.add(block); ++ } ++ }); ++ } ++ ++ public boolean dragonEggTeleport = true; ++ private void dragonEggSettings() { ++ dragonEggTeleport = getBoolean("blocks.dragon_egg.teleport", dragonEggTeleport); ++ } ++ ++ public boolean baselessEndCrystalExplode = true; ++ public double baselessEndCrystalExplosionPower = 6.0D; ++ public boolean baselessEndCrystalExplosionFire = false; ++ public net.minecraft.world.level.Level.ExplosionInteraction baselessEndCrystalExplosionEffect = net.minecraft.world.level.Level.ExplosionInteraction.BLOCK; ++ public boolean basedEndCrystalExplode = true; ++ public double basedEndCrystalExplosionPower = 6.0D; ++ public boolean basedEndCrystalExplosionFire = false; ++ public net.minecraft.world.level.Level.ExplosionInteraction basedEndCrystalExplosionEffect = net.minecraft.world.level.Level.ExplosionInteraction.BLOCK; ++ public int endCrystalCramming = 0; ++ public boolean endCrystalPlaceAnywhere = false; ++ private void endCrystalSettings() { ++ if (PurpurConfig.version < 31) { ++ if ("DESTROY".equals(getString("blocks.end-crystal.baseless.explosion-effect", baselessEndCrystalExplosionEffect.name()))) { ++ set("blocks.end-crystal.baseless.explosion-effect", "BLOCK"); ++ } ++ if ("DESTROY".equals(getString("blocks.end-crystal.base.explosion-effect", basedEndCrystalExplosionEffect.name()))) { ++ set("blocks.end-crystal.base.explosion-effect", "BLOCK"); ++ } ++ } ++ baselessEndCrystalExplode = getBoolean("blocks.end-crystal.baseless.explode", baselessEndCrystalExplode); ++ baselessEndCrystalExplosionPower = getDouble("blocks.end-crystal.baseless.explosion-power", baselessEndCrystalExplosionPower); ++ baselessEndCrystalExplosionFire = getBoolean("blocks.end-crystal.baseless.explosion-fire", baselessEndCrystalExplosionFire); ++ try { ++ baselessEndCrystalExplosionEffect = net.minecraft.world.level.Level.ExplosionInteraction.valueOf(getString("blocks.end-crystal.baseless.explosion-effect", baselessEndCrystalExplosionEffect.name())); ++ } catch (IllegalArgumentException e) { ++ log(Level.SEVERE, "Unknown value for `blocks.end-crystal.baseless.explosion-effect`! Using default of `BLOCK`"); ++ baselessEndCrystalExplosionEffect = net.minecraft.world.level.Level.ExplosionInteraction.BLOCK; ++ } ++ basedEndCrystalExplode = getBoolean("blocks.end-crystal.base.explode", basedEndCrystalExplode); ++ basedEndCrystalExplosionPower = getDouble("blocks.end-crystal.base.explosion-power", basedEndCrystalExplosionPower); ++ basedEndCrystalExplosionFire = getBoolean("blocks.end-crystal.base.explosion-fire", basedEndCrystalExplosionFire); ++ try { ++ basedEndCrystalExplosionEffect = net.minecraft.world.level.Level.ExplosionInteraction.valueOf(getString("blocks.end-crystal.base.explosion-effect", basedEndCrystalExplosionEffect.name())); ++ } catch (IllegalArgumentException e) { ++ log(Level.SEVERE, "Unknown value for `blocks.end-crystal.base.explosion-effect`! Using default of `BLOCK`"); ++ basedEndCrystalExplosionEffect = net.minecraft.world.level.Level.ExplosionInteraction.BLOCK; ++ } ++ endCrystalCramming = getInt("blocks.end-crystal.cramming-amount", endCrystalCramming); ++ endCrystalPlaceAnywhere = getBoolean("gameplay-mechanics.item.end-crystal.place-anywhere", endCrystalPlaceAnywhere); ++ } ++ ++ public boolean farmlandBypassMobGriefing = false; ++ public boolean farmlandGetsMoistFromBelow = false; ++ public boolean farmlandAlpha = false; ++ public boolean farmlandTramplingDisabled = false; ++ public boolean farmlandTramplingOnlyPlayers = false; ++ public boolean farmlandTramplingFeatherFalling = false; ++ public double farmlandTrampleHeight = -1D; ++ private void farmlandSettings() { ++ farmlandBypassMobGriefing = getBoolean("blocks.farmland.bypass-mob-griefing", farmlandBypassMobGriefing); ++ farmlandGetsMoistFromBelow = getBoolean("blocks.farmland.gets-moist-from-below", farmlandGetsMoistFromBelow); ++ farmlandAlpha = getBoolean("blocks.farmland.use-alpha-farmland", farmlandAlpha); ++ farmlandTramplingDisabled = getBoolean("blocks.farmland.disable-trampling", farmlandTramplingDisabled); ++ farmlandTramplingOnlyPlayers = getBoolean("blocks.farmland.only-players-trample", farmlandTramplingOnlyPlayers); ++ farmlandTramplingFeatherFalling = getBoolean("blocks.farmland.feather-fall-distance-affects-trampling", farmlandTramplingFeatherFalling); ++ farmlandTrampleHeight = getDouble("blocks.farmland.trample-height", farmlandTrampleHeight); ++ } ++ ++ public double floweringAzaleaGrowthChance = 0.0D; ++ private void floweringAzaleaSettings() { ++ floweringAzaleaGrowthChance = getDouble("blocks.flowering_azalea.growth-chance", floweringAzaleaGrowthChance); ++ } ++ ++ public boolean furnaceUseLavaFromUnderneath = false; ++ private void furnaceSettings() { ++ if (PurpurConfig.version < 17) { ++ furnaceUseLavaFromUnderneath = getBoolean("blocks.furnace.infinite-fuel", furnaceUseLavaFromUnderneath); ++ boolean oldValue = getBoolean("blocks.furnace.infinite-fuel", furnaceUseLavaFromUnderneath); ++ set("blocks.furnace.infinite-fuel", null); ++ set("blocks.furnace.use-lava-from-underneath", oldValue); ++ } ++ furnaceUseLavaFromUnderneath = getBoolean("blocks.furnace.use-lava-from-underneath", furnaceUseLavaFromUnderneath); ++ } ++ ++ public boolean mobsSpawnOnPackedIce = true; ++ public boolean mobsSpawnOnBlueIce = true; ++ public boolean snowOnBlueIce = true; ++ private void iceSettings() { ++ mobsSpawnOnPackedIce = getBoolean("blocks.packed_ice.allow-mob-spawns", mobsSpawnOnPackedIce); ++ mobsSpawnOnBlueIce = getBoolean("blocks.blue_ice.allow-mob-spawns", mobsSpawnOnBlueIce); ++ snowOnBlueIce = getBoolean("blocks.blue_ice.allow-snow-formation", snowOnBlueIce); ++ } ++ ++ public int lavaInfiniteRequiredSources = 2; ++ public int lavaSpeedNether = 10; ++ public int lavaSpeedNotNether = 30; ++ private void lavaSettings() { ++ lavaInfiniteRequiredSources = getInt("blocks.lava.infinite-required-sources", lavaInfiniteRequiredSources); ++ lavaSpeedNether = getInt("blocks.lava.speed.nether", lavaSpeedNether); ++ lavaSpeedNotNether = getInt("blocks.lava.speed.not-nether", lavaSpeedNotNether); ++ } ++ ++ public int pistonBlockPushLimit = 12; ++ private void pistonSettings() { ++ pistonBlockPushLimit = getInt("blocks.piston.block-push-limit", pistonBlockPushLimit); ++ } ++ ++ public boolean magmaBlockDamageWhenSneaking = false; ++ public boolean magmaBlockDamageWithFrostWalker = false; ++ private void magmaBlockSettings() { ++ magmaBlockDamageWhenSneaking = getBoolean("blocks.magma-block.damage-when-sneaking", magmaBlockDamageWhenSneaking); ++ magmaBlockDamageWithFrostWalker = getBoolean("blocks.magma-block.damage-with-frost-walker", magmaBlockDamageWithFrostWalker); ++ } ++ ++ public boolean powderSnowBypassMobGriefing = false; ++ private void powderSnowSettings() { ++ powderSnowBypassMobGriefing = getBoolean("blocks.powder_snow.bypass-mob-griefing", powderSnowBypassMobGriefing); ++ } ++ ++ public int railActivationRange = 8; ++ private void railSettings() { ++ railActivationRange = getInt("blocks.powered-rail.activation-range", railActivationRange); ++ } ++ ++ public boolean respawnAnchorExplode = true; ++ public double respawnAnchorExplosionPower = 5.0D; ++ public boolean respawnAnchorExplosionFire = true; ++ public net.minecraft.world.level.Level.ExplosionInteraction respawnAnchorExplosionEffect = net.minecraft.world.level.Level.ExplosionInteraction.BLOCK; ++ private void respawnAnchorSettings() { ++ if (PurpurConfig.version < 31) { ++ if ("DESTROY".equals(getString("blocks.respawn_anchor.explosion-effect", respawnAnchorExplosionEffect.name()))) { ++ set("blocks.respawn_anchor.explosion-effect", "BLOCK"); ++ } ++ } ++ respawnAnchorExplode = getBoolean("blocks.respawn_anchor.explode", respawnAnchorExplode); ++ respawnAnchorExplosionPower = getDouble("blocks.respawn_anchor.explosion-power", respawnAnchorExplosionPower); ++ respawnAnchorExplosionFire = getBoolean("blocks.respawn_anchor.explosion-fire", respawnAnchorExplosionFire); ++ try { ++ respawnAnchorExplosionEffect = net.minecraft.world.level.Level.ExplosionInteraction.valueOf(getString("blocks.respawn_anchor.explosion-effect", respawnAnchorExplosionEffect.name())); ++ } catch (IllegalArgumentException e) { ++ log(Level.SEVERE, "Unknown value for `blocks.respawn_anchor.explosion-effect`! Using default of `BLOCK`"); ++ respawnAnchorExplosionEffect = net.minecraft.world.level.Level.ExplosionInteraction.BLOCK; ++ } ++ } ++ ++ public boolean sculkShriekerCanSummonDefault = false; ++ private void sculkShriekerSettings() { ++ sculkShriekerCanSummonDefault = getBoolean("blocks.sculk_shrieker.can-summon-default", sculkShriekerCanSummonDefault); ++ } ++ ++ public boolean slabHalfBreak = false; ++ private void slabSettings() { ++ slabHalfBreak = getBoolean("blocks.slab.break-individual-slabs-when-sneaking", slabHalfBreak); ++ } ++ ++ public boolean spawnerDeactivateByRedstone = false; ++ public boolean spawnerFixMC238526 = false; ++ private void spawnerSettings() { ++ spawnerDeactivateByRedstone = getBoolean("blocks.spawner.deactivate-by-redstone", spawnerDeactivateByRedstone); ++ spawnerFixMC238526 = getBoolean("blocks.spawner.fix-mc-238526", spawnerFixMC238526); ++ } ++ ++ public int spongeAbsorptionArea = 65; ++ public int spongeAbsorptionRadius = 6; ++ public boolean spongeAbsorbsLava = false; ++ public boolean spongeAbsorbsWaterFromMud = false; ++ private void spongeSettings() { ++ spongeAbsorptionArea = getInt("blocks.sponge.absorption.area", spongeAbsorptionArea); ++ spongeAbsorptionRadius = getInt("blocks.sponge.absorption.radius", spongeAbsorptionRadius); ++ spongeAbsorbsLava = getBoolean("blocks.sponge.absorbs-lava", spongeAbsorbsLava); ++ spongeAbsorbsWaterFromMud = getBoolean("blocks.sponge.absorbs-water-from-mud", spongeAbsorbsWaterFromMud); ++ } ++ ++ public float stonecutterDamage = 0.0F; ++ private void stonecutterSettings() { ++ stonecutterDamage = (float) getDouble("blocks.stonecutter.damage", stonecutterDamage); ++ } ++ ++ public boolean turtleEggsBreakFromExpOrbs = false; ++ public boolean turtleEggsBreakFromItems = false; ++ public boolean turtleEggsBreakFromMinecarts = false; ++ public boolean turtleEggsBypassMobGriefing = false; ++ public int turtleEggsRandomTickCrackChance = 500; ++ public boolean turtleEggsTramplingFeatherFalling = false; ++ private void turtleEggSettings() { ++ turtleEggsBreakFromExpOrbs = getBoolean("blocks.turtle_egg.break-from-exp-orbs", turtleEggsBreakFromExpOrbs); ++ turtleEggsBreakFromItems = getBoolean("blocks.turtle_egg.break-from-items", turtleEggsBreakFromItems); ++ turtleEggsBreakFromMinecarts = getBoolean("blocks.turtle_egg.break-from-minecarts", turtleEggsBreakFromMinecarts); ++ turtleEggsBypassMobGriefing = getBoolean("blocks.turtle_egg.bypass-mob-griefing", turtleEggsBypassMobGriefing); ++ turtleEggsRandomTickCrackChance = getInt("blocks.turtle_egg.random-tick-crack-chance", turtleEggsRandomTickCrackChance); ++ turtleEggsTramplingFeatherFalling = getBoolean("blocks.turtle_egg.feather-fall-distance-affects-trampling", turtleEggsTramplingFeatherFalling); ++ } ++ ++ public int waterInfiniteRequiredSources = 2; ++ private void waterSources() { ++ waterInfiniteRequiredSources = getInt("blocks.water.infinite-required-sources", waterInfiniteRequiredSources); ++ } ++ ++ public boolean babiesAreRidable = true; ++ public boolean untamedTamablesAreRidable = true; ++ public boolean useNightVisionWhenRiding = false; ++ public boolean useDismountsUnderwaterTag = true; ++ private void ridableSettings() { ++ babiesAreRidable = getBoolean("ridable-settings.babies-are-ridable", babiesAreRidable); ++ untamedTamablesAreRidable = getBoolean("ridable-settings.untamed-tamables-are-ridable", untamedTamablesAreRidable); ++ useNightVisionWhenRiding = getBoolean("ridable-settings.use-night-vision", useNightVisionWhenRiding); ++ useDismountsUnderwaterTag = getBoolean("ridable-settings.use-dismounts-underwater-tag", useDismountsUnderwaterTag); ++ } ++ ++ public boolean allayRidable = false; ++ public boolean allayRidableInWater = true; ++ public boolean allayControllable = true; ++ private void allaySettings() { ++ allayRidable = getBoolean("mobs.allay.ridable", allayRidable); ++ allayRidableInWater = getBoolean("mobs.allay.ridable-in-water", allayRidableInWater); ++ allayControllable = getBoolean("mobs.allay.controllable", allayControllable); ++ } ++ ++ public boolean axolotlRidable = false; ++ public boolean axolotlControllable = true; ++ public double axolotlMaxHealth = 14.0D; ++ public int axolotlBreedingTicks = 6000; ++ public boolean axolotlTakeDamageFromWater = false; ++ public boolean axolotlAlwaysDropExp = false; ++ private void axolotlSettings() { ++ axolotlRidable = getBoolean("mobs.axolotl.ridable", axolotlRidable); ++ axolotlControllable = getBoolean("mobs.axolotl.controllable", axolotlControllable); ++ axolotlMaxHealth = getDouble("mobs.axolotl.attributes.max_health", axolotlMaxHealth); ++ axolotlBreedingTicks = getInt("mobs.axolotl.breeding-delay-ticks", axolotlBreedingTicks); ++ axolotlTakeDamageFromWater = getBoolean("mobs.axolotl.takes-damage-from-water", axolotlTakeDamageFromWater); ++ axolotlAlwaysDropExp = getBoolean("mobs.axolotl.always-drop-exp", axolotlAlwaysDropExp); ++ } ++ ++ public boolean batRidable = false; ++ public boolean batRidableInWater = true; ++ public boolean batControllable = true; ++ public double batMaxY = 320D; ++ public double batMaxHealth = 6.0D; ++ public double batFollowRange = 16.0D; ++ public double batKnockbackResistance = 0.0D; ++ public double batMovementSpeed = 0.6D; ++ public double batFlyingSpeed = 0.6D; ++ public double batArmor = 0.0D; ++ public double batArmorToughness = 0.0D; ++ public double batAttackKnockback = 0.0D; ++ public boolean batTakeDamageFromWater = false; ++ public boolean batAlwaysDropExp = false; ++ private void batSettings() { ++ batRidable = getBoolean("mobs.bat.ridable", batRidable); ++ batRidableInWater = getBoolean("mobs.bat.ridable-in-water", batRidableInWater); ++ batControllable = getBoolean("mobs.bat.controllable", batControllable); ++ batMaxY = getDouble("mobs.bat.ridable-max-y", batMaxY); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.bat.attributes.max-health", batMaxHealth); ++ set("mobs.bat.attributes.max-health", null); ++ set("mobs.bat.attributes.max_health", oldValue); ++ } ++ batMaxHealth = getDouble("mobs.bat.attributes.max_health", batMaxHealth); ++ batFollowRange = getDouble("mobs.bat.attributes.follow_range", batFollowRange); ++ batKnockbackResistance = getDouble("mobs.bat.attributes.knockback_resistance", batKnockbackResistance); ++ batMovementSpeed = getDouble("mobs.bat.attributes.movement_speed", batMovementSpeed); ++ batFlyingSpeed = getDouble("mobs.bat.attributes.flying_speed", batFlyingSpeed); ++ batArmor = getDouble("mobs.bat.attributes.armor", batArmor); ++ batArmorToughness = getDouble("mobs.bat.attributes.armor_toughness", batArmorToughness); ++ batAttackKnockback = getDouble("mobs.bat.attributes.attack_knockback", batAttackKnockback); ++ batTakeDamageFromWater = getBoolean("mobs.bat.takes-damage-from-water", batTakeDamageFromWater); ++ batAlwaysDropExp = getBoolean("mobs.bat.always-drop-exp", batAlwaysDropExp); ++ } ++ ++ public boolean beeRidable = false; ++ public boolean beeRidableInWater = true; ++ public boolean beeControllable = true; ++ public double beeMaxY = 320D; ++ public double beeMaxHealth = 10.0D; ++ public int beeBreedingTicks = 6000; ++ public boolean beeTakeDamageFromWater = false; ++ public boolean beeCanWorkAtNight = false; ++ public boolean beeCanWorkInRain = false; ++ public boolean beeAlwaysDropExp = false; ++ public boolean beeDiesAfterSting = true; ++ private void beeSettings() { ++ beeRidable = getBoolean("mobs.bee.ridable", beeRidable); ++ beeRidableInWater = getBoolean("mobs.bee.ridable-in-water", beeRidableInWater); ++ beeControllable = getBoolean("mobs.bee.controllable", beeControllable); ++ beeMaxY = getDouble("mobs.bee.ridable-max-y", beeMaxY); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.bee.attributes.max-health", beeMaxHealth); ++ set("mobs.bee.attributes.max-health", null); ++ set("mobs.bee.attributes.max_health", oldValue); ++ } ++ beeMaxHealth = getDouble("mobs.bee.attributes.max_health", beeMaxHealth); ++ beeBreedingTicks = getInt("mobs.bee.breeding-delay-ticks", beeBreedingTicks); ++ beeTakeDamageFromWater = getBoolean("mobs.bee.takes-damage-from-water", beeTakeDamageFromWater); ++ beeCanWorkAtNight = getBoolean("mobs.bee.can-work-at-night", beeCanWorkAtNight); ++ beeCanWorkInRain = getBoolean("mobs.bee.can-work-in-rain", beeCanWorkInRain); ++ beeAlwaysDropExp = getBoolean("mobs.bee.always-drop-exp", beeAlwaysDropExp); ++ beeDiesAfterSting = getBoolean("mobs.bee.dies-after-sting", beeDiesAfterSting); ++ } ++ ++ public boolean blazeRidable = false; ++ public boolean blazeRidableInWater = true; ++ public boolean blazeControllable = true; ++ public double blazeMaxY = 320D; ++ public double blazeMaxHealth = 20.0D; ++ public boolean blazeTakeDamageFromWater = true; ++ public boolean blazeAlwaysDropExp = false; ++ private void blazeSettings() { ++ blazeRidable = getBoolean("mobs.blaze.ridable", blazeRidable); ++ blazeRidableInWater = getBoolean("mobs.blaze.ridable-in-water", blazeRidableInWater); ++ blazeControllable = getBoolean("mobs.blaze.controllable", blazeControllable); ++ blazeMaxY = getDouble("mobs.blaze.ridable-max-y", blazeMaxY); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.blaze.attributes.max-health", blazeMaxHealth); ++ set("mobs.blaze.attributes.max-health", null); ++ set("mobs.blaze.attributes.max_health", oldValue); ++ } ++ blazeMaxHealth = getDouble("mobs.blaze.attributes.max_health", blazeMaxHealth); ++ blazeTakeDamageFromWater = getBoolean("mobs.blaze.takes-damage-from-water", blazeTakeDamageFromWater); ++ blazeAlwaysDropExp = getBoolean("mobs.blaze.always-drop-exp", blazeAlwaysDropExp); ++ } ++ ++ public boolean camelRidableInWater = false; ++ public double camelMaxHealthMin = 32.0D; ++ public double camelMaxHealthMax = 32.0D; ++ public double camelJumpStrengthMin = 0.42D; ++ public double camelJumpStrengthMax = 0.42D; ++ public double camelMovementSpeedMin = 0.09D; ++ public double camelMovementSpeedMax = 0.09D; ++ public int camelBreedingTicks = 6000; ++ private void camelSettings() { ++ camelRidableInWater = getBoolean("mobs.camel.ridable-in-water", camelRidableInWater); ++ camelMaxHealthMin = getDouble("mobs.camel.attributes.max_health.min", camelMaxHealthMin); ++ camelMaxHealthMax = getDouble("mobs.camel.attributes.max_health.max", camelMaxHealthMax); ++ camelJumpStrengthMin = getDouble("mobs.camel.attributes.jump_strength.min", camelJumpStrengthMin); ++ camelJumpStrengthMax = getDouble("mobs.camel.attributes.jump_strength.max", camelJumpStrengthMax); ++ camelMovementSpeedMin = getDouble("mobs.camel.attributes.movement_speed.min", camelMovementSpeedMin); ++ camelMovementSpeedMax = getDouble("mobs.camel.attributes.movement_speed.max", camelMovementSpeedMax); ++ camelBreedingTicks = getInt("mobs.camel.breeding-delay-ticks", camelBreedingTicks); ++ } ++ ++ public boolean catRidable = false; ++ public boolean catRidableInWater = true; ++ public boolean catControllable = true; ++ public double catMaxHealth = 10.0D; ++ public int catSpawnDelay = 1200; ++ public int catSpawnSwampHutScanRange = 16; ++ public int catSpawnVillageScanRange = 48; ++ public int catBreedingTicks = 6000; ++ public DyeColor catDefaultCollarColor = DyeColor.RED; ++ public boolean catTakeDamageFromWater = false; ++ public boolean catAlwaysDropExp = false; ++ private void catSettings() { ++ catRidable = getBoolean("mobs.cat.ridable", catRidable); ++ catRidableInWater = getBoolean("mobs.cat.ridable-in-water", catRidableInWater); ++ catControllable = getBoolean("mobs.cat.controllable", catControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.cat.attributes.max-health", catMaxHealth); ++ set("mobs.cat.attributes.max-health", null); ++ set("mobs.cat.attributes.max_health", oldValue); ++ } ++ catMaxHealth = getDouble("mobs.cat.attributes.max_health", catMaxHealth); ++ catSpawnDelay = getInt("mobs.cat.spawn-delay", catSpawnDelay); ++ catSpawnSwampHutScanRange = getInt("mobs.cat.scan-range-for-other-cats.swamp-hut", catSpawnSwampHutScanRange); ++ catSpawnVillageScanRange = getInt("mobs.cat.scan-range-for-other-cats.village", catSpawnVillageScanRange); ++ catBreedingTicks = getInt("mobs.cat.breeding-delay-ticks", catBreedingTicks); ++ try { ++ catDefaultCollarColor = DyeColor.valueOf(getString("mobs.cat.default-collar-color", catDefaultCollarColor.name())); ++ } catch (IllegalArgumentException ignore) { ++ catDefaultCollarColor = DyeColor.RED; ++ } ++ catTakeDamageFromWater = getBoolean("mobs.cat.takes-damage-from-water", catTakeDamageFromWater); ++ catAlwaysDropExp = getBoolean("mobs.cat.always-drop-exp", catAlwaysDropExp); ++ } ++ ++ public boolean caveSpiderRidable = false; ++ public boolean caveSpiderRidableInWater = true; ++ public boolean caveSpiderControllable = true; ++ public double caveSpiderMaxHealth = 12.0D; ++ public boolean caveSpiderTakeDamageFromWater = false; ++ public boolean caveSpiderAlwaysDropExp = false; ++ private void caveSpiderSettings() { ++ caveSpiderRidable = getBoolean("mobs.cave_spider.ridable", caveSpiderRidable); ++ caveSpiderRidableInWater = getBoolean("mobs.cave_spider.ridable-in-water", caveSpiderRidableInWater); ++ caveSpiderControllable = getBoolean("mobs.cave_spider.controllable", caveSpiderControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.cave_spider.attributes.max-health", caveSpiderMaxHealth); ++ set("mobs.cave_spider.attributes.max-health", null); ++ set("mobs.cave_spider.attributes.max_health", oldValue); ++ } ++ caveSpiderMaxHealth = getDouble("mobs.cave_spider.attributes.max_health", caveSpiderMaxHealth); ++ caveSpiderTakeDamageFromWater = getBoolean("mobs.cave_spider.takes-damage-from-water", caveSpiderTakeDamageFromWater); ++ caveSpiderAlwaysDropExp = getBoolean("mobs.cave_spider.always-drop-exp", caveSpiderAlwaysDropExp); ++ } ++ ++ public boolean chickenRidable = false; ++ public boolean chickenRidableInWater = false; ++ public boolean chickenControllable = true; ++ public double chickenMaxHealth = 4.0D; ++ public boolean chickenRetaliate = false; ++ public int chickenBreedingTicks = 6000; ++ public boolean chickenTakeDamageFromWater = false; ++ public boolean chickenAlwaysDropExp = false; ++ private void chickenSettings() { ++ chickenRidable = getBoolean("mobs.chicken.ridable", chickenRidable); ++ chickenRidableInWater = getBoolean("mobs.chicken.ridable-in-water", chickenRidableInWater); ++ chickenControllable = getBoolean("mobs.chicken.controllable", chickenControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.chicken.attributes.max-health", chickenMaxHealth); ++ set("mobs.chicken.attributes.max-health", null); ++ set("mobs.chicken.attributes.max_health", oldValue); ++ } ++ chickenMaxHealth = getDouble("mobs.chicken.attributes.max_health", chickenMaxHealth); ++ chickenRetaliate = getBoolean("mobs.chicken.retaliate", chickenRetaliate); ++ chickenBreedingTicks = getInt("mobs.chicken.breeding-delay-ticks", chickenBreedingTicks); ++ chickenTakeDamageFromWater = getBoolean("mobs.chicken.takes-damage-from-water", chickenTakeDamageFromWater); ++ chickenAlwaysDropExp = getBoolean("mobs.chicken.always-drop-exp", chickenAlwaysDropExp); ++ } ++ ++ public boolean codRidable = false; ++ public boolean codControllable = true; ++ public double codMaxHealth = 3.0D; ++ public boolean codTakeDamageFromWater = false; ++ public boolean codAlwaysDropExp = false; ++ private void codSettings() { ++ codRidable = getBoolean("mobs.cod.ridable", codRidable); ++ codControllable = getBoolean("mobs.cod.controllable", codControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.cod.attributes.max-health", codMaxHealth); ++ set("mobs.cod.attributes.max-health", null); ++ set("mobs.cod.attributes.max_health", oldValue); ++ } ++ codMaxHealth = getDouble("mobs.cod.attributes.max_health", codMaxHealth); ++ codTakeDamageFromWater = getBoolean("mobs.cod.takes-damage-from-water", codTakeDamageFromWater); ++ codAlwaysDropExp = getBoolean("mobs.cod.always-drop-exp", codAlwaysDropExp); ++ } ++ ++ public boolean cowRidable = false; ++ public boolean cowRidableInWater = true; ++ public boolean cowControllable = true; ++ public double cowMaxHealth = 10.0D; ++ public int cowFeedMushrooms = 0; ++ public int cowBreedingTicks = 6000; ++ public boolean cowTakeDamageFromWater = false; ++ public double cowNaturallyAggressiveToPlayersChance = 0.0D; ++ public double cowNaturallyAggressiveToPlayersDamage = 2.0D; ++ public boolean cowAlwaysDropExp = false; ++ private void cowSettings() { ++ if (PurpurConfig.version < 22) { ++ double oldValue = getDouble("mobs.cow.naturally-aggressive-to-players-chance", cowNaturallyAggressiveToPlayersChance); ++ set("mobs.cow.naturally-aggressive-to-players-chance", null); ++ set("mobs.cow.naturally-aggressive-to-players.chance", oldValue); ++ } ++ cowRidable = getBoolean("mobs.cow.ridable", cowRidable); ++ cowRidableInWater = getBoolean("mobs.cow.ridable-in-water", cowRidableInWater); ++ cowControllable = getBoolean("mobs.cow.controllable", cowControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.cow.attributes.max-health", cowMaxHealth); ++ set("mobs.cow.attributes.max-health", null); ++ set("mobs.cow.attributes.max_health", oldValue); ++ } ++ cowMaxHealth = getDouble("mobs.cow.attributes.max_health", cowMaxHealth); ++ cowFeedMushrooms = getInt("mobs.cow.feed-mushrooms-for-mooshroom", cowFeedMushrooms); ++ cowBreedingTicks = getInt("mobs.cow.breeding-delay-ticks", cowBreedingTicks); ++ cowTakeDamageFromWater = getBoolean("mobs.cow.takes-damage-from-water", cowTakeDamageFromWater); ++ cowNaturallyAggressiveToPlayersChance = getDouble("mobs.cow.naturally-aggressive-to-players.chance", cowNaturallyAggressiveToPlayersChance); ++ cowNaturallyAggressiveToPlayersDamage = getDouble("mobs.cow.naturally-aggressive-to-players.damage", cowNaturallyAggressiveToPlayersDamage); ++ cowAlwaysDropExp = getBoolean("mobs.cow.always-drop-exp", cowAlwaysDropExp); ++ } ++ ++ public boolean creeperRidable = false; ++ public boolean creeperRidableInWater = true; ++ public boolean creeperControllable = true; ++ public double creeperMaxHealth = 20.0D; ++ public double creeperChargedChance = 0.0D; ++ public boolean creeperAllowGriefing = true; ++ public boolean creeperBypassMobGriefing = false; ++ public boolean creeperTakeDamageFromWater = false; ++ public boolean creeperExplodeWhenKilled = false; ++ public boolean creeperHealthRadius = false; ++ public boolean creeperAlwaysDropExp = false; ++ public double creeperHeadVisibilityPercent = 0.5D; ++ public boolean creeperEncircleTarget = false; ++ private void creeperSettings() { ++ creeperRidable = getBoolean("mobs.creeper.ridable", creeperRidable); ++ creeperRidableInWater = getBoolean("mobs.creeper.ridable-in-water", creeperRidableInWater); ++ creeperControllable = getBoolean("mobs.creeper.controllable", creeperControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.creeper.attributes.max-health", creeperMaxHealth); ++ set("mobs.creeper.attributes.max-health", null); ++ set("mobs.creeper.attributes.max_health", oldValue); ++ } ++ creeperMaxHealth = getDouble("mobs.creeper.attributes.max_health", creeperMaxHealth); ++ creeperChargedChance = getDouble("mobs.creeper.naturally-charged-chance", creeperChargedChance); ++ creeperAllowGriefing = getBoolean("mobs.creeper.allow-griefing", creeperAllowGriefing); ++ creeperBypassMobGriefing = getBoolean("mobs.creeper.bypass-mob-griefing", creeperBypassMobGriefing); ++ creeperTakeDamageFromWater = getBoolean("mobs.creeper.takes-damage-from-water", creeperTakeDamageFromWater); ++ creeperExplodeWhenKilled = getBoolean("mobs.creeper.explode-when-killed", creeperExplodeWhenKilled); ++ creeperHealthRadius = getBoolean("mobs.creeper.health-impacts-explosion", creeperHealthRadius); ++ creeperAlwaysDropExp = getBoolean("mobs.creeper.always-drop-exp", creeperAlwaysDropExp); ++ creeperHeadVisibilityPercent = getDouble("mobs.creeper.head-visibility-percent", creeperHeadVisibilityPercent); ++ creeperEncircleTarget = getBoolean("mobs.creeper.encircle-target", creeperEncircleTarget); ++ } ++ ++ public boolean dolphinRidable = false; ++ public boolean dolphinControllable = true; ++ public int dolphinSpitCooldown = 20; ++ public float dolphinSpitSpeed = 1.0F; ++ public float dolphinSpitDamage = 2.0F; ++ public double dolphinMaxHealth = 10.0D; ++ public boolean dolphinDisableTreasureSearching = false; ++ public boolean dolphinTakeDamageFromWater = false; ++ public double dolphinNaturallyAggressiveToPlayersChance = 0.0D; ++ public boolean dolphinAlwaysDropExp = false; ++ private void dolphinSettings() { ++ dolphinRidable = getBoolean("mobs.dolphin.ridable", dolphinRidable); ++ dolphinControllable = getBoolean("mobs.dolphin.controllable", dolphinControllable); ++ dolphinSpitCooldown = getInt("mobs.dolphin.spit.cooldown", dolphinSpitCooldown); ++ dolphinSpitSpeed = (float) getDouble("mobs.dolphin.spit.speed", dolphinSpitSpeed); ++ dolphinSpitDamage = (float) getDouble("mobs.dolphin.spit.damage", dolphinSpitDamage); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.dolphin.attributes.max-health", dolphinMaxHealth); ++ set("mobs.dolphin.attributes.max-health", null); ++ set("mobs.dolphin.attributes.max_health", oldValue); ++ } ++ dolphinMaxHealth = getDouble("mobs.dolphin.attributes.max_health", dolphinMaxHealth); ++ dolphinDisableTreasureSearching = getBoolean("mobs.dolphin.disable-treasure-searching", dolphinDisableTreasureSearching); ++ dolphinTakeDamageFromWater = getBoolean("mobs.dolphin.takes-damage-from-water", dolphinTakeDamageFromWater); ++ dolphinNaturallyAggressiveToPlayersChance = getDouble("mobs.dolphin.naturally-aggressive-to-players-chance", dolphinNaturallyAggressiveToPlayersChance); ++ dolphinAlwaysDropExp = getBoolean("mobs.dolphin.always-drop-exp", dolphinAlwaysDropExp); ++ } ++ ++ public boolean donkeyRidableInWater = false; ++ public double donkeyMaxHealthMin = 15.0D; ++ public double donkeyMaxHealthMax = 30.0D; ++ public double donkeyJumpStrengthMin = 0.5D; ++ public double donkeyJumpStrengthMax = 0.5D; ++ public double donkeyMovementSpeedMin = 0.175D; ++ public double donkeyMovementSpeedMax = 0.175D; ++ public int donkeyBreedingTicks = 6000; ++ public boolean donkeyTakeDamageFromWater = false; ++ public boolean donkeyAlwaysDropExp = false; ++ private void donkeySettings() { ++ donkeyRidableInWater = getBoolean("mobs.donkey.ridable-in-water", donkeyRidableInWater); ++ if (PurpurConfig.version < 10) { ++ double oldMin = getDouble("mobs.donkey.attributes.max-health.min", donkeyMaxHealthMin); ++ double oldMax = getDouble("mobs.donkey.attributes.max-health.max", donkeyMaxHealthMax); ++ set("mobs.donkey.attributes.max-health", null); ++ set("mobs.donkey.attributes.max_health.min", oldMin); ++ set("mobs.donkey.attributes.max_health.max", oldMax); ++ } ++ donkeyMaxHealthMin = getDouble("mobs.donkey.attributes.max_health.min", donkeyMaxHealthMin); ++ donkeyMaxHealthMax = getDouble("mobs.donkey.attributes.max_health.max", donkeyMaxHealthMax); ++ donkeyJumpStrengthMin = getDouble("mobs.donkey.attributes.jump_strength.min", donkeyJumpStrengthMin); ++ donkeyJumpStrengthMax = getDouble("mobs.donkey.attributes.jump_strength.max", donkeyJumpStrengthMax); ++ donkeyMovementSpeedMin = getDouble("mobs.donkey.attributes.movement_speed.min", donkeyMovementSpeedMin); ++ donkeyMovementSpeedMax = getDouble("mobs.donkey.attributes.movement_speed.max", donkeyMovementSpeedMax); ++ donkeyBreedingTicks = getInt("mobs.donkey.breeding-delay-ticks", donkeyBreedingTicks); ++ donkeyTakeDamageFromWater = getBoolean("mobs.donkey.takes-damage-from-water", donkeyTakeDamageFromWater); ++ donkeyAlwaysDropExp = getBoolean("mobs.donkey.always-drop-exp", donkeyAlwaysDropExp); ++ } ++ ++ public boolean drownedRidable = false; ++ public boolean drownedRidableInWater = true; ++ public boolean drownedControllable = true; ++ public double drownedMaxHealth = 20.0D; ++ public double drownedSpawnReinforcements = 0.1D; ++ public boolean drownedJockeyOnlyBaby = true; ++ public double drownedJockeyChance = 0.05D; ++ public boolean drownedJockeyTryExistingChickens = true; ++ public boolean drownedTakeDamageFromWater = false; ++ public boolean drownedBreakDoors = false; ++ public boolean drownedAlwaysDropExp = false; ++ private void drownedSettings() { ++ drownedRidable = getBoolean("mobs.drowned.ridable", drownedRidable); ++ drownedRidableInWater = getBoolean("mobs.drowned.ridable-in-water", drownedRidableInWater); ++ drownedControllable = getBoolean("mobs.drowned.controllable", drownedControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.drowned.attributes.max-health", drownedMaxHealth); ++ set("mobs.drowned.attributes.max-health", null); ++ set("mobs.drowned.attributes.max_health", oldValue); ++ } ++ drownedMaxHealth = getDouble("mobs.drowned.attributes.max_health", drownedMaxHealth); ++ drownedSpawnReinforcements = getDouble("mobs.drowned.attributes.spawn_reinforcements", drownedSpawnReinforcements); ++ drownedJockeyOnlyBaby = getBoolean("mobs.drowned.jockey.only-babies", drownedJockeyOnlyBaby); ++ drownedJockeyChance = getDouble("mobs.drowned.jockey.chance", drownedJockeyChance); ++ drownedJockeyTryExistingChickens = getBoolean("mobs.drowned.jockey.try-existing-chickens", drownedJockeyTryExistingChickens); ++ drownedTakeDamageFromWater = getBoolean("mobs.drowned.takes-damage-from-water", drownedTakeDamageFromWater); ++ drownedBreakDoors = getBoolean("mobs.drowned.can-break-doors", drownedBreakDoors); ++ drownedAlwaysDropExp = getBoolean("mobs.drowned.always-drop-exp", drownedAlwaysDropExp); ++ } ++ ++ public boolean elderGuardianRidable = false; ++ public boolean elderGuardianControllable = true; ++ public double elderGuardianMaxHealth = 80.0D; ++ public boolean elderGuardianTakeDamageFromWater = false; ++ public boolean elderGuardianAlwaysDropExp = false; ++ private void elderGuardianSettings() { ++ elderGuardianRidable = getBoolean("mobs.elder_guardian.ridable", elderGuardianRidable); ++ elderGuardianControllable = getBoolean("mobs.elder_guardian.controllable", elderGuardianControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.elder_guardian.attributes.max-health", elderGuardianMaxHealth); ++ set("mobs.elder_guardian.attributes.max-health", null); ++ set("mobs.elder_guardian.attributes.max_health", oldValue); ++ } ++ elderGuardianMaxHealth = getDouble("mobs.elder_guardian.attributes.max_health", elderGuardianMaxHealth); ++ elderGuardianTakeDamageFromWater = getBoolean("mobs.elder_guardian.takes-damage-from-water", elderGuardianTakeDamageFromWater); ++ elderGuardianAlwaysDropExp = getBoolean("mobs.elder_guardian.always-drop-exp", elderGuardianAlwaysDropExp); ++ } ++ ++ public boolean enchantmentTableLapisPersists = false; ++ private void enchantmentTableSettings() { ++ enchantmentTableLapisPersists = getBoolean("blocks.enchantment-table.lapis-persists", enchantmentTableLapisPersists); ++ } ++ ++ public boolean enderDragonRidable = false; ++ public boolean enderDragonRidableInWater = true; ++ public boolean enderDragonControllable = true; ++ public double enderDragonMaxY = 320D; ++ public double enderDragonMaxHealth = 200.0D; ++ public boolean enderDragonAlwaysDropsFullExp = false; ++ public boolean enderDragonBypassMobGriefing = false; ++ public boolean enderDragonTakeDamageFromWater = false; ++ public boolean enderDragonCanRideVehicles = false; ++ private void enderDragonSettings() { ++ enderDragonRidable = getBoolean("mobs.ender_dragon.ridable", enderDragonRidable); ++ enderDragonRidableInWater = getBoolean("mobs.ender_dragon.ridable-in-water", enderDragonRidableInWater); ++ enderDragonControllable = getBoolean("mobs.ender_dragon.controllable", enderDragonControllable); ++ enderDragonMaxY = getDouble("mobs.ender_dragon.ridable-max-y", enderDragonMaxY); ++ if (PurpurConfig.version < 8) { ++ double oldValue = getDouble("mobs.ender_dragon.max-health", enderDragonMaxHealth); ++ set("mobs.ender_dragon.max-health", null); ++ set("mobs.ender_dragon.attributes.max_health", oldValue); ++ } else if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.ender_dragon.attributes.max-health", enderDragonMaxHealth); ++ set("mobs.ender_dragon.attributes.max-health", null); ++ set("mobs.ender_dragon.attributes.max_health", oldValue); ++ } ++ enderDragonMaxHealth = getDouble("mobs.ender_dragon.attributes.max_health", enderDragonMaxHealth); ++ enderDragonAlwaysDropsFullExp = getBoolean("mobs.ender_dragon.always-drop-full-exp", enderDragonAlwaysDropsFullExp); ++ enderDragonBypassMobGriefing = getBoolean("mobs.ender_dragon.bypass-mob-griefing", enderDragonBypassMobGriefing); ++ enderDragonTakeDamageFromWater = getBoolean("mobs.ender_dragon.takes-damage-from-water", enderDragonTakeDamageFromWater); ++ enderDragonCanRideVehicles = getBoolean("mobs.ender_dragon.can-ride-vehicles", enderDragonCanRideVehicles); ++ } ++ ++ public boolean endermanRidable = false; ++ public boolean endermanRidableInWater = true; ++ public boolean endermanControllable = true; ++ public double endermanMaxHealth = 40.0D; ++ public boolean endermanAllowGriefing = true; ++ public boolean endermanDespawnEvenWithBlock = false; ++ public boolean endermanBypassMobGriefing = false; ++ public boolean endermanTakeDamageFromWater = true; ++ public boolean endermanAggroEndermites = true; ++ public boolean endermanAggroEndermitesOnlyIfPlayerSpawned = false; ++ public boolean endermanIgnorePlayerDragonHead = false; ++ public boolean endermanDisableStareAggro = false; ++ public boolean endermanIgnoreProjectiles = false; ++ public boolean endermanAlwaysDropExp = false; ++ private void endermanSettings() { ++ endermanRidable = getBoolean("mobs.enderman.ridable", endermanRidable); ++ endermanRidableInWater = getBoolean("mobs.enderman.ridable-in-water", endermanRidableInWater); ++ endermanControllable = getBoolean("mobs.enderman.controllable", endermanControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.enderman.attributes.max-health", endermanMaxHealth); ++ set("mobs.enderman.attributes.max-health", null); ++ set("mobs.enderman.attributes.max_health", oldValue); ++ } ++ if (PurpurConfig.version < 15) { ++ // remove old option ++ set("mobs.enderman.aggressive-towards-spawned-endermites", null); ++ } ++ endermanMaxHealth = getDouble("mobs.enderman.attributes.max_health", endermanMaxHealth); ++ endermanAllowGriefing = getBoolean("mobs.enderman.allow-griefing", endermanAllowGriefing); ++ endermanDespawnEvenWithBlock = getBoolean("mobs.enderman.can-despawn-with-held-block", endermanDespawnEvenWithBlock); ++ endermanBypassMobGriefing = getBoolean("mobs.enderman.bypass-mob-griefing", endermanBypassMobGriefing); ++ endermanTakeDamageFromWater = getBoolean("mobs.enderman.takes-damage-from-water", endermanTakeDamageFromWater); ++ endermanAggroEndermites = getBoolean("mobs.enderman.aggressive-towards-endermites", endermanAggroEndermites); ++ endermanAggroEndermitesOnlyIfPlayerSpawned = getBoolean("mobs.enderman.aggressive-towards-endermites-only-spawned-by-player-thrown-ender-pearls", endermanAggroEndermitesOnlyIfPlayerSpawned); ++ endermanIgnorePlayerDragonHead = getBoolean("mobs.enderman.ignore-players-wearing-dragon-head", endermanIgnorePlayerDragonHead); ++ endermanDisableStareAggro = getBoolean("mobs.enderman.disable-player-stare-aggression", endermanDisableStareAggro); ++ endermanIgnoreProjectiles = getBoolean("mobs.enderman.ignore-projectiles", endermanIgnoreProjectiles); ++ endermanAlwaysDropExp = getBoolean("mobs.enderman.always-drop-exp", endermanAlwaysDropExp); ++ } ++ ++ public boolean endermiteRidable = false; ++ public boolean endermiteRidableInWater = true; ++ public boolean endermiteControllable = true; ++ public double endermiteMaxHealth = 8.0D; ++ public boolean endermiteTakeDamageFromWater = false; ++ public boolean endermiteAlwaysDropExp = false; ++ private void endermiteSettings() { ++ endermiteRidable = getBoolean("mobs.endermite.ridable", endermiteRidable); ++ endermiteRidableInWater = getBoolean("mobs.endermite.ridable-in-water", endermiteRidableInWater); ++ endermiteControllable = getBoolean("mobs.endermite.controllable", endermiteControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.endermite.attributes.max-health", endermiteMaxHealth); ++ set("mobs.endermite.attributes.max-health", null); ++ set("mobs.endermite.attributes.max_health", oldValue); ++ } ++ endermiteMaxHealth = getDouble("mobs.endermite.attributes.max_health", endermiteMaxHealth); ++ endermiteTakeDamageFromWater = getBoolean("mobs.endermite.takes-damage-from-water", endermiteTakeDamageFromWater); ++ endermiteAlwaysDropExp = getBoolean("mobs.endermite.always-drop-exp", endermiteAlwaysDropExp); ++ } ++ ++ public boolean evokerRidable = false; ++ public boolean evokerRidableInWater = true; ++ public boolean evokerControllable = true; ++ public double evokerMaxHealth = 24.0D; ++ public boolean evokerBypassMobGriefing = false; ++ public boolean evokerTakeDamageFromWater = false; ++ public boolean evokerAlwaysDropExp = false; ++ private void evokerSettings() { ++ evokerRidable = getBoolean("mobs.evoker.ridable", evokerRidable); ++ evokerRidableInWater = getBoolean("mobs.evoker.ridable-in-water", evokerRidableInWater); ++ evokerControllable = getBoolean("mobs.evoker.controllable", evokerControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.evoker.attributes.max-health", evokerMaxHealth); ++ set("mobs.evoker.attributes.max-health", null); ++ set("mobs.evoker.attributes.max_health", oldValue); ++ } ++ evokerMaxHealth = getDouble("mobs.evoker.attributes.max_health", evokerMaxHealth); ++ evokerBypassMobGriefing = getBoolean("mobs.evoker.bypass-mob-griefing", evokerBypassMobGriefing); ++ evokerTakeDamageFromWater = getBoolean("mobs.evoker.takes-damage-from-water", evokerTakeDamageFromWater); ++ evokerAlwaysDropExp = getBoolean("mobs.evoker.always-drop-exp", evokerAlwaysDropExp); ++ } ++ ++ public boolean foxRidable = false; ++ public boolean foxRidableInWater = true; ++ public boolean foxControllable = true; ++ public double foxMaxHealth = 10.0D; ++ public boolean foxTypeChangesWithTulips = false; ++ public int foxBreedingTicks = 6000; ++ public boolean foxBypassMobGriefing = false; ++ public boolean foxTakeDamageFromWater = false; ++ public boolean foxAlwaysDropExp = false; ++ private void foxSettings() { ++ foxRidable = getBoolean("mobs.fox.ridable", foxRidable); ++ foxRidableInWater = getBoolean("mobs.fox.ridable-in-water", foxRidableInWater); ++ foxControllable = getBoolean("mobs.fox.controllable", foxControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.fox.attributes.max-health", foxMaxHealth); ++ set("mobs.fox.attributes.max-health", null); ++ set("mobs.fox.attributes.max_health", oldValue); ++ } ++ foxMaxHealth = getDouble("mobs.fox.attributes.max_health", foxMaxHealth); ++ foxTypeChangesWithTulips = getBoolean("mobs.fox.tulips-change-type", foxTypeChangesWithTulips); ++ foxBreedingTicks = getInt("mobs.fox.breeding-delay-ticks", foxBreedingTicks); ++ foxBypassMobGriefing = getBoolean("mobs.fox.bypass-mob-griefing", foxBypassMobGriefing); ++ foxTakeDamageFromWater = getBoolean("mobs.fox.takes-damage-from-water", foxTakeDamageFromWater); ++ foxAlwaysDropExp = getBoolean("mobs.fox.always-drop-exp", foxAlwaysDropExp); ++ } ++ ++ public boolean frogRidable = false; ++ public boolean frogRidableInWater = true; ++ public boolean frogControllable = true; ++ public float frogRidableJumpHeight = 0.65F; ++ public int frogBreedingTicks = 6000; ++ private void frogSettings() { ++ frogRidable = getBoolean("mobs.frog.ridable", frogRidable); ++ frogRidableInWater = getBoolean("mobs.frog.ridable-in-water", frogRidableInWater); ++ frogControllable = getBoolean("mobs.frog.controllable", frogControllable); ++ frogRidableJumpHeight = (float) getDouble("mobs.frog.ridable-jump-height", frogRidableJumpHeight); ++ frogBreedingTicks = getInt("mobs.frog.breeding-delay-ticks", frogBreedingTicks); ++ } ++ ++ public boolean ghastRidable = false; ++ public boolean ghastRidableInWater = true; ++ public boolean ghastControllable = true; ++ public double ghastMaxY = 320D; ++ public double ghastMaxHealth = 10.0D; ++ public boolean ghastTakeDamageFromWater = false; ++ public boolean ghastAlwaysDropExp = false; ++ private void ghastSettings() { ++ ghastRidable = getBoolean("mobs.ghast.ridable", ghastRidable); ++ ghastRidableInWater = getBoolean("mobs.ghast.ridable-in-water", ghastRidableInWater); ++ ghastControllable = getBoolean("mobs.ghast.controllable", ghastControllable); ++ ghastMaxY = getDouble("mobs.ghast.ridable-max-y", ghastMaxY); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.ghast.attributes.max-health", ghastMaxHealth); ++ set("mobs.ghast.attributes.max-health", null); ++ set("mobs.ghast.attributes.max_health", oldValue); ++ } ++ ghastMaxHealth = getDouble("mobs.ghast.attributes.max_health", ghastMaxHealth); ++ ghastTakeDamageFromWater = getBoolean("mobs.ghast.takes-damage-from-water", ghastTakeDamageFromWater); ++ ghastAlwaysDropExp = getBoolean("mobs.ghast.always-drop-exp", ghastAlwaysDropExp); ++ } ++ ++ public boolean giantRidable = false; ++ public boolean giantRidableInWater = true; ++ public boolean giantControllable = true; ++ public double giantMovementSpeed = 0.5D; ++ public double giantAttackDamage = 50.0D; ++ public double giantMaxHealth = 100.0D; ++ public float giantStepHeight = 2.0F; ++ public float giantJumpHeight = 1.0F; ++ public boolean giantHaveAI = false; ++ public boolean giantHaveHostileAI = false; ++ public boolean giantTakeDamageFromWater = false; ++ public boolean giantAlwaysDropExp = false; ++ private void giantSettings() { ++ giantRidable = getBoolean("mobs.giant.ridable", giantRidable); ++ giantRidableInWater = getBoolean("mobs.giant.ridable-in-water", giantRidableInWater); ++ giantControllable = getBoolean("mobs.giant.controllable", giantControllable); ++ giantMovementSpeed = getDouble("mobs.giant.movement-speed", giantMovementSpeed); ++ giantAttackDamage = getDouble("mobs.giant.attack-damage", giantAttackDamage); ++ if (PurpurConfig.version < 8) { ++ double oldValue = getDouble("mobs.giant.max-health", giantMaxHealth); ++ set("mobs.giant.max-health", null); ++ set("mobs.giant.attributes.max_health", oldValue); ++ } else if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.giant.attributes.max-health", giantMaxHealth); ++ set("mobs.giant.attributes.max-health", null); ++ set("mobs.giant.attributes.max_health", oldValue); ++ } ++ giantMaxHealth = getDouble("mobs.giant.attributes.max_health", giantMaxHealth); ++ giantStepHeight = (float) getDouble("mobs.giant.step-height", giantStepHeight); ++ giantJumpHeight = (float) getDouble("mobs.giant.jump-height", giantJumpHeight); ++ giantHaveAI = getBoolean("mobs.giant.have-ai", giantHaveAI); ++ giantHaveHostileAI = getBoolean("mobs.giant.have-hostile-ai", giantHaveHostileAI); ++ giantTakeDamageFromWater = getBoolean("mobs.giant.takes-damage-from-water", giantTakeDamageFromWater); ++ giantAlwaysDropExp = getBoolean("mobs.giant.always-drop-exp", giantAlwaysDropExp); ++ } ++ ++ public boolean glowSquidRidable = false; ++ public boolean glowSquidControllable = true; ++ public double glowSquidMaxHealth = 10.0D; ++ public boolean glowSquidsCanFly = false; ++ public boolean glowSquidTakeDamageFromWater = false; ++ public boolean glowSquidAlwaysDropExp = false; ++ private void glowSquidSettings() { ++ glowSquidRidable = getBoolean("mobs.glow_squid.ridable", glowSquidRidable); ++ glowSquidControllable = getBoolean("mobs.glow_squid.controllable", glowSquidControllable); ++ glowSquidMaxHealth = getDouble("mobs.glow_squid.attributes.max_health", glowSquidMaxHealth); ++ glowSquidsCanFly = getBoolean("mobs.glow_squid.can-fly", glowSquidsCanFly); ++ glowSquidTakeDamageFromWater = getBoolean("mobs.glow_squid.takes-damage-from-water", glowSquidTakeDamageFromWater); ++ glowSquidAlwaysDropExp = getBoolean("mobs.glow_squid.always-drop-exp", glowSquidAlwaysDropExp); ++ } ++ ++ public boolean goatRidable = false; ++ public boolean goatRidableInWater = true; ++ public boolean goatControllable = true; ++ public double goatMaxHealth = 10.0D; ++ public int goatBreedingTicks = 6000; ++ public boolean goatTakeDamageFromWater = false; ++ public boolean goatAlwaysDropExp = false; ++ private void goatSettings() { ++ goatRidable = getBoolean("mobs.goat.ridable", goatRidable); ++ goatRidableInWater = getBoolean("mobs.goat.ridable-in-water", goatRidableInWater); ++ goatControllable = getBoolean("mobs.goat.controllable", goatControllable); ++ goatMaxHealth = getDouble("mobs.goat.attributes.max_health", goatMaxHealth); ++ goatBreedingTicks = getInt("mobs.goat.breeding-delay-ticks", goatBreedingTicks); ++ goatTakeDamageFromWater = getBoolean("mobs.goat.takes-damage-from-water", goatTakeDamageFromWater); ++ goatAlwaysDropExp = getBoolean("mobs.goat.always-drop-exp", goatAlwaysDropExp); ++ } ++ ++ public boolean guardianRidable = false; ++ public boolean guardianControllable = true; ++ public double guardianMaxHealth = 30.0D; ++ public boolean guardianTakeDamageFromWater = false; ++ public boolean guardianAlwaysDropExp = false; ++ private void guardianSettings() { ++ guardianRidable = getBoolean("mobs.guardian.ridable", guardianRidable); ++ guardianControllable = getBoolean("mobs.guardian.controllable", guardianControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.guardian.attributes.max-health", guardianMaxHealth); ++ set("mobs.guardian.attributes.max-health", null); ++ set("mobs.guardian.attributes.max_health", oldValue); ++ } ++ guardianMaxHealth = getDouble("mobs.guardian.attributes.max_health", guardianMaxHealth); ++ guardianTakeDamageFromWater = getBoolean("mobs.guardian.takes-damage-from-water", guardianTakeDamageFromWater); ++ guardianAlwaysDropExp = getBoolean("mobs.guardian.always-drop-exp", guardianAlwaysDropExp); ++ } ++ ++ public boolean forceHalloweenSeason = false; ++ public float chanceHeadHalloweenOnEntity = 0.25F; ++ private void halloweenSetting() { ++ forceHalloweenSeason = getBoolean("gameplay-mechanics.halloween.force", forceHalloweenSeason); ++ chanceHeadHalloweenOnEntity = (float) getDouble("gameplay-mechanics.halloween.head-chance", chanceHeadHalloweenOnEntity); ++ } ++ ++ public boolean hoglinRidable = false; ++ public boolean hoglinRidableInWater = true; ++ public boolean hoglinControllable = true; ++ public double hoglinMaxHealth = 40.0D; ++ public int hoglinBreedingTicks = 6000; ++ public boolean hoglinTakeDamageFromWater = false; ++ public boolean hoglinAlwaysDropExp = false; ++ private void hoglinSettings() { ++ hoglinRidable = getBoolean("mobs.hoglin.ridable", hoglinRidable); ++ hoglinRidableInWater = getBoolean("mobs.hoglin.ridable-in-water", hoglinRidableInWater); ++ hoglinControllable = getBoolean("mobs.hoglin.controllable", hoglinControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.hoglin.attributes.max-health", hoglinMaxHealth); ++ set("mobs.hoglin.attributes.max-health", null); ++ set("mobs.hoglin.attributes.max_health", oldValue); ++ } ++ hoglinMaxHealth = getDouble("mobs.hoglin.attributes.max_health", hoglinMaxHealth); ++ hoglinBreedingTicks = getInt("mobs.hoglin.breeding-delay-ticks", hoglinBreedingTicks); ++ hoglinTakeDamageFromWater = getBoolean("mobs.hoglin.takes-damage-from-water", hoglinTakeDamageFromWater); ++ hoglinAlwaysDropExp = getBoolean("mobs.hoglin.always-drop-exp", hoglinAlwaysDropExp); ++ } ++ ++ public boolean horseRidableInWater = false; ++ public double horseMaxHealthMin = 15.0D; ++ public double horseMaxHealthMax = 30.0D; ++ public double horseJumpStrengthMin = 0.4D; ++ public double horseJumpStrengthMax = 1.0D; ++ public double horseMovementSpeedMin = 0.1125D; ++ public double horseMovementSpeedMax = 0.3375D; ++ public int horseBreedingTicks = 6000; ++ public boolean horseTakeDamageFromWater = false; ++ public boolean horseAlwaysDropExp = false; ++ private void horseSettings() { ++ horseRidableInWater = getBoolean("mobs.horse.ridable-in-water", horseRidableInWater); ++ if (PurpurConfig.version < 10) { ++ double oldMin = getDouble("mobs.horse.attributes.max-health.min", horseMaxHealthMin); ++ double oldMax = getDouble("mobs.horse.attributes.max-health.max", horseMaxHealthMax); ++ set("mobs.horse.attributes.max-health", null); ++ set("mobs.horse.attributes.max_health.min", oldMin); ++ set("mobs.horse.attributes.max_health.max", oldMax); ++ } ++ horseMaxHealthMin = getDouble("mobs.horse.attributes.max_health.min", horseMaxHealthMin); ++ horseMaxHealthMax = getDouble("mobs.horse.attributes.max_health.max", horseMaxHealthMax); ++ horseJumpStrengthMin = getDouble("mobs.horse.attributes.jump_strength.min", horseJumpStrengthMin); ++ horseJumpStrengthMax = getDouble("mobs.horse.attributes.jump_strength.max", horseJumpStrengthMax); ++ horseMovementSpeedMin = getDouble("mobs.horse.attributes.movement_speed.min", horseMovementSpeedMin); ++ horseMovementSpeedMax = getDouble("mobs.horse.attributes.movement_speed.max", horseMovementSpeedMax); ++ horseBreedingTicks = getInt("mobs.horse.breeding-delay-ticks", horseBreedingTicks); ++ horseTakeDamageFromWater = getBoolean("mobs.horse.takes-damage-from-water", horseTakeDamageFromWater); ++ horseAlwaysDropExp = getBoolean("mobs.horse.always-drop-exp", horseAlwaysDropExp); ++ } ++ ++ public boolean huskRidable = false; ++ public boolean huskRidableInWater = true; ++ public boolean huskControllable = true; ++ public double huskMaxHealth = 20.0D; ++ public double huskSpawnReinforcements = 0.1D; ++ public boolean huskJockeyOnlyBaby = true; ++ public double huskJockeyChance = 0.05D; ++ public boolean huskJockeyTryExistingChickens = true; ++ public boolean huskTakeDamageFromWater = false; ++ public boolean huskAlwaysDropExp = false; ++ private void huskSettings() { ++ huskRidable = getBoolean("mobs.husk.ridable", huskRidable); ++ huskRidableInWater = getBoolean("mobs.husk.ridable-in-water", huskRidableInWater); ++ huskControllable = getBoolean("mobs.husk.controllable", huskControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.husk.attributes.max-health", huskMaxHealth); ++ set("mobs.husk.attributes.max-health", null); ++ set("mobs.husk.attributes.max_health", oldValue); ++ } ++ huskMaxHealth = getDouble("mobs.husk.attributes.max_health", huskMaxHealth); ++ huskSpawnReinforcements = getDouble("mobs.husk.attributes.spawn_reinforcements", huskSpawnReinforcements); ++ huskJockeyOnlyBaby = getBoolean("mobs.husk.jockey.only-babies", huskJockeyOnlyBaby); ++ huskJockeyChance = getDouble("mobs.husk.jockey.chance", huskJockeyChance); ++ huskJockeyTryExistingChickens = getBoolean("mobs.husk.jockey.try-existing-chickens", huskJockeyTryExistingChickens); ++ huskTakeDamageFromWater = getBoolean("mobs.husk.takes-damage-from-water", huskTakeDamageFromWater); ++ huskAlwaysDropExp = getBoolean("mobs.husk.always-drop-exp", huskAlwaysDropExp); ++ } ++ ++ public boolean illusionerRidable = false; ++ public boolean illusionerRidableInWater = true; ++ public boolean illusionerControllable = true; ++ public double illusionerMovementSpeed = 0.5D; ++ public double illusionerFollowRange = 18.0D; ++ public double illusionerMaxHealth = 32.0D; ++ public boolean illusionerTakeDamageFromWater = false; ++ public boolean illusionerAlwaysDropExp = false; ++ private void illusionerSettings() { ++ illusionerRidable = getBoolean("mobs.illusioner.ridable", illusionerRidable); ++ illusionerRidableInWater = getBoolean("mobs.illusioner.ridable-in-water", illusionerRidableInWater); ++ illusionerControllable = getBoolean("mobs.illusioner.controllable", illusionerControllable); ++ illusionerMovementSpeed = getDouble("mobs.illusioner.movement-speed", illusionerMovementSpeed); ++ illusionerFollowRange = getDouble("mobs.illusioner.follow-range", illusionerFollowRange); ++ if (PurpurConfig.version < 8) { ++ double oldValue = getDouble("mobs.illusioner.max-health", illusionerMaxHealth); ++ set("mobs.illusioner.max-health", null); ++ set("mobs.illusioner.attributes.max_health", oldValue); ++ } else if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.illusioner.attributes.max-health", illusionerMaxHealth); ++ set("mobs.illusioner.attributes.max-health", null); ++ set("mobs.illusioner.attributes.max_health", oldValue); ++ } ++ illusionerMaxHealth = getDouble("mobs.illusioner.attributes.max_health", illusionerMaxHealth); ++ illusionerTakeDamageFromWater = getBoolean("mobs.illusioner.takes-damage-from-water", illusionerTakeDamageFromWater); ++ illusionerAlwaysDropExp = getBoolean("mobs.illusioner.always-drop-exp", illusionerAlwaysDropExp); ++ } ++ ++ public boolean ironGolemRidable = false; ++ public boolean ironGolemRidableInWater = true; ++ public boolean ironGolemControllable = true; ++ public boolean ironGolemCanSwim = false; ++ public double ironGolemMaxHealth = 100.0D; ++ public boolean ironGolemTakeDamageFromWater = false; ++ public boolean ironGolemPoppyCalm = false; ++ public boolean ironGolemHealCalm = false; ++ public boolean ironGolemAlwaysDropExp = false; ++ private void ironGolemSettings() { ++ ironGolemRidable = getBoolean("mobs.iron_golem.ridable", ironGolemRidable); ++ ironGolemRidableInWater = getBoolean("mobs.iron_golem.ridable-in-water", ironGolemRidableInWater); ++ ironGolemControllable = getBoolean("mobs.iron_golem.controllable", ironGolemControllable); ++ ironGolemCanSwim = getBoolean("mobs.iron_golem.can-swim", ironGolemCanSwim); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.iron_golem.attributes.max-health", ironGolemMaxHealth); ++ set("mobs.iron_golem.attributes.max-health", null); ++ set("mobs.iron_golem.attributes.max_health", oldValue); ++ } ++ ironGolemMaxHealth = getDouble("mobs.iron_golem.attributes.max_health", ironGolemMaxHealth); ++ ironGolemTakeDamageFromWater = getBoolean("mobs.iron_golem.takes-damage-from-water", ironGolemTakeDamageFromWater); ++ ironGolemPoppyCalm = getBoolean("mobs.iron_golem.poppy-calms-anger", ironGolemPoppyCalm); ++ ironGolemHealCalm = getBoolean("mobs.iron_golem.healing-calms-anger", ironGolemHealCalm); ++ ironGolemAlwaysDropExp = getBoolean("mobs.iron_golem.always-drop-exp", ironGolemAlwaysDropExp); ++ } ++ ++ public boolean llamaRidable = false; ++ public boolean llamaRidableInWater = false; ++ public boolean llamaControllable = true; ++ public double llamaMaxHealthMin = 15.0D; ++ public double llamaMaxHealthMax = 30.0D; ++ public double llamaJumpStrengthMin = 0.5D; ++ public double llamaJumpStrengthMax = 0.5D; ++ public double llamaMovementSpeedMin = 0.175D; ++ public double llamaMovementSpeedMax = 0.175D; ++ public int llamaBreedingTicks = 6000; ++ public boolean llamaTakeDamageFromWater = false; ++ public boolean llamaJoinCaravans = true; ++ public boolean llamaAlwaysDropExp = false; ++ private void llamaSettings() { ++ llamaRidable = getBoolean("mobs.llama.ridable", llamaRidable); ++ llamaRidableInWater = getBoolean("mobs.llama.ridable-in-water", llamaRidableInWater); ++ llamaControllable = getBoolean("mobs.llama.controllable", llamaControllable); ++ if (PurpurConfig.version < 10) { ++ double oldMin = getDouble("mobs.llama.attributes.max-health.min", llamaMaxHealthMin); ++ double oldMax = getDouble("mobs.llama.attributes.max-health.max", llamaMaxHealthMax); ++ set("mobs.llama.attributes.max-health", null); ++ set("mobs.llama.attributes.max_health.min", oldMin); ++ set("mobs.llama.attributes.max_health.max", oldMax); ++ } ++ llamaMaxHealthMin = getDouble("mobs.llama.attributes.max_health.min", llamaMaxHealthMin); ++ llamaMaxHealthMax = getDouble("mobs.llama.attributes.max_health.max", llamaMaxHealthMax); ++ llamaJumpStrengthMin = getDouble("mobs.llama.attributes.jump_strength.min", llamaJumpStrengthMin); ++ llamaJumpStrengthMax = getDouble("mobs.llama.attributes.jump_strength.max", llamaJumpStrengthMax); ++ llamaMovementSpeedMin = getDouble("mobs.llama.attributes.movement_speed.min", llamaMovementSpeedMin); ++ llamaMovementSpeedMax = getDouble("mobs.llama.attributes.movement_speed.max", llamaMovementSpeedMax); ++ llamaBreedingTicks = getInt("mobs.llama.breeding-delay-ticks", llamaBreedingTicks); ++ llamaTakeDamageFromWater = getBoolean("mobs.llama.takes-damage-from-water", llamaTakeDamageFromWater); ++ llamaJoinCaravans = getBoolean("mobs.llama.join-caravans", llamaJoinCaravans); ++ llamaAlwaysDropExp = getBoolean("mobs.llama.always-drop-exp", llamaAlwaysDropExp); ++ } ++ ++ public boolean magmaCubeRidable = false; ++ public boolean magmaCubeRidableInWater = true; ++ public boolean magmaCubeControllable = true; ++ public String magmaCubeMaxHealth = "size * size"; ++ public String magmaCubeAttackDamage = "size"; ++ public Map magmaCubeMaxHealthCache = new HashMap<>(); ++ public Map magmaCubeAttackDamageCache = new HashMap<>(); ++ public boolean magmaCubeTakeDamageFromWater = false; ++ public boolean magmaCubeAlwaysDropExp = false; ++ private void magmaCubeSettings() { ++ magmaCubeRidable = getBoolean("mobs.magma_cube.ridable", magmaCubeRidable); ++ magmaCubeRidableInWater = getBoolean("mobs.magma_cube.ridable-in-water", magmaCubeRidableInWater); ++ magmaCubeControllable = getBoolean("mobs.magma_cube.controllable", magmaCubeControllable); ++ if (PurpurConfig.version < 10) { ++ String oldValue = getString("mobs.magma_cube.attributes.max-health", magmaCubeMaxHealth); ++ set("mobs.magma_cube.attributes.max-health", null); ++ set("mobs.magma_cube.attributes.max_health", oldValue); ++ } ++ magmaCubeMaxHealth = getString("mobs.magma_cube.attributes.max_health", magmaCubeMaxHealth); ++ magmaCubeAttackDamage = getString("mobs.magma_cube.attributes.attack_damage", magmaCubeAttackDamage); ++ magmaCubeMaxHealthCache.clear(); ++ magmaCubeAttackDamageCache.clear(); ++ magmaCubeTakeDamageFromWater = getBoolean("mobs.magma_cube.takes-damage-from-water", magmaCubeTakeDamageFromWater); ++ magmaCubeAlwaysDropExp = getBoolean("mobs.magma_cube.always-drop-exp", magmaCubeAlwaysDropExp); ++ } ++ ++ public boolean mooshroomRidable = false; ++ public boolean mooshroomRidableInWater = true; ++ public boolean mooshroomControllable = true; ++ public double mooshroomMaxHealth = 10.0D; ++ public int mooshroomBreedingTicks = 6000; ++ public boolean mooshroomTakeDamageFromWater = false; ++ public boolean mooshroomAlwaysDropExp = false; ++ private void mooshroomSettings() { ++ mooshroomRidable = getBoolean("mobs.mooshroom.ridable", mooshroomRidable); ++ mooshroomRidableInWater = getBoolean("mobs.mooshroom.ridable-in-water", mooshroomRidableInWater); ++ mooshroomControllable = getBoolean("mobs.mooshroom.controllable", mooshroomControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.mooshroom.attributes.max-health", mooshroomMaxHealth); ++ set("mobs.mooshroom.attributes.max-health", null); ++ set("mobs.mooshroom.attributes.max_health", oldValue); ++ } ++ mooshroomMaxHealth = getDouble("mobs.mooshroom.attributes.max_health", mooshroomMaxHealth); ++ mooshroomBreedingTicks = getInt("mobs.mooshroom.breeding-delay-ticks", mooshroomBreedingTicks); ++ mooshroomTakeDamageFromWater = getBoolean("mobs.mooshroom.takes-damage-from-water", mooshroomTakeDamageFromWater); ++ mooshroomAlwaysDropExp = getBoolean("mobs.mooshroom.always-drop-exp", mooshroomAlwaysDropExp); ++ } ++ ++ public boolean muleRidableInWater = false; ++ public double muleMaxHealthMin = 15.0D; ++ public double muleMaxHealthMax = 30.0D; ++ public double muleJumpStrengthMin = 0.5D; ++ public double muleJumpStrengthMax = 0.5D; ++ public double muleMovementSpeedMin = 0.175D; ++ public double muleMovementSpeedMax = 0.175D; ++ public int muleBreedingTicks = 6000; ++ public boolean muleTakeDamageFromWater = false; ++ public boolean muleAlwaysDropExp = false; ++ private void muleSettings() { ++ muleRidableInWater = getBoolean("mobs.mule.ridable-in-water", muleRidableInWater); ++ if (PurpurConfig.version < 10) { ++ double oldMin = getDouble("mobs.mule.attributes.max-health.min", muleMaxHealthMin); ++ double oldMax = getDouble("mobs.mule.attributes.max-health.max", muleMaxHealthMax); ++ set("mobs.mule.attributes.max-health", null); ++ set("mobs.mule.attributes.max_health.min", oldMin); ++ set("mobs.mule.attributes.max_health.max", oldMax); ++ } ++ muleMaxHealthMin = getDouble("mobs.mule.attributes.max_health.min", muleMaxHealthMin); ++ muleMaxHealthMax = getDouble("mobs.mule.attributes.max_health.max", muleMaxHealthMax); ++ muleJumpStrengthMin = getDouble("mobs.mule.attributes.jump_strength.min", muleJumpStrengthMin); ++ muleJumpStrengthMax = getDouble("mobs.mule.attributes.jump_strength.max", muleJumpStrengthMax); ++ muleMovementSpeedMin = getDouble("mobs.mule.attributes.movement_speed.min", muleMovementSpeedMin); ++ muleMovementSpeedMax = getDouble("mobs.mule.attributes.movement_speed.max", muleMovementSpeedMax); ++ muleBreedingTicks = getInt("mobs.mule.breeding-delay-ticks", muleBreedingTicks); ++ muleTakeDamageFromWater = getBoolean("mobs.mule.takes-damage-from-water", muleTakeDamageFromWater); ++ muleAlwaysDropExp = getBoolean("mobs.mule.always-drop-exp", muleAlwaysDropExp); ++ } ++ ++ public boolean ocelotRidable = false; ++ public boolean ocelotRidableInWater = true; ++ public boolean ocelotControllable = true; ++ public double ocelotMaxHealth = 10.0D; ++ public int ocelotBreedingTicks = 6000; ++ public boolean ocelotTakeDamageFromWater = false; ++ public boolean ocelotAlwaysDropExp = false; ++ public boolean ocelotSpawnUnderSeaLevel = false; ++ private void ocelotSettings() { ++ ocelotRidable = getBoolean("mobs.ocelot.ridable", ocelotRidable); ++ ocelotRidableInWater = getBoolean("mobs.ocelot.ridable-in-water", ocelotRidableInWater); ++ ocelotControllable = getBoolean("mobs.ocelot.controllable", ocelotControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.ocelot.attributes.max-health", ocelotMaxHealth); ++ set("mobs.ocelot.attributes.max-health", null); ++ set("mobs.ocelot.attributes.max_health", oldValue); ++ } ++ ocelotMaxHealth = getDouble("mobs.ocelot.attributes.max_health", ocelotMaxHealth); ++ ocelotBreedingTicks = getInt("mobs.ocelot.breeding-delay-ticks", ocelotBreedingTicks); ++ ocelotTakeDamageFromWater = getBoolean("mobs.ocelot.takes-damage-from-water", ocelotTakeDamageFromWater); ++ ocelotAlwaysDropExp = getBoolean("mobs.ocelot.always-drop-exp", ocelotAlwaysDropExp); ++ ocelotSpawnUnderSeaLevel = getBoolean("mobs.ocelot.spawn-below-sea-level", ocelotSpawnUnderSeaLevel); ++ } ++ ++ public boolean pandaRidable = false; ++ public boolean pandaRidableInWater = true; ++ public boolean pandaControllable = true; ++ public double pandaMaxHealth = 20.0D; ++ public int pandaBreedingTicks = 6000; ++ public boolean pandaTakeDamageFromWater = false; ++ public boolean pandaAlwaysDropExp = false; ++ private void pandaSettings() { ++ pandaRidable = getBoolean("mobs.panda.ridable", pandaRidable); ++ pandaRidableInWater = getBoolean("mobs.panda.ridable-in-water", pandaRidableInWater); ++ pandaControllable = getBoolean("mobs.panda.controllable", pandaControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.panda.attributes.max-health", pandaMaxHealth); ++ set("mobs.panda.attributes.max-health", null); ++ set("mobs.panda.attributes.max_health", oldValue); ++ } ++ pandaMaxHealth = getDouble("mobs.panda.attributes.max_health", pandaMaxHealth); ++ pandaBreedingTicks = getInt("mobs.panda.breeding-delay-ticks", pandaBreedingTicks); ++ pandaTakeDamageFromWater = getBoolean("mobs.panda.takes-damage-from-water", pandaTakeDamageFromWater); ++ pandaAlwaysDropExp = getBoolean("mobs.panda.always-drop-exp", pandaAlwaysDropExp); ++ } ++ ++ public boolean parrotRidable = false; ++ public boolean parrotRidableInWater = true; ++ public boolean parrotControllable = true; ++ public double parrotMaxY = 320D; ++ public double parrotMaxHealth = 6.0D; ++ public boolean parrotTakeDamageFromWater = false; ++ public boolean parrotBreedable = false; ++ public boolean parrotAlwaysDropExp = false; ++ private void parrotSettings() { ++ parrotRidable = getBoolean("mobs.parrot.ridable", parrotRidable); ++ parrotRidableInWater = getBoolean("mobs.parrot.ridable-in-water", parrotRidableInWater); ++ parrotControllable = getBoolean("mobs.parrot.controllable", parrotControllable); ++ parrotMaxY = getDouble("mobs.parrot.ridable-max-y", parrotMaxY); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.parrot.attributes.max-health", parrotMaxHealth); ++ set("mobs.parrot.attributes.max-health", null); ++ set("mobs.parrot.attributes.max_health", oldValue); ++ } ++ parrotMaxHealth = getDouble("mobs.parrot.attributes.max_health", parrotMaxHealth); ++ parrotTakeDamageFromWater = getBoolean("mobs.parrot.takes-damage-from-water", parrotTakeDamageFromWater); ++ parrotBreedable = getBoolean("mobs.parrot.can-breed", parrotBreedable); ++ parrotAlwaysDropExp = getBoolean("mobs.parrot.always-drop-exp", parrotAlwaysDropExp); ++ } ++ ++ public boolean phantomRidable = false; ++ public boolean phantomRidableInWater = true; ++ public boolean phantomControllable = true; ++ public double phantomMaxY = 320D; ++ public float phantomFlameDamage = 1.0F; ++ public int phantomFlameFireTime = 8; ++ public boolean phantomAllowGriefing = false; ++ public String phantomMaxHealth = "20.0"; ++ public String phantomAttackDamage = "6 + size"; ++ public Map phantomMaxHealthCache = new HashMap<>(); ++ public Map phantomAttackDamageCache = new HashMap<>(); ++ public double phantomAttackedByCrystalRadius = 0.0D; ++ public float phantomAttackedByCrystalDamage = 1.0F; ++ public double phantomOrbitCrystalRadius = 0.0D; ++ public int phantomSpawnMinSkyDarkness = 5; ++ public boolean phantomSpawnOnlyAboveSeaLevel = true; ++ public boolean phantomSpawnOnlyWithVisibleSky = true; ++ public double phantomSpawnLocalDifficultyChance = 3.0D; ++ public int phantomSpawnMinPerAttempt = 1; ++ public int phantomSpawnMaxPerAttempt = -1; ++ public int phantomBurnInLight = 0; ++ public boolean phantomIgnorePlayersWithTorch = false; ++ public boolean phantomBurnInDaylight = true; ++ public boolean phantomFlamesOnSwoop = false; ++ public boolean phantomTakeDamageFromWater = false; ++ public boolean phantomAlwaysDropExp = false; ++ public int phantomMinSize = 0; ++ public int phantomMaxSize = 0; ++ private void phantomSettings() { ++ phantomRidable = getBoolean("mobs.phantom.ridable", phantomRidable); ++ phantomRidableInWater = getBoolean("mobs.phantom.ridable-in-water", phantomRidableInWater); ++ phantomControllable = getBoolean("mobs.phantom.controllable", phantomControllable); ++ phantomMaxY = getDouble("mobs.phantom.ridable-max-y", phantomMaxY); ++ phantomFlameDamage = (float) getDouble("mobs.phantom.flames.damage", phantomFlameDamage); ++ phantomFlameFireTime = getInt("mobs.phantom.flames.fire-time", phantomFlameFireTime); ++ phantomAllowGriefing = getBoolean("mobs.phantom.allow-griefing", phantomAllowGriefing); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.phantom.attributes.max-health", Double.parseDouble(phantomMaxHealth)); ++ set("mobs.phantom.attributes.max-health", null); ++ set("mobs.phantom.attributes.max_health", String.valueOf(oldValue)); ++ } ++ if (PurpurConfig.version < 25) { ++ double oldValue = getDouble("mobs.phantom.attributes.max_health", Double.parseDouble(phantomMaxHealth)); ++ set("mobs.phantom.attributes.max_health", String.valueOf(oldValue)); ++ } ++ phantomMaxHealth = getString("mobs.phantom.attributes.max_health", phantomMaxHealth); ++ phantomAttackDamage = getString("mobs.phantom.attributes.attack_damage", phantomAttackDamage); ++ phantomMaxHealthCache.clear(); ++ phantomAttackDamageCache.clear(); ++ phantomAttackedByCrystalRadius = getDouble("mobs.phantom.attacked-by-crystal-range", phantomAttackedByCrystalRadius); ++ phantomAttackedByCrystalDamage = (float) getDouble("mobs.phantom.attacked-by-crystal-damage", phantomAttackedByCrystalDamage); ++ phantomOrbitCrystalRadius = getDouble("mobs.phantom.orbit-crystal-radius", phantomOrbitCrystalRadius); ++ phantomSpawnMinSkyDarkness = getInt("mobs.phantom.spawn.min-sky-darkness", phantomSpawnMinSkyDarkness); ++ phantomSpawnOnlyAboveSeaLevel = getBoolean("mobs.phantom.spawn.only-above-sea-level", phantomSpawnOnlyAboveSeaLevel); ++ phantomSpawnOnlyWithVisibleSky = getBoolean("mobs.phantom.spawn.only-with-visible-sky", phantomSpawnOnlyWithVisibleSky); ++ phantomSpawnLocalDifficultyChance = getDouble("mobs.phantom.spawn.local-difficulty-chance", phantomSpawnLocalDifficultyChance); ++ phantomSpawnMinPerAttempt = getInt("mobs.phantom.spawn.per-attempt.min", phantomSpawnMinPerAttempt); ++ phantomSpawnMaxPerAttempt = getInt("mobs.phantom.spawn.per-attempt.max", phantomSpawnMaxPerAttempt); ++ phantomBurnInLight = getInt("mobs.phantom.burn-in-light", phantomBurnInLight); ++ phantomBurnInDaylight = getBoolean("mobs.phantom.burn-in-daylight", phantomBurnInDaylight); ++ phantomIgnorePlayersWithTorch = getBoolean("mobs.phantom.ignore-players-with-torch", phantomIgnorePlayersWithTorch); ++ phantomFlamesOnSwoop = getBoolean("mobs.phantom.flames-on-swoop", phantomFlamesOnSwoop); ++ phantomTakeDamageFromWater = getBoolean("mobs.phantom.takes-damage-from-water", phantomTakeDamageFromWater); ++ phantomAlwaysDropExp = getBoolean("mobs.phantom.always-drop-exp", phantomAlwaysDropExp); ++ phantomMinSize = Mth.clamp(getInt("mobs.phantom.size.min", phantomMinSize), 0, 64); ++ phantomMaxSize = Mth.clamp(getInt("mobs.phantom.size.max", phantomMaxSize), 0, 64); ++ if (phantomMinSize > phantomMaxSize) { ++ phantomMinSize = phantomMinSize ^ phantomMaxSize; ++ phantomMaxSize = phantomMinSize ^ phantomMaxSize; ++ phantomMinSize = phantomMinSize ^ phantomMaxSize; ++ } ++ } ++ ++ public boolean pigRidable = false; ++ public boolean pigRidableInWater = false; ++ public boolean pigControllable = true; ++ public double pigMaxHealth = 10.0D; ++ public boolean pigGiveSaddleBack = false; ++ public int pigBreedingTicks = 6000; ++ public boolean pigTakeDamageFromWater = false; ++ public boolean pigAlwaysDropExp = false; ++ private void pigSettings() { ++ pigRidable = getBoolean("mobs.pig.ridable", pigRidable); ++ pigRidableInWater = getBoolean("mobs.pig.ridable-in-water", pigRidableInWater); ++ pigControllable = getBoolean("mobs.pig.controllable", pigControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.pig.attributes.max-health", pigMaxHealth); ++ set("mobs.pig.attributes.max-health", null); ++ set("mobs.pig.attributes.max_health", oldValue); ++ } ++ pigMaxHealth = getDouble("mobs.pig.attributes.max_health", pigMaxHealth); ++ pigGiveSaddleBack = getBoolean("mobs.pig.give-saddle-back", pigGiveSaddleBack); ++ pigBreedingTicks = getInt("mobs.pig.breeding-delay-ticks", pigBreedingTicks); ++ pigTakeDamageFromWater = getBoolean("mobs.pig.takes-damage-from-water", pigTakeDamageFromWater); ++ pigAlwaysDropExp = getBoolean("mobs.pig.always-drop-exp", pigAlwaysDropExp); ++ } ++ ++ public boolean piglinRidable = false; ++ public boolean piglinRidableInWater = true; ++ public boolean piglinControllable = true; ++ public double piglinMaxHealth = 16.0D; ++ public boolean piglinBypassMobGriefing = false; ++ public boolean piglinTakeDamageFromWater = false; ++ public int piglinPortalSpawnModifier = 2000; ++ public boolean piglinAlwaysDropExp = false; ++ public double piglinHeadVisibilityPercent = 0.5D; ++ public boolean piglinIgnoresArmorWithGoldTrim = false; ++ private void piglinSettings() { ++ piglinRidable = getBoolean("mobs.piglin.ridable", piglinRidable); ++ piglinRidableInWater = getBoolean("mobs.piglin.ridable-in-water", piglinRidableInWater); ++ piglinControllable = getBoolean("mobs.piglin.controllable", piglinControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.piglin.attributes.max-health", piglinMaxHealth); ++ set("mobs.piglin.attributes.max-health", null); ++ set("mobs.piglin.attributes.max_health", oldValue); ++ } ++ piglinMaxHealth = getDouble("mobs.piglin.attributes.max_health", piglinMaxHealth); ++ piglinBypassMobGriefing = getBoolean("mobs.piglin.bypass-mob-griefing", piglinBypassMobGriefing); ++ piglinTakeDamageFromWater = getBoolean("mobs.piglin.takes-damage-from-water", piglinTakeDamageFromWater); ++ piglinPortalSpawnModifier = getInt("mobs.piglin.portal-spawn-modifier", piglinPortalSpawnModifier); ++ piglinAlwaysDropExp = getBoolean("mobs.piglin.always-drop-exp", piglinAlwaysDropExp); ++ piglinHeadVisibilityPercent = getDouble("mobs.piglin.head-visibility-percent", piglinHeadVisibilityPercent); ++ piglinIgnoresArmorWithGoldTrim = getBoolean("mobs.piglin.ignores-armor-with-gold-trim", piglinIgnoresArmorWithGoldTrim); ++ } ++ ++ public boolean piglinBruteRidable = false; ++ public boolean piglinBruteRidableInWater = true; ++ public boolean piglinBruteControllable = true; ++ public double piglinBruteMaxHealth = 50.0D; ++ public boolean piglinBruteTakeDamageFromWater = false; ++ public boolean piglinBruteAlwaysDropExp = false; ++ private void piglinBruteSettings() { ++ piglinBruteRidable = getBoolean("mobs.piglin_brute.ridable", piglinBruteRidable); ++ piglinBruteRidableInWater = getBoolean("mobs.piglin_brute.ridable-in-water", piglinBruteRidableInWater); ++ piglinBruteControllable = getBoolean("mobs.piglin_brute.controllable", piglinBruteControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.piglin_brute.attributes.max-health", piglinBruteMaxHealth); ++ set("mobs.piglin_brute.attributes.max-health", null); ++ set("mobs.piglin_brute.attributes.max_health", oldValue); ++ } ++ piglinBruteMaxHealth = getDouble("mobs.piglin_brute.attributes.max_health", piglinBruteMaxHealth); ++ piglinBruteTakeDamageFromWater = getBoolean("mobs.piglin_brute.takes-damage-from-water", piglinBruteTakeDamageFromWater); ++ piglinBruteAlwaysDropExp = getBoolean("mobs.piglin_brute.always-drop-exp", piglinBruteAlwaysDropExp); ++ } ++ ++ public boolean pillagerRidable = false; ++ public boolean pillagerRidableInWater = true; ++ public boolean pillagerControllable = true; ++ public double pillagerMaxHealth = 24.0D; ++ public boolean pillagerBypassMobGriefing = false; ++ public boolean pillagerTakeDamageFromWater = false; ++ public boolean pillagerAlwaysDropExp = false; ++ private void pillagerSettings() { ++ pillagerRidable = getBoolean("mobs.pillager.ridable", pillagerRidable); ++ pillagerRidableInWater = getBoolean("mobs.pillager.ridable-in-water", pillagerRidableInWater); ++ pillagerControllable = getBoolean("mobs.pillager.controllable", pillagerControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.pillager.attributes.max-health", pillagerMaxHealth); ++ set("mobs.pillager.attributes.max-health", null); ++ set("mobs.pillager.attributes.max_health", oldValue); ++ } ++ pillagerMaxHealth = getDouble("mobs.pillager.attributes.max_health", pillagerMaxHealth); ++ pillagerBypassMobGriefing = getBoolean("mobs.pillager.bypass-mob-griefing", pillagerBypassMobGriefing); ++ pillagerTakeDamageFromWater = getBoolean("mobs.pillager.takes-damage-from-water", pillagerTakeDamageFromWater); ++ pillagerAlwaysDropExp = getBoolean("mobs.pillager.always-drop-exp", pillagerAlwaysDropExp); ++ } ++ ++ public boolean polarBearRidable = false; ++ public boolean polarBearRidableInWater = true; ++ public boolean polarBearControllable = true; ++ public double polarBearMaxHealth = 30.0D; ++ public String polarBearBreedableItemString = ""; ++ public Item polarBearBreedableItem = null; ++ public int polarBearBreedingTicks = 6000; ++ public boolean polarBearTakeDamageFromWater = false; ++ public boolean polarBearAlwaysDropExp = false; ++ private void polarBearSettings() { ++ polarBearRidable = getBoolean("mobs.polar_bear.ridable", polarBearRidable); ++ polarBearRidableInWater = getBoolean("mobs.polar_bear.ridable-in-water", polarBearRidableInWater); ++ polarBearControllable = getBoolean("mobs.polar_bear.controllable", polarBearControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.polar_bear.attributes.max-health", polarBearMaxHealth); ++ set("mobs.polar_bear.attributes.max-health", null); ++ set("mobs.polar_bear.attributes.max_health", oldValue); ++ } ++ polarBearMaxHealth = getDouble("mobs.polar_bear.attributes.max_health", polarBearMaxHealth); ++ polarBearBreedableItemString = getString("mobs.polar_bear.breedable-item", polarBearBreedableItemString); ++ Item item = BuiltInRegistries.ITEM.get(new ResourceLocation(polarBearBreedableItemString)); ++ if (item != Items.AIR) polarBearBreedableItem = item; ++ polarBearBreedingTicks = getInt("mobs.polar_bear.breeding-delay-ticks", polarBearBreedingTicks); ++ polarBearTakeDamageFromWater = getBoolean("mobs.polar_bear.takes-damage-from-water", polarBearTakeDamageFromWater); ++ polarBearAlwaysDropExp = getBoolean("mobs.polar_bear.always-drop-exp", polarBearAlwaysDropExp); ++ } ++ ++ public boolean pufferfishRidable = false; ++ public boolean pufferfishControllable = true; ++ public double pufferfishMaxHealth = 3.0D; ++ public boolean pufferfishTakeDamageFromWater = false; ++ public boolean pufferfishAlwaysDropExp = false; ++ private void pufferfishSettings() { ++ pufferfishRidable = getBoolean("mobs.pufferfish.ridable", pufferfishRidable); ++ pufferfishControllable = getBoolean("mobs.pufferfish.controllable", pufferfishControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.pufferfish.attributes.max-health", pufferfishMaxHealth); ++ set("mobs.pufferfish.attributes.max-health", null); ++ set("mobs.pufferfish.attributes.max_health", oldValue); ++ } ++ pufferfishMaxHealth = getDouble("mobs.pufferfish.attributes.max_health", pufferfishMaxHealth); ++ pufferfishTakeDamageFromWater = getBoolean("mobs.pufferfish.takes-damage-from-water", pufferfishTakeDamageFromWater); ++ pufferfishAlwaysDropExp = getBoolean("mobs.pufferfish.always-drop-exp", pufferfishAlwaysDropExp); ++ } ++ ++ public boolean rabbitRidable = false; ++ public boolean rabbitRidableInWater = true; ++ public boolean rabbitControllable = true; ++ public double rabbitMaxHealth = 3.0D; ++ public double rabbitNaturalToast = 0.0D; ++ public double rabbitNaturalKiller = 0.0D; ++ public int rabbitBreedingTicks = 6000; ++ public boolean rabbitBypassMobGriefing = false; ++ public boolean rabbitTakeDamageFromWater = false; ++ public boolean rabbitAlwaysDropExp = false; ++ private void rabbitSettings() { ++ rabbitRidable = getBoolean("mobs.rabbit.ridable", rabbitRidable); ++ rabbitRidableInWater = getBoolean("mobs.rabbit.ridable-in-water", rabbitRidableInWater); ++ rabbitControllable = getBoolean("mobs.rabbit.controllable", rabbitControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.rabbit.attributes.max-health", rabbitMaxHealth); ++ set("mobs.rabbit.attributes.max-health", null); ++ set("mobs.rabbit.attributes.max_health", oldValue); ++ } ++ rabbitMaxHealth = getDouble("mobs.rabbit.attributes.max_health", rabbitMaxHealth); ++ rabbitNaturalToast = getDouble("mobs.rabbit.spawn-toast-chance", rabbitNaturalToast); ++ rabbitNaturalKiller = getDouble("mobs.rabbit.spawn-killer-rabbit-chance", rabbitNaturalKiller); ++ rabbitBreedingTicks = getInt("mobs.rabbit.breeding-delay-ticks", rabbitBreedingTicks); ++ rabbitBypassMobGriefing = getBoolean("mobs.rabbit.bypass-mob-griefing", rabbitBypassMobGriefing); ++ rabbitTakeDamageFromWater = getBoolean("mobs.rabbit.takes-damage-from-water", rabbitTakeDamageFromWater); ++ rabbitAlwaysDropExp = getBoolean("mobs.rabbit.always-drop-exp", rabbitAlwaysDropExp); ++ } ++ ++ public boolean ravagerRidable = false; ++ public boolean ravagerRidableInWater = false; ++ public boolean ravagerControllable = true; ++ public double ravagerMaxHealth = 100.0D; ++ public boolean ravagerBypassMobGriefing = false; ++ public boolean ravagerTakeDamageFromWater = false; ++ public List ravagerGriefableBlocks = new ArrayList<>(); ++ public boolean ravagerAlwaysDropExp = false; ++ private void ravagerSettings() { ++ ravagerRidable = getBoolean("mobs.ravager.ridable", ravagerRidable); ++ ravagerRidableInWater = getBoolean("mobs.ravager.ridable-in-water", ravagerRidableInWater); ++ ravagerControllable = getBoolean("mobs.ravager.controllable", ravagerControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.ravager.attributes.max-health", ravagerMaxHealth); ++ set("mobs.ravager.attributes.max-health", null); ++ set("mobs.ravager.attributes.max_health", oldValue); ++ } ++ ravagerMaxHealth = getDouble("mobs.ravager.attributes.max_health", ravagerMaxHealth); ++ ravagerBypassMobGriefing = getBoolean("mobs.ravager.bypass-mob-griefing", ravagerBypassMobGriefing); ++ ravagerTakeDamageFromWater = getBoolean("mobs.ravager.takes-damage-from-water", ravagerTakeDamageFromWater); ++ getList("mobs.ravager.griefable-blocks", new ArrayList(){{ ++ add("minecraft:oak_leaves"); ++ add("minecraft:spruce_leaves"); ++ add("minecraft:birch_leaves"); ++ add("minecraft:jungle_leaves"); ++ add("minecraft:acacia_leaves"); ++ add("minecraft:dark_oak_leaves"); ++ add("minecraft:beetroots"); ++ add("minecraft:carrots"); ++ add("minecraft:potatoes"); ++ add("minecraft:wheat"); ++ }}).forEach(key -> { ++ Block block = BuiltInRegistries.BLOCK.get(new ResourceLocation(key.toString())); ++ if (!block.defaultBlockState().isAir()) { ++ ravagerGriefableBlocks.add(block); ++ } ++ }); ++ ravagerAlwaysDropExp = getBoolean("mobs.ravager.always-drop-exp", ravagerAlwaysDropExp); ++ } ++ ++ public boolean salmonRidable = false; ++ public boolean salmonControllable = true; ++ public double salmonMaxHealth = 3.0D; ++ public boolean salmonTakeDamageFromWater = false; ++ public boolean salmonAlwaysDropExp = false; ++ private void salmonSettings() { ++ salmonRidable = getBoolean("mobs.salmon.ridable", salmonRidable); ++ salmonControllable = getBoolean("mobs.salmon.controllable", salmonControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.salmon.attributes.max-health", salmonMaxHealth); ++ set("mobs.salmon.attributes.max-health", null); ++ set("mobs.salmon.attributes.max_health", oldValue); ++ } ++ salmonMaxHealth = getDouble("mobs.salmon.attributes.max_health", salmonMaxHealth); ++ salmonTakeDamageFromWater = getBoolean("mobs.salmon.takes-damage-from-water", salmonTakeDamageFromWater); ++ salmonAlwaysDropExp = getBoolean("mobs.salmon.always-drop-exp", salmonAlwaysDropExp); ++ } ++ ++ public boolean sheepRidable = false; ++ public boolean sheepRidableInWater = true; ++ public boolean sheepControllable = true; ++ public double sheepMaxHealth = 8.0D; ++ public int sheepBreedingTicks = 6000; ++ public boolean sheepBypassMobGriefing = false; ++ public boolean sheepTakeDamageFromWater = false; ++ public boolean sheepAlwaysDropExp = false; ++ private void sheepSettings() { ++ sheepRidable = getBoolean("mobs.sheep.ridable", sheepRidable); ++ sheepRidableInWater = getBoolean("mobs.sheep.ridable-in-water", sheepRidableInWater); ++ sheepControllable = getBoolean("mobs.sheep.controllable", sheepControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.sheep.attributes.max-health", sheepMaxHealth); ++ set("mobs.sheep.attributes.max-health", null); ++ set("mobs.sheep.attributes.max_health", oldValue); ++ } ++ sheepMaxHealth = getDouble("mobs.sheep.attributes.max_health", sheepMaxHealth); ++ sheepBreedingTicks = getInt("mobs.sheep.breeding-delay-ticks", sheepBreedingTicks); ++ sheepBypassMobGriefing = getBoolean("mobs.sheep.bypass-mob-griefing", sheepBypassMobGriefing); ++ sheepTakeDamageFromWater = getBoolean("mobs.sheep.takes-damage-from-water", sheepTakeDamageFromWater); ++ sheepAlwaysDropExp = getBoolean("mobs.sheep.always-drop-exp", sheepAlwaysDropExp); ++ } ++ ++ public boolean shulkerRidable = false; ++ public boolean shulkerRidableInWater = true; ++ public boolean shulkerControllable = true; ++ public double shulkerMaxHealth = 30.0D; ++ public boolean shulkerTakeDamageFromWater = false; ++ public float shulkerSpawnFromBulletBaseChance = 1.0F; ++ public boolean shulkerSpawnFromBulletRequireOpenLid = true; ++ public double shulkerSpawnFromBulletNearbyRange = 8.0D; ++ public String shulkerSpawnFromBulletNearbyEquation = "(nearby - 1) / 5.0"; ++ public boolean shulkerSpawnFromBulletRandomColor = false; ++ public boolean shulkerChangeColorWithDye = false; ++ public boolean shulkerAlwaysDropExp = false; ++ private void shulkerSettings() { ++ shulkerRidable = getBoolean("mobs.shulker.ridable", shulkerRidable); ++ shulkerRidableInWater = getBoolean("mobs.shulker.ridable-in-water", shulkerRidableInWater); ++ shulkerControllable = getBoolean("mobs.shulker.controllable", shulkerControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.shulker.attributes.max-health", shulkerMaxHealth); ++ set("mobs.shulker.attributes.max-health", null); ++ set("mobs.shulker.attributes.max_health", oldValue); ++ } ++ shulkerMaxHealth = getDouble("mobs.shulker.attributes.max_health", shulkerMaxHealth); ++ shulkerTakeDamageFromWater = getBoolean("mobs.shulker.takes-damage-from-water", shulkerTakeDamageFromWater); ++ shulkerSpawnFromBulletBaseChance = (float) getDouble("mobs.shulker.spawn-from-bullet.base-chance", shulkerSpawnFromBulletBaseChance); ++ shulkerSpawnFromBulletRequireOpenLid = getBoolean("mobs.shulker.spawn-from-bullet.require-open-lid", shulkerSpawnFromBulletRequireOpenLid); ++ shulkerSpawnFromBulletNearbyRange = getDouble("mobs.shulker.spawn-from-bullet.nearby-range", shulkerSpawnFromBulletNearbyRange); ++ shulkerSpawnFromBulletNearbyEquation = getString("mobs.shulker.spawn-from-bullet.nearby-equation", shulkerSpawnFromBulletNearbyEquation); ++ shulkerSpawnFromBulletRandomColor = getBoolean("mobs.shulker.spawn-from-bullet.random-color", shulkerSpawnFromBulletRandomColor); ++ shulkerChangeColorWithDye = getBoolean("mobs.shulker.change-color-with-dye", shulkerChangeColorWithDye); ++ shulkerAlwaysDropExp = getBoolean("mobs.shulker.always-drop-exp", shulkerAlwaysDropExp); ++ } ++ ++ public boolean silverfishRidable = false; ++ public boolean silverfishRidableInWater = true; ++ public boolean silverfishControllable = true; ++ public double silverfishMaxHealth = 8.0D; ++ public boolean silverfishBypassMobGriefing = false; ++ public boolean silverfishTakeDamageFromWater = false; ++ public boolean silverfishAlwaysDropExp = false; ++ private void silverfishSettings() { ++ silverfishRidable = getBoolean("mobs.silverfish.ridable", silverfishRidable); ++ silverfishRidableInWater = getBoolean("mobs.silverfish.ridable-in-water", silverfishRidableInWater); ++ silverfishControllable = getBoolean("mobs.silverfish.controllable", silverfishControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.silverfish.attributes.max-health", silverfishMaxHealth); ++ set("mobs.silverfish.attributes.max-health", null); ++ set("mobs.silverfish.attributes.max_health", oldValue); ++ } ++ silverfishMaxHealth = getDouble("mobs.silverfish.attributes.max_health", silverfishMaxHealth); ++ silverfishBypassMobGriefing = getBoolean("mobs.silverfish.bypass-mob-griefing", silverfishBypassMobGriefing); ++ silverfishTakeDamageFromWater = getBoolean("mobs.silverfish.takes-damage-from-water", silverfishTakeDamageFromWater); ++ silverfishAlwaysDropExp = getBoolean("mobs.silverfish.always-drop-exp", silverfishAlwaysDropExp); ++ } ++ ++ public boolean skeletonRidable = false; ++ public boolean skeletonRidableInWater = true; ++ public boolean skeletonControllable = true; ++ public double skeletonMaxHealth = 20.0D; ++ public boolean skeletonTakeDamageFromWater = false; ++ public boolean skeletonAlwaysDropExp = false; ++ public double skeletonHeadVisibilityPercent = 0.5D; ++ public int skeletonFeedWitherRoses = 0; ++ public String skeletonBowAccuracy = "14 - difficulty * 4"; ++ public Map skeletonBowAccuracyMap = new HashMap<>(); ++ private void skeletonSettings() { ++ skeletonRidable = getBoolean("mobs.skeleton.ridable", skeletonRidable); ++ skeletonRidableInWater = getBoolean("mobs.skeleton.ridable-in-water", skeletonRidableInWater); ++ skeletonControllable = getBoolean("mobs.skeleton.controllable", skeletonControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.skeleton.attributes.max-health", skeletonMaxHealth); ++ set("mobs.skeleton.attributes.max-health", null); ++ set("mobs.skeleton.attributes.max_health", oldValue); ++ } ++ skeletonMaxHealth = getDouble("mobs.skeleton.attributes.max_health", skeletonMaxHealth); ++ skeletonTakeDamageFromWater = getBoolean("mobs.skeleton.takes-damage-from-water", skeletonTakeDamageFromWater); ++ skeletonAlwaysDropExp = getBoolean("mobs.skeleton.always-drop-exp", skeletonAlwaysDropExp); ++ skeletonHeadVisibilityPercent = getDouble("mobs.skeleton.head-visibility-percent", skeletonHeadVisibilityPercent); ++ skeletonFeedWitherRoses = getInt("mobs.skeleton.feed-wither-roses", skeletonFeedWitherRoses); ++ final String defaultSkeletonBowAccuracy = skeletonBowAccuracy; ++ skeletonBowAccuracy = getString("mobs.skeleton.bow-accuracy", skeletonBowAccuracy); ++ for (int i = 1; i < 4; i++) { ++ final float divergence; ++ try { ++ divergence = ((Number) Entity.scriptEngine.eval("let difficulty = " + i + "; " + skeletonBowAccuracy)).floatValue(); ++ } catch (javax.script.ScriptException e) { ++ e.printStackTrace(); ++ break; ++ } ++ skeletonBowAccuracyMap.put(i, divergence); ++ } ++ } ++ ++ public boolean skeletonHorseRidable = false; ++ public boolean skeletonHorseRidableInWater = true; ++ public boolean skeletonHorseCanSwim = false; ++ public double skeletonHorseMaxHealthMin = 15.0D; ++ public double skeletonHorseMaxHealthMax = 15.0D; ++ public double skeletonHorseJumpStrengthMin = 0.4D; ++ public double skeletonHorseJumpStrengthMax = 1.0D; ++ public double skeletonHorseMovementSpeedMin = 0.2D; ++ public double skeletonHorseMovementSpeedMax = 0.2D; ++ public boolean skeletonHorseTakeDamageFromWater = false; ++ public boolean skeletonHorseAlwaysDropExp = false; ++ private void skeletonHorseSettings() { ++ skeletonHorseRidable = getBoolean("mobs.skeleton_horse.ridable", skeletonHorseRidable); ++ skeletonHorseRidableInWater = getBoolean("mobs.skeleton_horse.ridable-in-water", skeletonHorseRidableInWater); ++ skeletonHorseCanSwim = getBoolean("mobs.skeleton_horse.can-swim", skeletonHorseCanSwim); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.skeleton_horse.attributes.max-health", skeletonHorseMaxHealthMin); ++ set("mobs.skeleton_horse.attributes.max-health", null); ++ set("mobs.skeleton_horse.attributes.max_health.min", oldValue); ++ set("mobs.skeleton_horse.attributes.max_health.max", oldValue); ++ } ++ skeletonHorseMaxHealthMin = getDouble("mobs.skeleton_horse.attributes.max_health.min", skeletonHorseMaxHealthMin); ++ skeletonHorseMaxHealthMax = getDouble("mobs.skeleton_horse.attributes.max_health.max", skeletonHorseMaxHealthMax); ++ skeletonHorseJumpStrengthMin = getDouble("mobs.skeleton_horse.attributes.jump_strength.min", skeletonHorseJumpStrengthMin); ++ skeletonHorseJumpStrengthMax = getDouble("mobs.skeleton_horse.attributes.jump_strength.max", skeletonHorseJumpStrengthMax); ++ skeletonHorseMovementSpeedMin = getDouble("mobs.skeleton_horse.attributes.movement_speed.min", skeletonHorseMovementSpeedMin); ++ skeletonHorseMovementSpeedMax = getDouble("mobs.skeleton_horse.attributes.movement_speed.max", skeletonHorseMovementSpeedMax); ++ skeletonHorseTakeDamageFromWater = getBoolean("mobs.skeleton_horse.takes-damage-from-water", skeletonHorseTakeDamageFromWater); ++ skeletonHorseAlwaysDropExp = getBoolean("mobs.skeleton_horse.always-drop-exp", skeletonHorseAlwaysDropExp); ++ } ++ ++ public boolean slimeRidable = false; ++ public boolean slimeRidableInWater = true; ++ public boolean slimeControllable = true; ++ public String slimeMaxHealth = "size * size"; ++ public String slimeAttackDamage = "size"; ++ public Map slimeMaxHealthCache = new HashMap<>(); ++ public Map slimeAttackDamageCache = new HashMap<>(); ++ public boolean slimeTakeDamageFromWater = false; ++ public boolean slimeAlwaysDropExp = false; ++ private void slimeSettings() { ++ slimeRidable = getBoolean("mobs.slime.ridable", slimeRidable); ++ slimeRidableInWater = getBoolean("mobs.slime.ridable-in-water", slimeRidableInWater); ++ slimeControllable = getBoolean("mobs.slime.controllable", slimeControllable); ++ if (PurpurConfig.version < 10) { ++ String oldValue = getString("mobs.slime.attributes.max-health", slimeMaxHealth); ++ set("mobs.slime.attributes.max-health", null); ++ set("mobs.slime.attributes.max_health", oldValue); ++ } ++ slimeMaxHealth = getString("mobs.slime.attributes.max_health", slimeMaxHealth); ++ slimeAttackDamage = getString("mobs.slime.attributes.attack_damage", slimeAttackDamage); ++ slimeMaxHealthCache.clear(); ++ slimeAttackDamageCache.clear(); ++ slimeTakeDamageFromWater = getBoolean("mobs.slime.takes-damage-from-water", slimeTakeDamageFromWater); ++ slimeAlwaysDropExp = getBoolean("mobs.slime.always-drop-exp", slimeAlwaysDropExp); ++ } ++ ++ public boolean snowGolemRidable = false; ++ public boolean snowGolemRidableInWater = true; ++ public boolean snowGolemControllable = true; ++ public boolean snowGolemLeaveTrailWhenRidden = false; ++ public double snowGolemMaxHealth = 4.0D; ++ public boolean snowGolemPutPumpkinBack = false; ++ public int snowGolemSnowBallMin = 20; ++ public int snowGolemSnowBallMax = 20; ++ public float snowGolemSnowBallModifier = 10.0F; ++ public double snowGolemAttackDistance = 1.25D; ++ public boolean snowGolemBypassMobGriefing = false; ++ public boolean snowGolemTakeDamageFromWater = true; ++ public boolean snowGolemAlwaysDropExp = false; ++ private void snowGolemSettings() { ++ snowGolemRidable = getBoolean("mobs.snow_golem.ridable", snowGolemRidable); ++ snowGolemRidableInWater = getBoolean("mobs.snow_golem.ridable-in-water", snowGolemRidableInWater); ++ snowGolemControllable = getBoolean("mobs.snow_golem.controllable", snowGolemControllable); ++ snowGolemLeaveTrailWhenRidden = getBoolean("mobs.snow_golem.leave-trail-when-ridden", snowGolemLeaveTrailWhenRidden); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.snow_golem.attributes.max-health", snowGolemMaxHealth); ++ set("mobs.snow_golem.attributes.max-health", null); ++ set("mobs.snow_golem.attributes.max_health", oldValue); ++ } ++ snowGolemMaxHealth = getDouble("mobs.snow_golem.attributes.max_health", snowGolemMaxHealth); ++ snowGolemPutPumpkinBack = getBoolean("mobs.snow_golem.pumpkin-can-be-added-back", snowGolemPutPumpkinBack); ++ snowGolemSnowBallMin = getInt("mobs.snow_golem.min-shoot-interval-ticks", snowGolemSnowBallMin); ++ snowGolemSnowBallMax = getInt("mobs.snow_golem.max-shoot-interval-ticks", snowGolemSnowBallMax); ++ snowGolemSnowBallModifier = (float) getDouble("mobs.snow_golem.snow-ball-modifier", snowGolemSnowBallModifier); ++ snowGolemAttackDistance = getDouble("mobs.snow_golem.attack-distance", snowGolemAttackDistance); ++ snowGolemBypassMobGriefing = getBoolean("mobs.snow_golem.bypass-mob-griefing", snowGolemBypassMobGriefing); ++ snowGolemTakeDamageFromWater = getBoolean("mobs.snow_golem.takes-damage-from-water", snowGolemTakeDamageFromWater); ++ snowGolemAlwaysDropExp = getBoolean("mobs.snow_golem.always-drop-exp", snowGolemAlwaysDropExp); ++ } ++ ++ public boolean snifferRidable = false; ++ public boolean snifferRidableInWater = true; ++ public boolean snifferControllable = true; ++ public double snifferMaxHealth = 14.0D; ++ public int snifferBreedingTicks = 6000; ++ private void snifferSettings() { ++ snifferRidable = getBoolean("mobs.sniffer.ridable", snifferRidable); ++ snifferRidableInWater = getBoolean("mobs.sniffer.ridable-in-water", snifferRidableInWater); ++ snifferControllable = getBoolean("mobs.sniffer.controllable", snifferControllable); ++ snifferMaxHealth = getDouble("mobs.sniffer.attributes.max_health", snifferMaxHealth); ++ snifferBreedingTicks = getInt("mobs.sniffer.breeding-delay-ticks", chickenBreedingTicks); ++ } ++ ++ public boolean squidRidable = false; ++ public boolean squidControllable = true; ++ public double squidMaxHealth = 10.0D; ++ public boolean squidImmuneToEAR = true; ++ public double squidOffsetWaterCheck = 0.0D; ++ public boolean squidsCanFly = false; ++ public boolean squidTakeDamageFromWater = false; ++ public boolean squidAlwaysDropExp = false; ++ private void squidSettings() { ++ squidRidable = getBoolean("mobs.squid.ridable", squidRidable); ++ squidControllable = getBoolean("mobs.squid.controllable", squidControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.squid.attributes.max-health", squidMaxHealth); ++ set("mobs.squid.attributes.max-health", null); ++ set("mobs.squid.attributes.max_health", oldValue); ++ } ++ squidMaxHealth = getDouble("mobs.squid.attributes.max_health", squidMaxHealth); ++ squidImmuneToEAR = getBoolean("mobs.squid.immune-to-EAR", squidImmuneToEAR); ++ squidOffsetWaterCheck = getDouble("mobs.squid.water-offset-check", squidOffsetWaterCheck); ++ squidsCanFly = getBoolean("mobs.squid.can-fly", squidsCanFly); ++ squidTakeDamageFromWater = getBoolean("mobs.squid.takes-damage-from-water", squidTakeDamageFromWater); ++ squidAlwaysDropExp = getBoolean("mobs.squid.always-drop-exp", squidAlwaysDropExp); ++ } ++ ++ public boolean spiderRidable = false; ++ public boolean spiderRidableInWater = false; ++ public boolean spiderControllable = true; ++ public double spiderMaxHealth = 16.0D; ++ public boolean spiderTakeDamageFromWater = false; ++ public boolean spiderAlwaysDropExp = false; ++ private void spiderSettings() { ++ spiderRidable = getBoolean("mobs.spider.ridable", spiderRidable); ++ spiderRidableInWater = getBoolean("mobs.spider.ridable-in-water", spiderRidableInWater); ++ spiderControllable = getBoolean("mobs.spider.controllable", spiderControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.spider.attributes.max-health", spiderMaxHealth); ++ set("mobs.spider.attributes.max-health", null); ++ set("mobs.spider.attributes.max_health", oldValue); ++ } ++ spiderMaxHealth = getDouble("mobs.spider.attributes.max_health", spiderMaxHealth); ++ spiderTakeDamageFromWater = getBoolean("mobs.spider.takes-damage-from-water", spiderTakeDamageFromWater); ++ spiderAlwaysDropExp = getBoolean("mobs.spider.always-drop-exp", spiderAlwaysDropExp); ++ } ++ ++ public boolean strayRidable = false; ++ public boolean strayRidableInWater = true; ++ public boolean strayControllable = true; ++ public double strayMaxHealth = 20.0D; ++ public boolean strayTakeDamageFromWater = false; ++ public boolean strayAlwaysDropExp = false; ++ private void straySettings() { ++ strayRidable = getBoolean("mobs.stray.ridable", strayRidable); ++ strayRidableInWater = getBoolean("mobs.stray.ridable-in-water", strayRidableInWater); ++ strayControllable = getBoolean("mobs.stray.controllable", strayControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.stray.attributes.max-health", strayMaxHealth); ++ set("mobs.stray.attributes.max-health", null); ++ set("mobs.stray.attributes.max_health", oldValue); ++ } ++ strayMaxHealth = getDouble("mobs.stray.attributes.max_health", strayMaxHealth); ++ strayTakeDamageFromWater = getBoolean("mobs.stray.takes-damage-from-water", strayTakeDamageFromWater); ++ strayAlwaysDropExp = getBoolean("mobs.stray.always-drop-exp", strayAlwaysDropExp); ++ } ++ ++ public boolean striderRidable = false; ++ public boolean striderRidableInWater = false; ++ public boolean striderControllable = true; ++ public double striderMaxHealth = 20.0D; ++ public int striderBreedingTicks = 6000; ++ public boolean striderGiveSaddleBack = false; ++ public boolean striderTakeDamageFromWater = true; ++ public boolean striderAlwaysDropExp = false; ++ private void striderSettings() { ++ striderRidable = getBoolean("mobs.strider.ridable", striderRidable); ++ striderRidableInWater = getBoolean("mobs.strider.ridable-in-water", striderRidableInWater); ++ striderControllable = getBoolean("mobs.strider.controllable", striderControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.strider.attributes.max-health", striderMaxHealth); ++ set("mobs.strider.attributes.max-health", null); ++ set("mobs.strider.attributes.max_health", oldValue); ++ } ++ striderMaxHealth = getDouble("mobs.strider.attributes.max_health", striderMaxHealth); ++ striderBreedingTicks = getInt("mobs.strider.breeding-delay-ticks", striderBreedingTicks); ++ striderGiveSaddleBack = getBoolean("mobs.strider.give-saddle-back", striderGiveSaddleBack); ++ striderTakeDamageFromWater = getBoolean("mobs.strider.takes-damage-from-water", striderTakeDamageFromWater); ++ striderAlwaysDropExp = getBoolean("mobs.strider.always-drop-exp", striderAlwaysDropExp); ++ } ++ ++ public boolean tadpoleRidable = false; ++ public boolean tadpoleRidableInWater = true; ++ public boolean tadpoleControllable = true; ++ private void tadpoleSettings() { ++ tadpoleRidable = getBoolean("mobs.tadpole.ridable", tadpoleRidable); ++ tadpoleRidableInWater = getBoolean("mobs.tadpole.ridable-in-water", tadpoleRidableInWater); ++ tadpoleControllable = getBoolean("mobs.tadpole.controllable", tadpoleControllable); ++ } ++ ++ public boolean traderLlamaRidable = false; ++ public boolean traderLlamaRidableInWater = false; ++ public boolean traderLlamaControllable = true; ++ public double traderLlamaMaxHealthMin = 15.0D; ++ public double traderLlamaMaxHealthMax = 30.0D; ++ public double traderLlamaJumpStrengthMin = 0.5D; ++ public double traderLlamaJumpStrengthMax = 0.5D; ++ public double traderLlamaMovementSpeedMin = 0.175D; ++ public double traderLlamaMovementSpeedMax = 0.175D; ++ public int traderLlamaBreedingTicks = 6000; ++ public boolean traderLlamaTakeDamageFromWater = false; ++ public boolean traderLlamaAlwaysDropExp = false; ++ private void traderLlamaSettings() { ++ traderLlamaRidable = getBoolean("mobs.trader_llama.ridable", traderLlamaRidable); ++ traderLlamaRidableInWater = getBoolean("mobs.trader_llama.ridable-in-water", traderLlamaRidableInWater); ++ traderLlamaControllable = getBoolean("mobs.trader_llama.controllable", traderLlamaControllable); ++ if (PurpurConfig.version < 10) { ++ double oldMin = getDouble("mobs.trader_llama.attributes.max-health.min", traderLlamaMaxHealthMin); ++ double oldMax = getDouble("mobs.trader_llama.attributes.max-health.max", traderLlamaMaxHealthMax); ++ set("mobs.trader_llama.attributes.max-health", null); ++ set("mobs.trader_llama.attributes.max_health.min", oldMin); ++ set("mobs.trader_llama.attributes.max_health.max", oldMax); ++ } ++ traderLlamaMaxHealthMin = getDouble("mobs.trader_llama.attributes.max_health.min", traderLlamaMaxHealthMin); ++ traderLlamaMaxHealthMax = getDouble("mobs.trader_llama.attributes.max_health.max", traderLlamaMaxHealthMax); ++ traderLlamaJumpStrengthMin = getDouble("mobs.trader_llama.attributes.jump_strength.min", traderLlamaJumpStrengthMin); ++ traderLlamaJumpStrengthMax = getDouble("mobs.trader_llama.attributes.jump_strength.max", traderLlamaJumpStrengthMax); ++ traderLlamaMovementSpeedMin = getDouble("mobs.trader_llama.attributes.movement_speed.min", traderLlamaMovementSpeedMin); ++ traderLlamaMovementSpeedMax = getDouble("mobs.trader_llama.attributes.movement_speed.max", traderLlamaMovementSpeedMax); ++ traderLlamaBreedingTicks = getInt("mobs.trader_llama.breeding-delay-ticks", traderLlamaBreedingTicks); ++ traderLlamaTakeDamageFromWater = getBoolean("mobs.trader_llama.takes-damage-from-water", traderLlamaTakeDamageFromWater); ++ traderLlamaAlwaysDropExp = getBoolean("mobs.trader_llama.always-drop-exp", traderLlamaAlwaysDropExp); ++ } ++ ++ public boolean tropicalFishRidable = false; ++ public boolean tropicalFishControllable = true; ++ public double tropicalFishMaxHealth = 3.0D; ++ public boolean tropicalFishTakeDamageFromWater = false; ++ public boolean tropicalFishAlwaysDropExp = false; ++ private void tropicalFishSettings() { ++ tropicalFishRidable = getBoolean("mobs.tropical_fish.ridable", tropicalFishRidable); ++ tropicalFishControllable = getBoolean("mobs.tropical_fish.controllable", tropicalFishControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.tropical_fish.attributes.max-health", tropicalFishMaxHealth); ++ set("mobs.tropical_fish.attributes.max-health", null); ++ set("mobs.tropical_fish.attributes.max_health", oldValue); ++ } ++ tropicalFishMaxHealth = getDouble("mobs.tropical_fish.attributes.max_health", tropicalFishMaxHealth); ++ tropicalFishTakeDamageFromWater = getBoolean("mobs.tropical_fish.takes-damage-from-water", tropicalFishTakeDamageFromWater); ++ tropicalFishAlwaysDropExp = getBoolean("mobs.tropical_fish.always-drop-exp", tropicalFishAlwaysDropExp); ++ } ++ ++ public boolean turtleRidable = false; ++ public boolean turtleRidableInWater = true; ++ public boolean turtleControllable = true; ++ public double turtleMaxHealth = 30.0D; ++ public int turtleBreedingTicks = 6000; ++ public boolean turtleTakeDamageFromWater = false; ++ public boolean turtleAlwaysDropExp = false; ++ private void turtleSettings() { ++ turtleRidable = getBoolean("mobs.turtle.ridable", turtleRidable); ++ turtleRidableInWater = getBoolean("mobs.turtle.ridable-in-water", turtleRidableInWater); ++ turtleControllable = getBoolean("mobs.turtle.controllable", turtleControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.turtle.attributes.max-health", turtleMaxHealth); ++ set("mobs.turtle.attributes.max-health", null); ++ set("mobs.turtle.attributes.max_health", oldValue); ++ } ++ turtleMaxHealth = getDouble("mobs.turtle.attributes.max_health", turtleMaxHealth); ++ turtleBreedingTicks = getInt("mobs.turtle.breeding-delay-ticks", turtleBreedingTicks); ++ turtleTakeDamageFromWater = getBoolean("mobs.turtle.takes-damage-from-water", turtleTakeDamageFromWater); ++ turtleAlwaysDropExp = getBoolean("mobs.turtle.always-drop-exp", turtleAlwaysDropExp); ++ } ++ ++ public boolean vexRidable = false; ++ public boolean vexRidableInWater = true; ++ public boolean vexControllable = true; ++ public double vexMaxY = 320D; ++ public double vexMaxHealth = 14.0D; ++ public boolean vexTakeDamageFromWater = false; ++ public boolean vexAlwaysDropExp = false; ++ private void vexSettings() { ++ vexRidable = getBoolean("mobs.vex.ridable", vexRidable); ++ vexRidableInWater = getBoolean("mobs.vex.ridable-in-water", vexRidableInWater); ++ vexControllable = getBoolean("mobs.vex.controllable", vexControllable); ++ vexMaxY = getDouble("mobs.vex.ridable-max-y", vexMaxY); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.vex.attributes.max-health", vexMaxHealth); ++ set("mobs.vex.attributes.max-health", null); ++ set("mobs.vex.attributes.max_health", oldValue); ++ } ++ vexMaxHealth = getDouble("mobs.vex.attributes.max_health", vexMaxHealth); ++ vexTakeDamageFromWater = getBoolean("mobs.vex.takes-damage-from-water", vexTakeDamageFromWater); ++ vexAlwaysDropExp = getBoolean("mobs.vex.always-drop-exp", vexAlwaysDropExp); ++ } ++ ++ public boolean villagerRidable = false; ++ public boolean villagerRidableInWater = true; ++ public boolean villagerControllable = true; ++ public double villagerMaxHealth = 20.0D; ++ public boolean villagerFollowEmeraldBlock = false; ++ public boolean villagerCanBeLeashed = false; ++ public boolean villagerCanBreed = true; ++ public int villagerBreedingTicks = 6000; ++ public boolean villagerClericsFarmWarts = false; ++ public boolean villagerClericFarmersThrowWarts = true; ++ public boolean villagerBypassMobGriefing = false; ++ public boolean villagerTakeDamageFromWater = false; ++ public boolean villagerAllowTrading = true; ++ public boolean villagerAlwaysDropExp = false; ++ public int villagerMinimumDemand = 0; ++ public boolean villagerLobotomizeEnabled = false; ++ public int villagerLobotomizeCheckInterval = 100; ++ public boolean villagerLobotomizeWaitUntilTradeLocked = false; ++ public boolean villagerDisplayTradeItem = true; ++ public int villagerSpawnIronGolemRadius = 0; ++ public int villagerSpawnIronGolemLimit = 0; ++ public int villagerAcquirePoiSearchRadius = 48; ++ public int villagerNearestBedSensorSearchRadius = 48; ++ private void villagerSettings() { ++ villagerRidable = getBoolean("mobs.villager.ridable", villagerRidable); ++ villagerRidableInWater = getBoolean("mobs.villager.ridable-in-water", villagerRidableInWater); ++ villagerControllable = getBoolean("mobs.villager.controllable", villagerControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.villager.attributes.max-health", villagerMaxHealth); ++ set("mobs.villager.attributes.max-health", null); ++ set("mobs.villager.attributes.max_health", oldValue); ++ } ++ villagerMaxHealth = getDouble("mobs.villager.attributes.max_health", villagerMaxHealth); ++ villagerFollowEmeraldBlock = getBoolean("mobs.villager.follow-emerald-blocks", villagerFollowEmeraldBlock); ++ villagerCanBeLeashed = getBoolean("mobs.villager.can-be-leashed", villagerCanBeLeashed); ++ villagerCanBreed = getBoolean("mobs.villager.can-breed", villagerCanBreed); ++ villagerBreedingTicks = getInt("mobs.villager.breeding-delay-ticks", villagerBreedingTicks); ++ villagerClericsFarmWarts = getBoolean("mobs.villager.clerics-farm-warts", villagerClericsFarmWarts); ++ villagerClericFarmersThrowWarts = getBoolean("mobs.villager.cleric-wart-farmers-throw-warts-at-villagers", villagerClericFarmersThrowWarts); ++ villagerBypassMobGriefing = getBoolean("mobs.villager.bypass-mob-griefing", villagerBypassMobGriefing); ++ villagerTakeDamageFromWater = getBoolean("mobs.villager.takes-damage-from-water", villagerTakeDamageFromWater); ++ villagerAllowTrading = getBoolean("mobs.villager.allow-trading", villagerAllowTrading); ++ villagerAlwaysDropExp = getBoolean("mobs.villager.always-drop-exp", villagerAlwaysDropExp); ++ villagerMinimumDemand = getInt("mobs.villager.minimum-demand", villagerMinimumDemand); ++ if (PurpurConfig.version < 9) { ++ boolean oldValue = getBoolean("mobs.villager.lobotomize-1x1", villagerLobotomizeEnabled); ++ set("mobs.villager.lobotomize.enabled", oldValue); ++ set("mobs.villager.lobotomize-1x1", null); ++ } ++ if (PurpurConfig.version < 27) { ++ int oldValue = getInt("mobs.villager.lobotomize.check-interval", villagerLobotomizeCheckInterval); ++ set("mobs.villager.lobotomize.check-interval", oldValue == 60 ? 100 : oldValue); ++ } ++ villagerLobotomizeEnabled = getBoolean("mobs.villager.lobotomize.enabled", villagerLobotomizeEnabled); ++ villagerLobotomizeCheckInterval = getInt("mobs.villager.lobotomize.check-interval", villagerLobotomizeCheckInterval); ++ villagerLobotomizeWaitUntilTradeLocked = getBoolean("mobs.villager.lobotomize.wait-until-trade-locked", villagerLobotomizeWaitUntilTradeLocked); ++ villagerDisplayTradeItem = getBoolean("mobs.villager.display-trade-item", villagerDisplayTradeItem); ++ villagerSpawnIronGolemRadius = getInt("mobs.villager.spawn-iron-golem.radius", villagerSpawnIronGolemRadius); ++ villagerSpawnIronGolemLimit = getInt("mobs.villager.spawn-iron-golem.limit", villagerSpawnIronGolemLimit); ++ villagerAcquirePoiSearchRadius = getInt("mobs.villager.search-radius.acquire-poi", villagerAcquirePoiSearchRadius); ++ villagerNearestBedSensorSearchRadius = getInt("mobs.villager.search-radius.nearest-bed-sensor", villagerNearestBedSensorSearchRadius); ++ } ++ ++ public boolean vindicatorRidable = false; ++ public boolean vindicatorRidableInWater = true; ++ public boolean vindicatorControllable = true; ++ public double vindicatorMaxHealth = 24.0D; ++ public double vindicatorJohnnySpawnChance = 0D; ++ public boolean vindicatorTakeDamageFromWater = false; ++ public boolean vindicatorAlwaysDropExp = false; ++ private void vindicatorSettings() { ++ vindicatorRidable = getBoolean("mobs.vindicator.ridable", vindicatorRidable); ++ vindicatorRidableInWater = getBoolean("mobs.vindicator.ridable-in-water", vindicatorRidableInWater); ++ vindicatorControllable = getBoolean("mobs.vindicator.controllable", vindicatorControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.vindicator.attributes.max-health", vindicatorMaxHealth); ++ set("mobs.vindicator.attributes.max-health", null); ++ set("mobs.vindicator.attributes.max_health", oldValue); ++ } ++ vindicatorMaxHealth = getDouble("mobs.vindicator.attributes.max_health", vindicatorMaxHealth); ++ vindicatorJohnnySpawnChance = getDouble("mobs.vindicator.johnny.spawn-chance", vindicatorJohnnySpawnChance); ++ vindicatorTakeDamageFromWater = getBoolean("mobs.vindicator.takes-damage-from-water", vindicatorTakeDamageFromWater); ++ vindicatorAlwaysDropExp = getBoolean("mobs.vindicator.always-drop-exp", vindicatorAlwaysDropExp); ++ } ++ ++ public boolean wanderingTraderRidable = false; ++ public boolean wanderingTraderRidableInWater = true; ++ public boolean wanderingTraderControllable = true; ++ public double wanderingTraderMaxHealth = 20.0D; ++ public boolean wanderingTraderFollowEmeraldBlock = false; ++ public boolean wanderingTraderCanBeLeashed = false; ++ public boolean wanderingTraderTakeDamageFromWater = false; ++ public boolean wanderingTraderAllowTrading = true; ++ public boolean wanderingTraderAlwaysDropExp = false; ++ private void wanderingTraderSettings() { ++ wanderingTraderRidable = getBoolean("mobs.wandering_trader.ridable", wanderingTraderRidable); ++ wanderingTraderRidableInWater = getBoolean("mobs.wandering_trader.ridable-in-water", wanderingTraderRidableInWater); ++ wanderingTraderControllable = getBoolean("mobs.wandering_trader.controllable", wanderingTraderControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.wandering_trader.attributes.max-health", wanderingTraderMaxHealth); ++ set("mobs.wandering_trader.attributes.max-health", null); ++ set("mobs.wandering_trader.attributes.max_health", oldValue); ++ } ++ wanderingTraderMaxHealth = getDouble("mobs.wandering_trader.attributes.max_health", wanderingTraderMaxHealth); ++ wanderingTraderFollowEmeraldBlock = getBoolean("mobs.wandering_trader.follow-emerald-blocks", wanderingTraderFollowEmeraldBlock); ++ wanderingTraderCanBeLeashed = getBoolean("mobs.wandering_trader.can-be-leashed", wanderingTraderCanBeLeashed); ++ wanderingTraderTakeDamageFromWater = getBoolean("mobs.wandering_trader.takes-damage-from-water", wanderingTraderTakeDamageFromWater); ++ wanderingTraderAllowTrading = getBoolean("mobs.wandering_trader.allow-trading", wanderingTraderAllowTrading); ++ wanderingTraderAlwaysDropExp = getBoolean("mobs.wandering_trader.always-drop-exp", wanderingTraderAlwaysDropExp); ++ } ++ ++ public boolean wardenRidable = false; ++ public boolean wardenRidableInWater = true; ++ public boolean wardenControllable = true; ++ private void wardenSettings() { ++ wardenRidable = getBoolean("mobs.warden.ridable", wardenRidable); ++ wardenRidableInWater = getBoolean("mobs.warden.ridable-in-water", wardenRidableInWater); ++ wardenControllable = getBoolean("mobs.warden.controllable", wardenControllable); ++ } ++ ++ public boolean witchRidable = false; ++ public boolean witchRidableInWater = true; ++ public boolean witchControllable = true; ++ public double witchMaxHealth = 26.0D; ++ public boolean witchTakeDamageFromWater = false; ++ public boolean witchAlwaysDropExp = false; ++ private void witchSettings() { ++ witchRidable = getBoolean("mobs.witch.ridable", witchRidable); ++ witchRidableInWater = getBoolean("mobs.witch.ridable-in-water", witchRidableInWater); ++ witchControllable = getBoolean("mobs.witch.controllable", witchControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.witch.attributes.max-health", witchMaxHealth); ++ set("mobs.witch.attributes.max-health", null); ++ set("mobs.witch.attributes.max_health", oldValue); ++ } ++ witchMaxHealth = getDouble("mobs.witch.attributes.max_health", witchMaxHealth); ++ witchTakeDamageFromWater = getBoolean("mobs.witch.takes-damage-from-water", witchTakeDamageFromWater); ++ witchAlwaysDropExp = getBoolean("mobs.witch.always-drop-exp", witchAlwaysDropExp); ++ } ++ ++ public boolean witherRidable = false; ++ public boolean witherRidableInWater = true; ++ public boolean witherControllable = true; ++ public double witherMaxY = 320D; ++ public double witherMaxHealth = 300.0D; ++ public float witherHealthRegenAmount = 1.0f; ++ public int witherHealthRegenDelay = 20; ++ public boolean witherBypassMobGriefing = false; ++ public boolean witherTakeDamageFromWater = false; ++ public boolean witherCanRideVehicles = false; ++ public float witherExplosionRadius = 1.0F; ++ public boolean witherPlaySpawnSound = true; ++ public boolean witherAlwaysDropExp = false; ++ private void witherSettings() { ++ witherRidable = getBoolean("mobs.wither.ridable", witherRidable); ++ witherRidableInWater = getBoolean("mobs.wither.ridable-in-water", witherRidableInWater); ++ witherControllable = getBoolean("mobs.wither.controllable", witherControllable); ++ witherMaxY = getDouble("mobs.wither.ridable-max-y", witherMaxY); ++ if (PurpurConfig.version < 8) { ++ double oldValue = getDouble("mobs.wither.max-health", witherMaxHealth); ++ set("mobs.wither.max_health", null); ++ set("mobs.wither.attributes.max-health", oldValue); ++ } else if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.wither.attributes.max-health", witherMaxHealth); ++ set("mobs.wither.attributes.max-health", null); ++ set("mobs.wither.attributes.max_health", oldValue); ++ } ++ witherMaxHealth = getDouble("mobs.wither.attributes.max_health", witherMaxHealth); ++ witherHealthRegenAmount = (float) getDouble("mobs.wither.health-regen-amount", witherHealthRegenAmount); ++ witherHealthRegenDelay = getInt("mobs.wither.health-regen-delay", witherHealthRegenDelay); ++ witherBypassMobGriefing = getBoolean("mobs.wither.bypass-mob-griefing", witherBypassMobGriefing); ++ witherTakeDamageFromWater = getBoolean("mobs.wither.takes-damage-from-water", witherTakeDamageFromWater); ++ witherCanRideVehicles = getBoolean("mobs.wither.can-ride-vehicles", witherCanRideVehicles); ++ witherExplosionRadius = (float) getDouble("mobs.wither.explosion-radius", witherExplosionRadius); ++ witherPlaySpawnSound = getBoolean("mobs.wither.play-spawn-sound", witherPlaySpawnSound); ++ witherAlwaysDropExp = getBoolean("mobs.wither.always-drop-exp", witherAlwaysDropExp); ++ } ++ ++ public boolean witherSkeletonRidable = false; ++ public boolean witherSkeletonRidableInWater = true; ++ public boolean witherSkeletonControllable = true; ++ public double witherSkeletonMaxHealth = 20.0D; ++ public boolean witherSkeletonTakeDamageFromWater = false; ++ public boolean witherSkeletonAlwaysDropExp = false; ++ private void witherSkeletonSettings() { ++ witherSkeletonRidable = getBoolean("mobs.wither_skeleton.ridable", witherSkeletonRidable); ++ witherSkeletonRidableInWater = getBoolean("mobs.wither_skeleton.ridable-in-water", witherSkeletonRidableInWater); ++ witherSkeletonControllable = getBoolean("mobs.wither_skeleton.controllable", witherSkeletonControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.wither_skeleton.attributes.max-health", witherSkeletonMaxHealth); ++ set("mobs.wither_skeleton.attributes.max-health", null); ++ set("mobs.wither_skeleton.attributes.max_health", oldValue); ++ } ++ witherSkeletonMaxHealth = getDouble("mobs.wither_skeleton.attributes.max_health", witherSkeletonMaxHealth); ++ witherSkeletonTakeDamageFromWater = getBoolean("mobs.wither_skeleton.takes-damage-from-water", witherSkeletonTakeDamageFromWater); ++ witherSkeletonAlwaysDropExp = getBoolean("mobs.wither_skeleton.always-drop-exp", witherSkeletonAlwaysDropExp); ++ } ++ ++ public boolean wolfRidable = false; ++ public boolean wolfRidableInWater = true; ++ public boolean wolfControllable = true; ++ public double wolfMaxHealth = 8.0D; ++ public DyeColor wolfDefaultCollarColor = DyeColor.RED; ++ public boolean wolfMilkCuresRabies = true; ++ public double wolfNaturalRabid = 0.0D; ++ public int wolfBreedingTicks = 6000; ++ public boolean wolfTakeDamageFromWater = false; ++ public boolean wolfAlwaysDropExp = false; ++ private void wolfSettings() { ++ wolfRidable = getBoolean("mobs.wolf.ridable", wolfRidable); ++ wolfRidableInWater = getBoolean("mobs.wolf.ridable-in-water", wolfRidableInWater); ++ wolfControllable = getBoolean("mobs.wolf.controllable", wolfControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.wolf.attributes.max-health", wolfMaxHealth); ++ set("mobs.wolf.attributes.max-health", null); ++ set("mobs.wolf.attributes.max_health", oldValue); ++ } ++ wolfMaxHealth = getDouble("mobs.wolf.attributes.max_health", wolfMaxHealth); ++ try { ++ wolfDefaultCollarColor = DyeColor.valueOf(getString("mobs.wolf.default-collar-color", wolfDefaultCollarColor.name())); ++ } catch (IllegalArgumentException ignore) { ++ wolfDefaultCollarColor = DyeColor.RED; ++ } ++ wolfMilkCuresRabies = getBoolean("mobs.wolf.milk-cures-rabid-wolves", wolfMilkCuresRabies); ++ wolfNaturalRabid = getDouble("mobs.wolf.spawn-rabid-chance", wolfNaturalRabid); ++ wolfBreedingTicks = getInt("mobs.wolf.breeding-delay-ticks", wolfBreedingTicks); ++ wolfTakeDamageFromWater = getBoolean("mobs.wolf.takes-damage-from-water", wolfTakeDamageFromWater); ++ wolfAlwaysDropExp = getBoolean("mobs.wolf.always-drop-exp", wolfAlwaysDropExp); ++ } ++ ++ public boolean zoglinRidable = false; ++ public boolean zoglinRidableInWater = true; ++ public boolean zoglinControllable = true; ++ public double zoglinMaxHealth = 40.0D; ++ public boolean zoglinTakeDamageFromWater = false; ++ public boolean zoglinAlwaysDropExp = false; ++ private void zoglinSettings() { ++ zoglinRidable = getBoolean("mobs.zoglin.ridable", zoglinRidable); ++ zoglinRidableInWater = getBoolean("mobs.zoglin.ridable-in-water", zoglinRidableInWater); ++ zoglinControllable = getBoolean("mobs.zoglin.controllable", zoglinControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.zoglin.attributes.max-health", zoglinMaxHealth); ++ set("mobs.zoglin.attributes.max-health", null); ++ set("mobs.zoglin.attributes.max_health", oldValue); ++ } ++ zoglinMaxHealth = getDouble("mobs.zoglin.attributes.max_health", zoglinMaxHealth); ++ zoglinTakeDamageFromWater = getBoolean("mobs.zoglin.takes-damage-from-water", zoglinTakeDamageFromWater); ++ zoglinAlwaysDropExp = getBoolean("mobs.zoglin.always-drop-exp", zoglinAlwaysDropExp); ++ } ++ ++ public boolean zombieRidable = false; ++ public boolean zombieRidableInWater = true; ++ public boolean zombieControllable = true; ++ public double zombieMaxHealth = 20.0D; ++ public double zombieSpawnReinforcements = 0.1D; ++ public boolean zombieJockeyOnlyBaby = true; ++ public double zombieJockeyChance = 0.05D; ++ public boolean zombieJockeyTryExistingChickens = true; ++ public boolean zombieAggressiveTowardsVillagerWhenLagging = true; ++ public boolean zombieBypassMobGriefing = false; ++ public boolean zombieTakeDamageFromWater = false; ++ public boolean zombieAlwaysDropExp = false; ++ public double zombieHeadVisibilityPercent = 0.5D; ++ private void zombieSettings() { ++ zombieRidable = getBoolean("mobs.zombie.ridable", zombieRidable); ++ zombieRidableInWater = getBoolean("mobs.zombie.ridable-in-water", zombieRidableInWater); ++ zombieControllable = getBoolean("mobs.zombie.controllable", zombieControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.zombie.attributes.max-health", zombieMaxHealth); ++ set("mobs.zombie.attributes.max-health", null); ++ set("mobs.zombie.attributes.max_health", oldValue); ++ } ++ zombieMaxHealth = getDouble("mobs.zombie.attributes.max_health", zombieMaxHealth); ++ zombieSpawnReinforcements = getDouble("mobs.zombie.attributes.spawn_reinforcements", zombieSpawnReinforcements); ++ zombieJockeyOnlyBaby = getBoolean("mobs.zombie.jockey.only-babies", zombieJockeyOnlyBaby); ++ zombieJockeyChance = getDouble("mobs.zombie.jockey.chance", zombieJockeyChance); ++ zombieJockeyTryExistingChickens = getBoolean("mobs.zombie.jockey.try-existing-chickens", zombieJockeyTryExistingChickens); ++ zombieAggressiveTowardsVillagerWhenLagging = getBoolean("mobs.zombie.aggressive-towards-villager-when-lagging", zombieAggressiveTowardsVillagerWhenLagging); ++ zombieBypassMobGriefing = getBoolean("mobs.zombie.bypass-mob-griefing", zombieBypassMobGriefing); ++ zombieTakeDamageFromWater = getBoolean("mobs.zombie.takes-damage-from-water", zombieTakeDamageFromWater); ++ zombieAlwaysDropExp = getBoolean("mobs.zombie.always-drop-exp", zombieAlwaysDropExp); ++ zombieHeadVisibilityPercent = getDouble("mobs.zombie.head-visibility-percent", zombieHeadVisibilityPercent); ++ } ++ ++ public boolean zombieHorseRidable = false; ++ public boolean zombieHorseRidableInWater = false; ++ public boolean zombieHorseCanSwim = false; ++ public double zombieHorseMaxHealthMin = 15.0D; ++ public double zombieHorseMaxHealthMax = 15.0D; ++ public double zombieHorseJumpStrengthMin = 0.4D; ++ public double zombieHorseJumpStrengthMax = 1.0D; ++ public double zombieHorseMovementSpeedMin = 0.2D; ++ public double zombieHorseMovementSpeedMax = 0.2D; ++ public double zombieHorseSpawnChance = 0.0D; ++ public boolean zombieHorseTakeDamageFromWater = false; ++ public boolean zombieHorseAlwaysDropExp = false; ++ private void zombieHorseSettings() { ++ zombieHorseRidable = getBoolean("mobs.zombie_horse.ridable", zombieHorseRidable); ++ zombieHorseRidableInWater = getBoolean("mobs.zombie_horse.ridable-in-water", zombieHorseRidableInWater); ++ zombieHorseCanSwim = getBoolean("mobs.zombie_horse.can-swim", zombieHorseCanSwim); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.zombie_horse.attributes.max-health", zombieHorseMaxHealthMin); ++ set("mobs.zombie_horse.attributes.max-health", null); ++ set("mobs.zombie_horse.attributes.max_health.min", oldValue); ++ set("mobs.zombie_horse.attributes.max_health.max", oldValue); ++ } ++ zombieHorseMaxHealthMin = getDouble("mobs.zombie_horse.attributes.max_health.min", zombieHorseMaxHealthMin); ++ zombieHorseMaxHealthMax = getDouble("mobs.zombie_horse.attributes.max_health.max", zombieHorseMaxHealthMax); ++ zombieHorseJumpStrengthMin = getDouble("mobs.zombie_horse.attributes.jump_strength.min", zombieHorseJumpStrengthMin); ++ zombieHorseJumpStrengthMax = getDouble("mobs.zombie_horse.attributes.jump_strength.max", zombieHorseJumpStrengthMax); ++ zombieHorseMovementSpeedMin = getDouble("mobs.zombie_horse.attributes.movement_speed.min", zombieHorseMovementSpeedMin); ++ zombieHorseMovementSpeedMax = getDouble("mobs.zombie_horse.attributes.movement_speed.max", zombieHorseMovementSpeedMax); ++ zombieHorseSpawnChance = getDouble("mobs.zombie_horse.spawn-chance", zombieHorseSpawnChance); ++ zombieHorseTakeDamageFromWater = getBoolean("mobs.zombie_horse.takes-damage-from-water", zombieHorseTakeDamageFromWater); ++ zombieHorseAlwaysDropExp = getBoolean("mobs.zombie_horse.always-drop-exp", zombieHorseAlwaysDropExp); ++ } ++ ++ public boolean zombieVillagerRidable = false; ++ public boolean zombieVillagerRidableInWater = true; ++ public boolean zombieVillagerControllable = true; ++ public double zombieVillagerMaxHealth = 20.0D; ++ public double zombieVillagerSpawnReinforcements = 0.1D; ++ public boolean zombieVillagerJockeyOnlyBaby = true; ++ public double zombieVillagerJockeyChance = 0.05D; ++ public boolean zombieVillagerJockeyTryExistingChickens = true; ++ public boolean zombieVillagerTakeDamageFromWater = false; ++ public int zombieVillagerCuringTimeMin = 3600; ++ public int zombieVillagerCuringTimeMax = 6000; ++ public boolean zombieVillagerCureEnabled = true; ++ public boolean zombieVillagerAlwaysDropExp = false; ++ private void zombieVillagerSettings() { ++ zombieVillagerRidable = getBoolean("mobs.zombie_villager.ridable", zombieVillagerRidable); ++ zombieVillagerRidableInWater = getBoolean("mobs.zombie_villager.ridable-in-water", zombieVillagerRidableInWater); ++ zombieVillagerControllable = getBoolean("mobs.zombie_villager.controllable", zombieVillagerControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.zombie_villager.attributes.max-health", zombieVillagerMaxHealth); ++ set("mobs.zombie_villager.attributes.max-health", null); ++ set("mobs.zombie_villager.attributes.max_health", oldValue); ++ } ++ zombieVillagerMaxHealth = getDouble("mobs.zombie_villager.attributes.max_health", zombieVillagerMaxHealth); ++ zombieVillagerSpawnReinforcements = getDouble("mobs.zombie_villager.attributes.spawn_reinforcements", zombieVillagerSpawnReinforcements); ++ zombieVillagerJockeyOnlyBaby = getBoolean("mobs.zombie_villager.jockey.only-babies", zombieVillagerJockeyOnlyBaby); ++ zombieVillagerJockeyChance = getDouble("mobs.zombie_villager.jockey.chance", zombieVillagerJockeyChance); ++ zombieVillagerJockeyTryExistingChickens = getBoolean("mobs.zombie_villager.jockey.try-existing-chickens", zombieVillagerJockeyTryExistingChickens); ++ zombieVillagerTakeDamageFromWater = getBoolean("mobs.zombie_villager.takes-damage-from-water", zombieVillagerTakeDamageFromWater); ++ zombieVillagerCuringTimeMin = getInt("mobs.zombie_villager.curing_time.min", zombieVillagerCuringTimeMin); ++ zombieVillagerCuringTimeMax = getInt("mobs.zombie_villager.curing_time.max", zombieVillagerCuringTimeMax); ++ zombieVillagerCureEnabled = getBoolean("mobs.zombie_villager.cure.enabled", zombieVillagerCureEnabled); ++ zombieVillagerAlwaysDropExp = getBoolean("mobs.zombie_villager.always-drop-exp", zombieVillagerAlwaysDropExp); ++ } ++ ++ public boolean zombifiedPiglinRidable = false; ++ public boolean zombifiedPiglinRidableInWater = true; ++ public boolean zombifiedPiglinControllable = true; ++ public double zombifiedPiglinMaxHealth = 20.0D; ++ public double zombifiedPiglinSpawnReinforcements = 0.0D; ++ public boolean zombifiedPiglinJockeyOnlyBaby = true; ++ public double zombifiedPiglinJockeyChance = 0.05D; ++ public boolean zombifiedPiglinJockeyTryExistingChickens = true; ++ public boolean zombifiedPiglinCountAsPlayerKillWhenAngry = true; ++ public boolean zombifiedPiglinTakeDamageFromWater = false; ++ public boolean zombifiedPiglinAlwaysDropExp = false; ++ private void zombifiedPiglinSettings() { ++ zombifiedPiglinRidable = getBoolean("mobs.zombified_piglin.ridable", zombifiedPiglinRidable); ++ zombifiedPiglinRidableInWater = getBoolean("mobs.zombified_piglin.ridable-in-water", zombifiedPiglinRidableInWater); ++ zombifiedPiglinControllable = getBoolean("mobs.zombified_piglin.controllable", zombifiedPiglinControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.zombified_piglin.attributes.max-health", zombifiedPiglinMaxHealth); ++ set("mobs.zombified_piglin.attributes.max-health", null); ++ set("mobs.zombified_piglin.attributes.max_health", oldValue); ++ } ++ zombifiedPiglinMaxHealth = getDouble("mobs.zombified_piglin.attributes.max_health", zombifiedPiglinMaxHealth); ++ zombifiedPiglinSpawnReinforcements = getDouble("mobs.zombified_piglin.attributes.spawn_reinforcements", zombifiedPiglinSpawnReinforcements); ++ zombifiedPiglinJockeyOnlyBaby = getBoolean("mobs.zombified_piglin.jockey.only-babies", zombifiedPiglinJockeyOnlyBaby); ++ zombifiedPiglinJockeyChance = getDouble("mobs.zombified_piglin.jockey.chance", zombifiedPiglinJockeyChance); ++ zombifiedPiglinJockeyTryExistingChickens = getBoolean("mobs.zombified_piglin.jockey.try-existing-chickens", zombifiedPiglinJockeyTryExistingChickens); ++ zombifiedPiglinCountAsPlayerKillWhenAngry = getBoolean("mobs.zombified_piglin.count-as-player-kill-when-angry", zombifiedPiglinCountAsPlayerKillWhenAngry); ++ zombifiedPiglinTakeDamageFromWater = getBoolean("mobs.zombified_piglin.takes-damage-from-water", zombifiedPiglinTakeDamageFromWater); ++ zombifiedPiglinAlwaysDropExp = getBoolean("mobs.zombified_piglin.always-drop-exp", zombifiedPiglinAlwaysDropExp); ++ } ++ ++ public float hungerStarvationDamage = 1.0F; ++ private void hungerSettings() { ++ hungerStarvationDamage = (float) getDouble("hunger.starvation-damage", hungerStarvationDamage); ++ } ++ ++ public int conduitDistance = 16; ++ public double conduitDamageDistance = 8; ++ public float conduitDamageAmount = 4; ++ public Block[] conduitBlocks; ++ private void conduitSettings() { ++ conduitDistance = getInt("blocks.conduit.effect-distance", conduitDistance); ++ conduitDamageDistance = getDouble("blocks.conduit.mob-damage.distance", conduitDamageDistance); ++ conduitDamageAmount = (float) getDouble("blocks.conduit.mob-damage.damage-amount", conduitDamageAmount); ++ List conduitBlockList = new ArrayList<>(); ++ getList("blocks.conduit.valid-ring-blocks", new ArrayList(){{ ++ add("minecraft:prismarine"); ++ add("minecraft:prismarine_bricks"); ++ add("minecraft:sea_lantern"); ++ add("minecraft:dark_prismarine"); ++ }}).forEach(key -> { ++ Block block = BuiltInRegistries.BLOCK.get(new ResourceLocation(key.toString())); ++ if (!block.defaultBlockState().isAir()) { ++ conduitBlockList.add(block); ++ } ++ }); ++ conduitBlocks = conduitBlockList.toArray(Block[]::new); ++ } ++ ++ public float cauldronRainChance = 0.05F; ++ public float cauldronPowderSnowChance = 0.1F; ++ public float cauldronDripstoneWaterFillChance = 0.17578125F; ++ public float cauldronDripstoneLavaFillChance = 0.05859375F; ++ private void cauldronSettings() { ++ cauldronRainChance = (float) getDouble("blocks.cauldron.fill-chances.rain", cauldronRainChance); ++ cauldronPowderSnowChance = (float) getDouble("blocks.cauldron.fill-chances.powder-snow", cauldronPowderSnowChance); ++ cauldronDripstoneWaterFillChance = (float) getDouble("blocks.cauldron.fill-chances.dripstone-water", cauldronDripstoneWaterFillChance); ++ cauldronDripstoneLavaFillChance = (float) getDouble("blocks.cauldron.fill-chances.dripstone-lava", cauldronDripstoneLavaFillChance); ++ } ++ ++ public float shearsCanDefuseTntChance = 0.00F; ++ public boolean shearsCanDefuseTnt = false; ++ private void shearsCanDefuseTntSettings() { ++ shearsCanDefuseTntChance = (float) getDouble("gameplay-mechanics.item.shears.defuse-tnt-chance", 0.00D); ++ shearsCanDefuseTnt = shearsCanDefuseTntChance > 0.00F; ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/command/CompassCommand.java b/src/main/java/org/purpurmc/purpur/command/CompassCommand.java +new file mode 100644 +index 0000000000000000000000000000000000000000..79b8490832d2a0cc7846ddcb091cb6bcac74ea45 +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/command/CompassCommand.java +@@ -0,0 +1,27 @@ ++package org.purpurmc.purpur.command; ++ ++import com.mojang.brigadier.CommandDispatcher; ++import net.minecraft.commands.CommandSourceStack; ++import net.minecraft.commands.Commands; ++import net.minecraft.server.level.ServerPlayer; ++import org.purpurmc.purpur.task.CompassTask; ++ ++public class CompassCommand { ++ public static void register(CommandDispatcher dispatcher) { ++ dispatcher.register(Commands.literal("compass") ++ .requires(listener -> listener.hasPermission(2, "bukkit.command.compass")) ++ .executes(context -> { ++ ServerPlayer player = context.getSource().getPlayerOrException(); ++ CompassTask task = CompassTask.instance(); ++ if (player.compassBar()) { ++ task.removePlayer(player.getBukkitEntity()); ++ player.compassBar(false); ++ } else { ++ task.addPlayer(player.getBukkitEntity()); ++ player.compassBar(true); ++ } ++ return 1; ++ }) ++ ); ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/command/CreditsCommand.java b/src/main/java/org/purpurmc/purpur/command/CreditsCommand.java +new file mode 100644 +index 0000000000000000000000000000000000000000..40d2fab4a9728ac90c36e30c130f3116b7025d11 +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/command/CreditsCommand.java +@@ -0,0 +1,35 @@ ++package org.purpurmc.purpur.command; ++ ++import com.mojang.brigadier.CommandDispatcher; ++import net.minecraft.commands.CommandSourceStack; ++import net.minecraft.commands.Commands; ++import net.minecraft.commands.arguments.EntityArgument; ++import net.minecraft.network.protocol.game.ClientboundGameEventPacket; ++import net.minecraft.server.level.ServerPlayer; ++import org.purpurmc.purpur.PurpurConfig; ++ ++import java.util.Collection; ++import java.util.Collections; ++ ++public class CreditsCommand { ++ public static void register(CommandDispatcher dispatcher) { ++ dispatcher.register(Commands.literal("credits") ++ .requires((listener) -> listener.hasPermission(2, "bukkit.command.credits")) ++ .executes((context) -> execute(context.getSource(), Collections.singleton(context.getSource().getPlayerOrException()))) ++ .then(Commands.argument("targets", EntityArgument.players()) ++ .requires(listener -> listener.hasPermission(2, "bukkit.command.credits.other")) ++ .executes((context) -> execute(context.getSource(), EntityArgument.getPlayers(context, "targets"))) ++ ) ++ ); ++ } ++ ++ private static int execute(CommandSourceStack sender, Collection targets) { ++ for (ServerPlayer player : targets) { ++ ClientboundGameEventPacket packet = new ClientboundGameEventPacket(ClientboundGameEventPacket.WIN_GAME, 1F); ++ player.connection.send(packet); ++ String output = String.format(PurpurConfig.creditsCommandOutput, player.getGameProfile().getName()); ++ sender.sendSuccess(output, false); ++ } ++ return targets.size(); ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/command/DemoCommand.java b/src/main/java/org/purpurmc/purpur/command/DemoCommand.java +new file mode 100644 +index 0000000000000000000000000000000000000000..235f3cd89f675b70a6152a00534608c0902f19fd +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/command/DemoCommand.java +@@ -0,0 +1,35 @@ ++package org.purpurmc.purpur.command; ++ ++import com.mojang.brigadier.CommandDispatcher; ++import net.minecraft.commands.CommandSourceStack; ++import net.minecraft.commands.Commands; ++import net.minecraft.commands.arguments.EntityArgument; ++import net.minecraft.network.protocol.game.ClientboundGameEventPacket; ++import net.minecraft.server.level.ServerPlayer; ++import org.purpurmc.purpur.PurpurConfig; ++ ++import java.util.Collection; ++import java.util.Collections; ++ ++public class DemoCommand { ++ public static void register(CommandDispatcher dispatcher) { ++ dispatcher.register(Commands.literal("demo") ++ .requires((listener) -> listener.hasPermission(2, "bukkit.command.demo")) ++ .executes((context) -> execute(context.getSource(), Collections.singleton(context.getSource().getPlayerOrException()))) ++ .then(Commands.argument("targets", EntityArgument.players()) ++ .requires(listener -> listener.hasPermission(2, "bukkit.command.demo.other")) ++ .executes((context) -> execute(context.getSource(), EntityArgument.getPlayers(context, "targets"))) ++ ) ++ ); ++ } ++ ++ private static int execute(CommandSourceStack sender, Collection targets) { ++ for (ServerPlayer player : targets) { ++ ClientboundGameEventPacket packet = new ClientboundGameEventPacket(ClientboundGameEventPacket.DEMO_EVENT, 0); ++ player.connection.send(packet); ++ String output = String.format(PurpurConfig.demoCommandOutput, player.getGameProfile().getName()); ++ sender.sendSuccess(output, false); ++ } ++ return targets.size(); ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/command/PingCommand.java b/src/main/java/org/purpurmc/purpur/command/PingCommand.java +new file mode 100644 +index 0000000000000000000000000000000000000000..f202b98a194604e39798fdb8e417c6d2835f71c8 +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/command/PingCommand.java +@@ -0,0 +1,33 @@ ++package org.purpurmc.purpur.command; ++ ++import com.mojang.brigadier.CommandDispatcher; ++import net.minecraft.commands.CommandSourceStack; ++import net.minecraft.commands.Commands; ++import net.minecraft.commands.arguments.EntityArgument; ++import net.minecraft.server.level.ServerPlayer; ++import org.purpurmc.purpur.PurpurConfig; ++import org.bukkit.craftbukkit.util.CraftChatMessage; ++ ++import java.util.Collection; ++import java.util.Collections; ++ ++public class PingCommand { ++ public static void register(CommandDispatcher dispatcher) { ++ dispatcher.register(Commands.literal("ping") ++ .requires((listener) -> listener.hasPermission(2, "bukkit.command.ping")) ++ .executes((context) -> execute(context.getSource(), Collections.singleton(context.getSource().getPlayerOrException()))) ++ .then(Commands.argument("targets", EntityArgument.players()) ++ .requires(listener -> listener.hasPermission(2, "bukkit.command.ping.other")) ++ .executes((context) -> execute(context.getSource(), EntityArgument.getPlayers(context, "targets"))) ++ ) ++ ); ++ } ++ ++ private static int execute(CommandSourceStack sender, Collection targets) { ++ for (ServerPlayer player : targets) { ++ String output = String.format(PurpurConfig.pingCommandOutput, player.getGameProfile().getName(), player.connection.latency()); ++ sender.sendSuccess(output, false); ++ } ++ return targets.size(); ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/command/PurpurCommand.java b/src/main/java/org/purpurmc/purpur/command/PurpurCommand.java +new file mode 100644 +index 0000000000000000000000000000000000000000..2621e54879e9ab0029a875f1d09eee67878b90d5 +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/command/PurpurCommand.java +@@ -0,0 +1,66 @@ ++package org.purpurmc.purpur.command; ++ ++import net.minecraft.server.MinecraftServer; ++import net.minecraft.server.level.ServerLevel; ++import org.purpurmc.purpur.PurpurConfig; ++import org.bukkit.ChatColor; ++import org.bukkit.Location; ++import org.bukkit.command.Command; ++import org.bukkit.command.CommandSender; ++ ++import java.io.File; ++import java.util.Collections; ++import java.util.List; ++import java.util.stream.Collectors; ++import java.util.stream.Stream; ++ ++public class PurpurCommand extends Command { ++ public PurpurCommand(String name) { ++ super(name); ++ this.description = "Purpur related commands"; ++ this.usageMessage = "/purpur [reload | version]"; ++ this.setPermission("bukkit.command.purpur"); ++ } ++ ++ @Override ++ public List tabComplete(CommandSender sender, String alias, String[] args, Location location) throws IllegalArgumentException { ++ if (args.length == 1) { ++ return Stream.of("reload", "version") ++ .filter(arg -> arg.startsWith(args[0].toLowerCase())) ++ .collect(Collectors.toList()); ++ } ++ return Collections.emptyList(); ++ } ++ ++ @Override ++ public boolean execute(CommandSender sender, String commandLabel, String[] args) { ++ if (!testPermission(sender)) return true; ++ ++ if (args.length != 1) { ++ sender.sendMessage(ChatColor.RED + "Usage: " + usageMessage); ++ return false; ++ } ++ ++ if (args[0].equalsIgnoreCase("reload")) { ++ Command.broadcastCommandMessage(sender, ChatColor.RED + "Please note that this command is not supported and may cause issues."); ++ Command.broadcastCommandMessage(sender, ChatColor.RED + "If you encounter any issues please use the /stop command to restart your server."); ++ ++ MinecraftServer console = MinecraftServer.getServer(); ++ PurpurConfig.init((File) console.options.valueOf("purpur-settings")); ++ for (ServerLevel level : console.getAllLevels()) { ++ level.purpurConfig.init(); ++ level.resetBreedingCooldowns(); ++ } ++ console.server.reloadCount++; ++ ++ Command.broadcastCommandMessage(sender, ChatColor.GREEN + "Purpur config reload complete."); ++ } else if (args[0].equalsIgnoreCase("version")) { ++ Command verCmd = org.bukkit.Bukkit.getServer().getCommandMap().getCommand("version"); ++ if (verCmd != null) { ++ return verCmd.execute(sender, commandLabel, new String[0]); ++ } ++ } ++ ++ return true; ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/command/RamBarCommand.java b/src/main/java/org/purpurmc/purpur/command/RamBarCommand.java +new file mode 100644 +index 0000000000000000000000000000000000000000..2852c07adb080c34905f5d1b19efed8ea47eecc6 +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/command/RamBarCommand.java +@@ -0,0 +1,44 @@ ++package org.purpurmc.purpur.command; ++ ++import com.mojang.brigadier.CommandDispatcher; ++import net.kyori.adventure.text.Component; ++import net.kyori.adventure.text.format.NamedTextColor; ++import net.kyori.adventure.text.minimessage.MiniMessage; ++import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; ++import net.minecraft.commands.CommandSourceStack; ++import net.minecraft.commands.Commands; ++import net.minecraft.commands.arguments.EntityArgument; ++import net.minecraft.server.level.ServerPlayer; ++import org.purpurmc.purpur.PurpurConfig; ++import org.purpurmc.purpur.task.RamBarTask; ++ ++import java.util.Collection; ++import java.util.Collections; ++ ++public class RamBarCommand { ++ public static void register(CommandDispatcher dispatcher) { ++ dispatcher.register(Commands.literal("rambar") ++ .requires(listener -> listener.hasPermission(2, "bukkit.command.rambar")) ++ .executes(context -> execute(context.getSource(), Collections.singleton(context.getSource().getPlayerOrException()))) ++ .then(Commands.argument("targets", EntityArgument.players()) ++ .requires(listener -> listener.hasPermission(2, "bukkit.command.rambar.other")) ++ .executes((context) -> execute(context.getSource(), EntityArgument.getPlayers(context, "targets"))) ++ ) ++ ); ++ } ++ ++ private static int execute(CommandSourceStack sender, Collection targets) { ++ for (ServerPlayer player : targets) { ++ boolean result = RamBarTask.instance().togglePlayer(player.getBukkitEntity()); ++ player.ramBar(result); ++ ++ Component output = MiniMessage.miniMessage().deserialize(PurpurConfig.rambarCommandOutput, ++ Placeholder.component("onoff", Component.translatable(result ? "options.on" : "options.off") ++ .color(result ? NamedTextColor.GREEN : NamedTextColor.RED)), ++ Placeholder.parsed("target", player.getGameProfile().getName())); ++ ++ sender.sendSuccess(output, false); ++ } ++ return targets.size(); ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/command/RamCommand.java b/src/main/java/org/purpurmc/purpur/command/RamCommand.java +new file mode 100644 +index 0000000000000000000000000000000000000000..992f8dfc628c7485e335191e1308cdfd4eedfbe8 +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/command/RamCommand.java +@@ -0,0 +1,30 @@ ++package org.purpurmc.purpur.command; ++ ++import com.mojang.brigadier.CommandDispatcher; ++import io.papermc.paper.adventure.PaperAdventure; ++import net.kyori.adventure.text.minimessage.MiniMessage; ++import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; ++import net.minecraft.commands.CommandSourceStack; ++import net.minecraft.commands.Commands; ++import org.purpurmc.purpur.PurpurConfig; ++import org.purpurmc.purpur.task.RamBarTask; ++ ++public class RamCommand { ++ public static void register(CommandDispatcher dispatcher) { ++ dispatcher.register(Commands.literal("ram") ++ .requires(listener -> listener.hasPermission(2, "bukkit.command.ram")) ++ .executes(context -> { ++ CommandSourceStack sender = context.getSource(); ++ RamBarTask ramBar = RamBarTask.instance(); ++ sender.sendSuccess(() -> PaperAdventure.asVanilla(MiniMessage.miniMessage().deserialize(PurpurConfig.ramCommandOutput, ++ Placeholder.component("allocated", ramBar.format(ramBar.getAllocated())), ++ Placeholder.component("used", ramBar.format(ramBar.getUsed())), ++ Placeholder.component("xmx", ramBar.format(ramBar.getXmx())), ++ Placeholder.component("xms", ramBar.format(ramBar.getXms())), ++ Placeholder.unparsed("percent", ((int) (ramBar.getPercent() * 100)) + "%") ++ )), false); ++ return 1; ++ }) ++ ); ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/command/TPSBarCommand.java b/src/main/java/org/purpurmc/purpur/command/TPSBarCommand.java +new file mode 100644 +index 0000000000000000000000000000000000000000..d8f9b044107ff7c29a83eb5378aa9f5465ba1995 +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/command/TPSBarCommand.java +@@ -0,0 +1,44 @@ ++package org.purpurmc.purpur.command; ++ ++import com.mojang.brigadier.CommandDispatcher; ++import net.kyori.adventure.text.Component; ++import net.kyori.adventure.text.format.NamedTextColor; ++import net.kyori.adventure.text.minimessage.MiniMessage; ++import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; ++import net.minecraft.commands.CommandSourceStack; ++import net.minecraft.commands.Commands; ++import net.minecraft.commands.arguments.EntityArgument; ++import net.minecraft.server.level.ServerPlayer; ++import org.purpurmc.purpur.PurpurConfig; ++import org.purpurmc.purpur.task.TPSBarTask; ++ ++import java.util.Collection; ++import java.util.Collections; ++ ++public class TPSBarCommand { ++ public static void register(CommandDispatcher dispatcher) { ++ dispatcher.register(Commands.literal("tpsbar") ++ .requires(listener -> listener.hasPermission(2, "bukkit.command.tpsbar")) ++ .executes(context -> execute(context.getSource(), Collections.singleton(context.getSource().getPlayerOrException()))) ++ .then(Commands.argument("targets", EntityArgument.players()) ++ .requires(listener -> listener.hasPermission(2, "bukkit.command.tpsbar.other")) ++ .executes((context) -> execute(context.getSource(), EntityArgument.getPlayers(context, "targets"))) ++ ) ++ ); ++ } ++ ++ private static int execute(CommandSourceStack sender, Collection targets) { ++ for (ServerPlayer player : targets) { ++ boolean result = TPSBarTask.instance().togglePlayer(player.getBukkitEntity()); ++ player.tpsBar(result); ++ ++ Component output = MiniMessage.miniMessage().deserialize(PurpurConfig.tpsbarCommandOutput, ++ Placeholder.component("onoff", Component.translatable(result ? "options.on" : "options.off") ++ .color(result ? NamedTextColor.GREEN : NamedTextColor.RED)), ++ Placeholder.parsed("target", player.getGameProfile().getName())); ++ ++ sender.sendSuccess(output, false); ++ } ++ return targets.size(); ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/command/UptimeCommand.java b/src/main/java/org/purpurmc/purpur/command/UptimeCommand.java +new file mode 100644 +index 0000000000000000000000000000000000000000..4bb475099bcf8f05d5f1474e7fbf29c57c2c40cd +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/command/UptimeCommand.java +@@ -0,0 +1,55 @@ ++package org.purpurmc.purpur.command; ++ ++import com.mojang.brigadier.CommandDispatcher; ++import net.kyori.adventure.text.Component; ++import net.kyori.adventure.text.minimessage.MiniMessage; ++import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; ++import net.minecraft.commands.CommandSourceStack; ++import net.minecraft.commands.Commands; ++import net.minecraft.server.MinecraftServer; ++import org.purpurmc.purpur.PurpurConfig; ++ ++import java.util.concurrent.TimeUnit; ++import java.util.function.Function; ++ ++public class UptimeCommand { ++ public static void register(CommandDispatcher dispatcher) { ++ dispatcher.register(Commands.literal("uptime") ++ .requires((listener) -> listener.hasPermission(2, "bukkit.command.uptime")) ++ .executes((context) -> execute(context.getSource())) ++ ); ++ } ++ ++ private static int execute(CommandSourceStack sender) { ++ Data data = new Data(); ++ ++ data.format = PurpurConfig.uptimeFormat; ++ data.hide = true; ++ data.millis = System.currentTimeMillis() - MinecraftServer.startTimeMillis; ++ ++ process(data, "", PurpurConfig.uptimeDay, PurpurConfig.uptimeDays, TimeUnit.DAYS, TimeUnit.MILLISECONDS::toDays); ++ process(data, "", PurpurConfig.uptimeHour, PurpurConfig.uptimeHours, TimeUnit.HOURS, TimeUnit.MILLISECONDS::toHours); ++ process(data, "", PurpurConfig.uptimeMinute, PurpurConfig.uptimeMinutes, TimeUnit.MINUTES, TimeUnit.MILLISECONDS::toMinutes); ++ data.hide = false; // never hide seconds ++ process(data, "", PurpurConfig.uptimeSecond, PurpurConfig.uptimeSeconds, TimeUnit.SECONDS, TimeUnit.MILLISECONDS::toSeconds); ++ ++ Component output = MiniMessage.miniMessage().deserialize(PurpurConfig.uptimeCommandOutput, Placeholder.unparsed("uptime", data.format)); ++ sender.sendSuccess(output, false); ++ return 1; ++ } ++ ++ private static void process(Data data, String replace, String singular, String plural, TimeUnit unit, Function func) { ++ if (data.format.contains(replace)) { ++ long val = func.apply(data.millis); ++ if (data.hide) data.hide = val == 0; ++ if (!data.hide) data.millis -= unit.toMillis(val); ++ data.format = data.format.replace(replace, data.hide ? "" : String.format(val == 1 ? singular : plural, val)); ++ } ++ } ++ ++ private static class Data { ++ String format; ++ boolean hide; ++ long millis; ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/controller/FlyingMoveControllerWASD.java b/src/main/java/org/purpurmc/purpur/controller/FlyingMoveControllerWASD.java +new file mode 100644 +index 0000000000000000000000000000000000000000..ed494e0ad278813a0eb261101447b84cca3ad7aa +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/controller/FlyingMoveControllerWASD.java +@@ -0,0 +1,71 @@ ++package org.purpurmc.purpur.controller; ++ ++import net.minecraft.world.entity.Mob; ++import net.minecraft.world.entity.ai.attributes.Attributes; ++import net.minecraft.world.entity.player.Player; ++ ++public class FlyingMoveControllerWASD extends MoveControllerWASD { ++ protected final float groundSpeedModifier; ++ protected final float flyingSpeedModifier; ++ protected int tooHighCooldown = 0; ++ protected boolean setNoGravityFlag; ++ ++ public FlyingMoveControllerWASD(Mob entity) { ++ this(entity, 1.0F); ++ } ++ ++ public FlyingMoveControllerWASD(Mob entity, float groundSpeedModifier) { ++ this(entity, groundSpeedModifier, 1.0F, true); ++ } ++ ++ public FlyingMoveControllerWASD(Mob entity, float groundSpeedModifier, float flyingSpeedModifier) { ++ this(entity, groundSpeedModifier, flyingSpeedModifier, true); ++ } ++ ++ public FlyingMoveControllerWASD(Mob entity, float groundSpeedModifier, float flyingSpeedModifier, boolean setNoGravityFlag) { ++ super(entity); ++ this.groundSpeedModifier = groundSpeedModifier; ++ this.flyingSpeedModifier = flyingSpeedModifier; ++ this.setNoGravityFlag = setNoGravityFlag; ++ } ++ ++ @Override ++ public void purpurTick(Player rider) { ++ float forward = Math.max(0.0F, rider.getForwardMot()); ++ float vertical = forward == 0.0F ? 0.0F : -(rider.xRotO / 45.0F); ++ float strafe = rider.getStrafeMot(); ++ ++ if (rider.jumping && spacebarEvent(entity)) { ++ entity.onSpacebar(); ++ } ++ ++ if (entity.getY() >= entity.getMaxY() || --tooHighCooldown > 0) { ++ if (tooHighCooldown <= 0) { ++ tooHighCooldown = 20; ++ } ++ entity.setDeltaMovement(entity.getDeltaMovement().add(0.0D, -0.05D, 0.0D)); ++ vertical = 0.0F; ++ } ++ ++ setSpeedModifier(entity.getAttributeValue(Attributes.MOVEMENT_SPEED)); ++ float speed = (float) getSpeedModifier(); ++ ++ if (entity.onGround) { ++ speed *= groundSpeedModifier; // TODO = fix this! ++ } else { ++ speed *= flyingSpeedModifier; ++ } ++ ++ if (setNoGravityFlag) { ++ entity.setNoGravity(forward > 0); ++ } ++ ++ entity.setSpeed(speed); ++ entity.setVerticalMot(vertical); ++ entity.setStrafeMot(strafe); ++ entity.setForwardMot(forward); ++ ++ setForward(entity.getForwardMot()); ++ setStrafe(entity.getStrafeMot()); ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/controller/FlyingWithSpacebarMoveControllerWASD.java b/src/main/java/org/purpurmc/purpur/controller/FlyingWithSpacebarMoveControllerWASD.java +new file mode 100644 +index 0000000000000000000000000000000000000000..9383c07fa53141127106a1f289366a040960d52e +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/controller/FlyingWithSpacebarMoveControllerWASD.java +@@ -0,0 +1,63 @@ ++package org.purpurmc.purpur.controller; ++ ++import net.minecraft.world.entity.Mob; ++import net.minecraft.world.entity.ai.attributes.Attributes; ++import net.minecraft.world.entity.player.Player; ++import net.minecraft.world.phys.Vec3; ++ ++public class FlyingWithSpacebarMoveControllerWASD extends FlyingMoveControllerWASD { ++ public FlyingWithSpacebarMoveControllerWASD(Mob entity) { ++ super(entity); ++ } ++ ++ public FlyingWithSpacebarMoveControllerWASD(Mob entity, float groundSpeedModifier) { ++ super(entity, groundSpeedModifier); ++ } ++ ++ @Override ++ public void purpurTick(Player rider) { ++ float forward = rider.getForwardMot(); ++ float strafe = rider.getStrafeMot() * 0.5F; ++ float vertical = 0; ++ ++ if (forward < 0.0F) { ++ forward *= 0.5F; ++ strafe *= 0.5F; ++ } ++ ++ float speed = (float) entity.getAttributeValue(Attributes.MOVEMENT_SPEED); ++ ++ if (entity.onGround) { ++ speed *= groundSpeedModifier; ++ } ++ ++ if (rider.jumping && spacebarEvent(entity) && !entity.onSpacebar()) { ++ entity.setNoGravity(true); ++ vertical = 1.0F; ++ } else { ++ entity.setNoGravity(false); ++ } ++ ++ if (entity.getY() >= entity.getMaxY() || --tooHighCooldown > 0) { ++ if (tooHighCooldown <= 0) { ++ tooHighCooldown = 20; ++ } ++ entity.setDeltaMovement(entity.getDeltaMovement().add(0.0D, -0.2D, 0.0D)); ++ vertical = 0.0F; ++ } ++ ++ setSpeedModifier(speed); ++ entity.setSpeed((float) getSpeedModifier()); ++ entity.setVerticalMot(vertical); ++ entity.setStrafeMot(strafe); ++ entity.setForwardMot(forward); ++ ++ setForward(entity.getForwardMot()); ++ setStrafe(entity.getStrafeMot()); ++ ++ Vec3 mot = entity.getDeltaMovement(); ++ if (mot.y > 0.2D) { ++ entity.setDeltaMovement(mot.x, 0.2D, mot.z); ++ } ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/controller/LookControllerWASD.java b/src/main/java/org/purpurmc/purpur/controller/LookControllerWASD.java +new file mode 100644 +index 0000000000000000000000000000000000000000..b8c25c96e95dd5ec3ad9fa4c41bd6c08e144832d +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/controller/LookControllerWASD.java +@@ -0,0 +1,76 @@ ++package org.purpurmc.purpur.controller; ++ ++ ++import net.minecraft.network.protocol.game.ClientboundMoveEntityPacket; ++import net.minecraft.util.Mth; ++import net.minecraft.world.entity.Mob; ++import net.minecraft.world.entity.ai.control.LookControl; ++import net.minecraft.world.entity.player.Player; ++ ++public class LookControllerWASD extends LookControl { ++ protected final Mob entity; ++ private float yOffset = 0; ++ private float xOffset = 0; ++ ++ public LookControllerWASD(Mob entity) { ++ super(entity); ++ this.entity = entity; ++ } ++ ++ // tick ++ @Override ++ public void tick() { ++ if (entity.getRider() != null && entity.isControllable()) { ++ purpurTick(entity.getRider()); ++ } else { ++ vanillaTick(); ++ } ++ } ++ ++ protected void purpurTick(Player rider) { ++ setYawPitch(rider.getYRot(), rider.getXRot()); ++ } ++ ++ public void vanillaTick() { ++ super.tick(); ++ } ++ ++ public void setYawPitch(float yRot, float xRot) { ++ entity.setXRot(normalizePitch(xRot + xOffset)); ++ entity.setYRot(normalizeYaw(yRot + yOffset)); ++ entity.setYHeadRot(entity.getYRot()); ++ entity.xRotO = entity.getXRot(); ++ entity.yRotO = entity.getYRot(); ++ ++ entity.tracker.broadcast(new ClientboundMoveEntityPacket ++ .PosRot(entity.getId(), ++ (short) 0, (short) 0, (short) 0, ++ (byte) Mth.floor(entity.getYRot() * 256.0F / 360.0F), ++ (byte) Mth.floor(entity.getXRot() * 256.0F / 360.0F), ++ entity.onGround)); ++ } ++ ++ public void setOffsets(float yaw, float pitch) { ++ yOffset = yaw; ++ xOffset = pitch; ++ } ++ ++ public float normalizeYaw(float yaw) { ++ yaw %= 360.0f; ++ if (yaw >= 180.0f) { ++ yaw -= 360.0f; ++ } else if (yaw < -180.0f) { ++ yaw += 360.0f; ++ } ++ return yaw; ++ } ++ ++ public float normalizePitch(float pitch) { ++ if (pitch > 90.0f) { ++ pitch = 90.0f; ++ } else if (pitch < -90.0f) { ++ pitch = -90.0f; ++ } ++ return pitch; ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/controller/MoveControllerWASD.java b/src/main/java/org/purpurmc/purpur/controller/MoveControllerWASD.java +new file mode 100644 +index 0000000000000000000000000000000000000000..21fd6ea2a482758a3016e3bc2cdebe2d89267481 +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/controller/MoveControllerWASD.java +@@ -0,0 +1,89 @@ ++package org.purpurmc.purpur.controller; ++ ++import net.minecraft.world.entity.Mob; ++import net.minecraft.world.entity.ai.attributes.Attributes; ++import net.minecraft.world.entity.ai.control.MoveControl; ++import net.minecraft.world.entity.player.Player; ++import org.purpurmc.purpur.event.entity.RidableSpacebarEvent; ++ ++public class MoveControllerWASD extends MoveControl { ++ protected final Mob entity; ++ private final double speedModifier; ++ ++ public MoveControllerWASD(Mob entity) { ++ this(entity, 1.0D); ++ } ++ ++ public MoveControllerWASD(Mob entity, double speedModifier) { ++ super(entity); ++ this.entity = entity; ++ this.speedModifier = speedModifier; ++ } ++ ++ @Override ++ public boolean hasWanted() { ++ return entity.getRider() != null ? strafeForwards != 0 || strafeRight != 0 : super.hasWanted(); ++ } ++ ++ @Override ++ public void tick() { ++ if (entity.getRider() != null && entity.isControllable()) { ++ purpurTick(entity.getRider()); ++ } else { ++ vanillaTick(); ++ } ++ } ++ ++ public void vanillaTick() { ++ super.tick(); ++ } ++ ++ public void purpurTick(Player rider) { ++ float forward = rider.getForwardMot() * 0.5F; ++ float strafe = rider.getStrafeMot() * 0.25F; ++ ++ if (forward <= 0.0F) { ++ forward *= 0.5F; ++ } ++ ++ float yawOffset = 0; ++ if (strafe != 0) { ++ if (forward == 0) { ++ yawOffset += strafe > 0 ? -90 : 90; ++ forward = Math.abs(strafe * 2); ++ } else { ++ yawOffset += strafe > 0 ? -30 : 30; ++ strafe /= 2; ++ if (forward < 0) { ++ yawOffset += strafe > 0 ? -110 : 110; ++ forward *= -1; ++ } ++ } ++ } else if (forward < 0) { ++ yawOffset -= 180; ++ forward *= -1; ++ } ++ ++ ((LookControllerWASD) entity.getLookControl()).setOffsets(yawOffset, 0); ++ ++ if (rider.jumping && spacebarEvent(entity) && !entity.onSpacebar() && entity.onGround) { ++ entity.jumpFromGround(); ++ } ++ ++ setSpeedModifier(entity.getAttributeValue(Attributes.MOVEMENT_SPEED) * speedModifier); ++ ++ entity.setSpeed((float) getSpeedModifier()); ++ entity.setForwardMot(forward); ++ ++ setForward(entity.getForwardMot()); ++ setStrafe(entity.getStrafeMot()); ++ } ++ ++ public static boolean spacebarEvent(Mob entity) { ++ if (RidableSpacebarEvent.getHandlerList().getRegisteredListeners().length > 0) { ++ return new RidableSpacebarEvent(entity.getBukkitEntity()).callEvent(); ++ } else { ++ return true; ++ } ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/controller/WaterMoveControllerWASD.java b/src/main/java/org/purpurmc/purpur/controller/WaterMoveControllerWASD.java +new file mode 100644 +index 0000000000000000000000000000000000000000..ba2a37dad43e238e54632975abea8ee6fafaa9e0 +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/controller/WaterMoveControllerWASD.java +@@ -0,0 +1,50 @@ ++package org.purpurmc.purpur.controller; ++ ++import net.minecraft.world.entity.Mob; ++import net.minecraft.world.entity.ai.attributes.Attributes; ++import net.minecraft.world.entity.player.Player; ++ ++public class WaterMoveControllerWASD extends MoveControllerWASD { ++ private final double speedModifier; ++ ++ public WaterMoveControllerWASD(Mob entity) { ++ this(entity, 1.0D); ++ } ++ ++ public WaterMoveControllerWASD(Mob entity, double speedModifier) { ++ super(entity); ++ this.speedModifier = speedModifier; ++ } ++ ++ @Override ++ public void purpurTick(Player rider) { ++ float forward = rider.getForwardMot(); ++ float strafe = rider.getStrafeMot() * 0.5F; // strafe slower by default ++ float vertical = -(rider.xRotO / 90); ++ ++ if (forward == 0.0F) { ++ // strafe slower if not moving forward ++ strafe *= 0.5F; ++ // do not move vertically if not moving forward ++ vertical = 0.0F; ++ } else if (forward < 0.0F) { ++ // water animals can't swim backwards ++ forward = 0.0F; ++ vertical = 0.0F; ++ } ++ ++ if (rider.jumping && spacebarEvent(entity)) { ++ entity.onSpacebar(); ++ } ++ ++ setSpeedModifier(entity.getAttributeValue(Attributes.MOVEMENT_SPEED) * speedModifier); ++ entity.setSpeed((float) getSpeedModifier() * 0.1F); ++ ++ entity.setForwardMot(forward * (float) speedModifier); ++ entity.setStrafeMot(strafe * (float) speedModifier); ++ entity.setVerticalMot(vertical * (float) speedModifier); ++ ++ setForward(entity.getForwardMot()); ++ setStrafe(entity.getStrafeMot()); ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/entity/DolphinSpit.java b/src/main/java/org/purpurmc/purpur/entity/DolphinSpit.java +new file mode 100644 +index 0000000000000000000000000000000000000000..f25abee6dbf99c8d08f8e09db02b41df86115faa +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/entity/DolphinSpit.java +@@ -0,0 +1,107 @@ ++package org.purpurmc.purpur.entity; ++ ++import net.minecraft.core.particles.ParticleTypes; ++import net.minecraft.server.level.ServerLevel; ++import net.minecraft.util.Mth; ++import net.minecraft.world.damagesource.DamageSource; ++import net.minecraft.world.entity.Entity; ++import net.minecraft.world.entity.EntityType; ++import net.minecraft.world.entity.LivingEntity; ++import net.minecraft.world.entity.animal.Dolphin; ++import net.minecraft.world.entity.projectile.LlamaSpit; ++import net.minecraft.world.entity.projectile.ProjectileUtil; ++import net.minecraft.world.level.Level; ++import net.minecraft.world.level.block.state.BlockState; ++import net.minecraft.world.phys.BlockHitResult; ++import net.minecraft.world.phys.EntityHitResult; ++import net.minecraft.world.phys.HitResult; ++import net.minecraft.world.phys.Vec3; ++import org.bukkit.event.entity.EntityRemoveEvent; ++ ++public class DolphinSpit extends LlamaSpit { ++ public LivingEntity dolphin; ++ public int ticksLived; ++ ++ public DolphinSpit(EntityType type, Level world) { ++ super(type, world); ++ } ++ ++ public DolphinSpit(Level world, Dolphin dolphin) { ++ this(EntityType.LLAMA_SPIT, world); ++ setOwner(dolphin.getRider() != null ? dolphin.getRider() : dolphin); ++ this.dolphin = dolphin; ++ this.setPos( ++ dolphin.getX() - (double) (dolphin.getBbWidth() + 1.0F) * 0.5D * (double) Mth.sin(dolphin.yBodyRot * 0.017453292F), ++ dolphin.getEyeY() - 0.10000000149011612D, ++ dolphin.getZ() + (double) (dolphin.getBbWidth() + 1.0F) * 0.5D * (double) Mth.cos(dolphin.yBodyRot * 0.017453292F)); ++ } ++ ++ // Purpur start ++ @Override ++ public boolean canSaveToDisk() { ++ return false; ++ } ++ // Purpur end ++ ++ public void tick() { ++ super_tick(); ++ ++ Vec3 mot = this.getDeltaMovement(); ++ HitResult hitResult = ProjectileUtil.getHitResultOnMoveVector(this, this::canHitEntity); ++ ++ this.preHitTargetOrDeflectSelf(hitResult); ++ ++ double x = this.getX() + mot.x; ++ double y = this.getY() + mot.y; ++ double z = this.getZ() + mot.z; ++ ++ this.updateRotation(); ++ ++ Vec3 motDouble = mot.scale(2.0); ++ for (int i = 0; i < 5; i++) { ++ ((ServerLevel) level()).sendParticles(null, ParticleTypes.BUBBLE, ++ getX() + random.nextFloat() / 2 - 0.25F, ++ getY() + random.nextFloat() / 2 - 0.25F, ++ getZ() + random.nextFloat() / 2 - 0.25F, ++ 0, motDouble.x(), motDouble.y(), motDouble.z(), 0.1D, true); ++ } ++ ++ if (++ticksLived > 20) { ++ this.discard(EntityRemoveEvent.Cause.DISCARD); ++ } else { ++ this.setDeltaMovement(mot.scale(0.99D)); ++ if (!this.isNoGravity()) { ++ this.setDeltaMovement(this.getDeltaMovement().add(0.0D, -0.06D, 0.0D)); ++ } ++ ++ this.setPos(x, y, z); ++ } ++ } ++ ++ @Override ++ public void shoot(double x, double y, double z, float speed, float inaccuracy) { ++ setDeltaMovement(new Vec3(x, y, z).normalize().add( ++ random.nextGaussian() * (double) 0.0075F * (double) inaccuracy, ++ random.nextGaussian() * (double) 0.0075F * (double) inaccuracy, ++ random.nextGaussian() * (double) 0.0075F * (double) inaccuracy) ++ .scale(speed)); ++ } ++ ++ @Override ++ protected void onHitEntity(EntityHitResult entityHitResult) { ++ Entity shooter = this.getOwner(); ++ if (shooter instanceof LivingEntity) { ++ entityHitResult.getEntity().hurt(entityHitResult.getEntity().damageSources().mobProjectile(this, (LivingEntity) shooter), level().purpurConfig.dolphinSpitDamage); ++ } ++ } ++ ++ @Override ++ protected void onHitBlock(BlockHitResult blockHitResult) { ++ if (this.hitCancelled) { ++ return; ++ } ++ BlockState state = this.level().getBlockState(blockHitResult.getBlockPos()); ++ state.onProjectileHit(this.level(), state, blockHitResult, this); ++ this.discard(EntityRemoveEvent.Cause.DISCARD); ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/entity/PhantomFlames.java b/src/main/java/org/purpurmc/purpur/entity/PhantomFlames.java +new file mode 100644 +index 0000000000000000000000000000000000000000..75e31aee6e706f042398444f272888f9ad0fa3f4 +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/entity/PhantomFlames.java +@@ -0,0 +1,121 @@ ++package org.purpurmc.purpur.entity; ++ ++import net.minecraft.core.particles.ParticleTypes; ++import net.minecraft.server.level.ServerLevel; ++import net.minecraft.util.Mth; ++import net.minecraft.world.damagesource.DamageSource; ++import net.minecraft.world.entity.Entity; ++import net.minecraft.world.entity.EntityType; ++import net.minecraft.world.entity.LivingEntity; ++import net.minecraft.world.entity.decoration.ArmorStand; ++import net.minecraft.world.entity.monster.Phantom; ++import net.minecraft.world.entity.projectile.LlamaSpit; ++import net.minecraft.world.entity.projectile.ProjectileUtil; ++import net.minecraft.world.level.Level; ++import net.minecraft.world.level.block.state.BlockBehaviour; ++import net.minecraft.world.level.block.state.BlockState; ++import net.minecraft.world.phys.BlockHitResult; ++import net.minecraft.world.phys.EntityHitResult; ++import net.minecraft.world.phys.HitResult; ++import net.minecraft.world.phys.Vec3; ++ ++public class PhantomFlames extends LlamaSpit { ++ public Phantom phantom; ++ public int ticksLived; ++ public boolean canGrief = false; ++ ++ public PhantomFlames(EntityType type, Level world) { ++ super(type, world); ++ } ++ ++ public PhantomFlames(Level world, Phantom phantom) { ++ this(EntityType.LLAMA_SPIT, world); ++ setOwner(phantom.getRider() != null ? phantom.getRider() : phantom); ++ this.phantom = phantom; ++ this.setPos( ++ phantom.getX() - (double) (phantom.getBbWidth() + 1.0F) * 0.5D * (double) Mth.sin(phantom.yBodyRot * 0.017453292F), ++ phantom.getEyeY() - 0.10000000149011612D, ++ phantom.getZ() + (double) (phantom.getBbWidth() + 1.0F) * 0.5D * (double) Mth.cos(phantom.yBodyRot * 0.017453292F)); ++ } ++ ++ // Purpur start ++ @Override ++ public boolean canSaveToDisk() { ++ return false; ++ } ++ // Purpur end ++ ++ public void tick() { ++ super_tick(); ++ ++ Vec3 mot = this.getDeltaMovement(); ++ HitResult hitResult = ProjectileUtil.getHitResultOnMoveVector(this, this::canHitEntity); ++ ++ this.preHitTargetOrDeflectSelf(hitResult); ++ ++ double x = this.getX() + mot.x; ++ double y = this.getY() + mot.y; ++ double z = this.getZ() + mot.z; ++ ++ this.updateRotation(); ++ ++ Vec3 motDouble = mot.scale(2.0); ++ for (int i = 0; i < 5; i++) { ++ ((ServerLevel) level()).sendParticles(null, ParticleTypes.FLAME, ++ getX() + random.nextFloat() / 2 - 0.25F, ++ getY() + random.nextFloat() / 2 - 0.25F, ++ getZ() + random.nextFloat() / 2 - 0.25F, ++ 0, motDouble.x(), motDouble.y(), motDouble.z(), 0.1D, true); ++ } ++ ++ if (++ticksLived > 20) { ++ this.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.DISCARD); ++ } else if (this.level().getBlockStates(this.getBoundingBox()).noneMatch(BlockBehaviour.BlockStateBase::isAir)) { ++ this.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.DISCARD); ++ } else if (this.isInWaterOrBubble()) { ++ this.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.DISCARD); ++ } else { ++ this.setDeltaMovement(mot.scale(0.99D)); ++ if (!this.isNoGravity()) { ++ this.setDeltaMovement(this.getDeltaMovement().add(0.0D, -0.06D, 0.0D)); ++ } ++ ++ this.setPos(x, y, z); ++ } ++ } ++ ++ @Override ++ public void shoot(double x, double y, double z, float speed, float inaccuracy) { ++ setDeltaMovement(new Vec3(x, y, z).normalize().add( ++ random.nextGaussian() * (double) 0.0075F * (double) inaccuracy, ++ random.nextGaussian() * (double) 0.0075F * (double) inaccuracy, ++ random.nextGaussian() * (double) 0.0075F * (double) inaccuracy) ++ .scale(speed)); ++ } ++ ++ @Override ++ protected void onHitEntity(EntityHitResult entityHitResult) { ++ Entity shooter = this.getOwner(); ++ if (shooter instanceof LivingEntity) { ++ Entity target = entityHitResult.getEntity(); ++ if (canGrief || (target instanceof LivingEntity && !(target instanceof ArmorStand))) { ++ boolean hurt = target.hurt(target.damageSources().mobProjectile(this, (LivingEntity) shooter), level().purpurConfig.phantomFlameDamage); ++ if (hurt && level().purpurConfig.phantomFlameFireTime > 0) { ++ target.igniteForSeconds(level().purpurConfig.phantomFlameFireTime); ++ } ++ } ++ } ++ } ++ ++ @Override ++ protected void onHitBlock(BlockHitResult blockHitResult) { ++ if (this.hitCancelled) { ++ return; ++ } ++ if (this.canGrief) { ++ BlockState state = this.level().getBlockState(blockHitResult.getBlockPos()); ++ state.onProjectileHit(this.level(), state, blockHitResult, this); ++ } ++ this.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.DISCARD); ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/entity/PurpurStoredBee.java b/src/main/java/org/purpurmc/purpur/entity/PurpurStoredBee.java +new file mode 100644 +index 0000000000000000000000000000000000000000..7608bf0981fa0d37031e51e57e4086cb5ec4c88b +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/entity/PurpurStoredBee.java +@@ -0,0 +1,106 @@ ++package org.purpurmc.purpur.entity; ++ ++import io.papermc.paper.adventure.PaperAdventure; ++import net.kyori.adventure.text.Component; ++import net.minecraft.nbt.CompoundTag; ++import net.minecraft.nbt.Tag; ++import net.minecraft.server.MinecraftServer; ++import net.minecraft.world.item.component.CustomData; ++import net.minecraft.world.level.block.entity.BeehiveBlockEntity; ++import org.bukkit.block.EntityBlockStorage; ++import org.bukkit.craftbukkit.persistence.CraftPersistentDataContainer; ++import org.bukkit.craftbukkit.persistence.CraftPersistentDataTypeRegistry; ++import org.bukkit.entity.Bee; ++import org.bukkit.entity.EntityType; ++import org.bukkit.persistence.PersistentDataContainer; ++import org.jetbrains.annotations.NotNull; ++import org.jetbrains.annotations.Nullable; ++ ++import java.util.Locale; ++ ++public class PurpurStoredBee implements StoredEntity { ++ private static final CraftPersistentDataTypeRegistry DATA_TYPE_REGISTRY = new CraftPersistentDataTypeRegistry(); ++ ++ private final EntityBlockStorage blockStorage; ++ private final BeehiveBlockEntity.BeeData handle; ++ private final CraftPersistentDataContainer persistentDataContainer = new CraftPersistentDataContainer(PurpurStoredBee.DATA_TYPE_REGISTRY); ++ ++ private Component customName; ++ ++ public PurpurStoredBee(BeehiveBlockEntity.BeeData data, EntityBlockStorage blockStorage) { ++ this.handle = data; ++ this.blockStorage = blockStorage; ++ ++ CompoundTag customData = handle.occupant.entityData().copyTag(); ++ this.customName = customData.contains("CustomName") ++ ? PaperAdventure.asAdventure(net.minecraft.network.chat.Component.Serializer.fromJson(customData.getString("CustomName"), MinecraftServer.getDefaultRegistryAccess())) ++ : null; ++ ++ if(customData.contains("BukkitValues", Tag.TAG_COMPOUND)) { ++ this.persistentDataContainer.putAll(customData.getCompound("BukkitValues")); ++ } ++ } ++ ++ public BeehiveBlockEntity.BeeData getHandle() { ++ return handle; ++ } ++ ++ @Override ++ public @Nullable Component customName() { ++ return customName; ++ } ++ ++ @Override ++ public void customName(@Nullable Component customName) { ++ this.customName = customName; ++ } ++ ++ @Override ++ public @Nullable String getCustomName() { ++ return PaperAdventure.asPlain(customName, Locale.US); ++ } ++ ++ @Override ++ public void setCustomName(@Nullable String name) { ++ customName(name != null ? Component.text(name) : null); ++ } ++ ++ @Override ++ public @NotNull PersistentDataContainer getPersistentDataContainer() { ++ return persistentDataContainer; ++ } ++ ++ @Override ++ public boolean hasBeenReleased() { ++ return !blockStorage.getEntities().contains(this); ++ } ++ ++ @Override ++ public @Nullable Bee release() { ++ return blockStorage.releaseEntity(this); ++ } ++ ++ @Override ++ public @Nullable EntityBlockStorage getBlockStorage() { ++ if(hasBeenReleased()) { ++ return null; ++ } ++ ++ return blockStorage; ++ } ++ ++ @Override ++ public @NotNull EntityType getType() { ++ return EntityType.BEE; ++ } ++ ++ @Override ++ public void update() { ++ handle.occupant.entityData().copyTag().put("BukkitValues", this.persistentDataContainer.toTagCompound()); ++ if(customName == null) { ++ handle.occupant.entityData().copyTag().remove("CustomName"); ++ } else { ++ handle.occupant.entityData().copyTag().putString("CustomName", net.minecraft.network.chat.Component.Serializer.toJson(PaperAdventure.asVanilla(customName), MinecraftServer.getDefaultRegistryAccess())); ++ } ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/entity/ai/HasRider.java b/src/main/java/org/purpurmc/purpur/entity/ai/HasRider.java +new file mode 100644 +index 0000000000000000000000000000000000000000..8babdaddd8b33278aea0369dbbeeb445abe45016 +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/entity/ai/HasRider.java +@@ -0,0 +1,20 @@ ++package org.purpurmc.purpur.entity.ai; ++ ++import net.minecraft.world.entity.Mob; ++import net.minecraft.world.entity.ai.goal.Goal; ++ ++import java.util.EnumSet; ++ ++public class HasRider extends Goal { ++ public final Mob entity; ++ ++ public HasRider(Mob entity) { ++ this.entity = entity; ++ setFlags(EnumSet.of(Flag.MOVE, Flag.LOOK, Flag.TARGET, Flag.UNKNOWN_BEHAVIOR)); ++ } ++ ++ @Override ++ public boolean canUse() { ++ return entity.getRider() != null && entity.isControllable(); ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/entity/ai/HorseHasRider.java b/src/main/java/org/purpurmc/purpur/entity/ai/HorseHasRider.java +new file mode 100644 +index 0000000000000000000000000000000000000000..432f4f3d82af2f19820890b68d33189a9f2c69f9 +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/entity/ai/HorseHasRider.java +@@ -0,0 +1,17 @@ ++package org.purpurmc.purpur.entity.ai; ++ ++import net.minecraft.world.entity.animal.horse.AbstractHorse; ++ ++public class HorseHasRider extends HasRider { ++ public final AbstractHorse horse; ++ ++ public HorseHasRider(AbstractHorse entity) { ++ super(entity); ++ this.horse = entity; ++ } ++ ++ @Override ++ public boolean canUse() { ++ return super.canUse() && horse.isSaddled(); ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/entity/ai/LlamaHasRider.java b/src/main/java/org/purpurmc/purpur/entity/ai/LlamaHasRider.java +new file mode 100644 +index 0000000000000000000000000000000000000000..18a95e043cbffa65eeaaf65ff7695e5dc939820c +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/entity/ai/LlamaHasRider.java +@@ -0,0 +1,17 @@ ++package org.purpurmc.purpur.entity.ai; ++ ++import net.minecraft.world.entity.animal.horse.Llama; ++ ++public class LlamaHasRider extends HasRider { ++ public final Llama llama; ++ ++ public LlamaHasRider(Llama entity) { ++ super(entity); ++ this.llama = entity; ++ } ++ ++ @Override ++ public boolean canUse() { ++ return super.canUse() && llama.isSaddled() && llama.isControllable(); ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/entity/ai/ReceiveFlower.java b/src/main/java/org/purpurmc/purpur/entity/ai/ReceiveFlower.java +new file mode 100644 +index 0000000000000000000000000000000000000000..9660716f4162a4441c6e1b0baddef8f5086566c5 +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/entity/ai/ReceiveFlower.java +@@ -0,0 +1,91 @@ ++package org.purpurmc.purpur.entity.ai; ++ ++import net.minecraft.server.level.ServerLevel; ++import net.minecraft.server.level.ServerPlayer; ++import net.minecraft.world.InteractionHand; ++import net.minecraft.world.entity.Entity; ++import net.minecraft.world.entity.ai.goal.Goal; ++import net.minecraft.world.entity.animal.IronGolem; ++import net.minecraft.world.item.ItemStack; ++import net.minecraft.world.level.block.Blocks; ++ ++import java.util.EnumSet; ++import java.util.UUID; ++ ++public class ReceiveFlower extends Goal { ++ private final IronGolem irongolem; ++ private ServerPlayer target; ++ private int cooldown; ++ ++ public ReceiveFlower(IronGolem entity) { ++ this.irongolem = entity; ++ setFlags(EnumSet.of(Flag.MOVE, Flag.LOOK)); ++ } ++ ++ @Override ++ public boolean canUse() { ++ if (this.irongolem.getOfferFlowerTick() > 0) { ++ return false; ++ } ++ if (!this.irongolem.isAngry()) { ++ return false; ++ } ++ UUID uuid = this.irongolem.getPersistentAngerTarget(); ++ if (uuid == null) { ++ return false; ++ } ++ Entity target = ((ServerLevel) this.irongolem.level()).getEntity(uuid); ++ if (!(target instanceof ServerPlayer player)) { ++ return false; ++ } ++ InteractionHand hand = getPoppyHand(player); ++ if (hand == null) { ++ return false; ++ } ++ removeFlower(player, hand); ++ this.target = player; ++ return true; ++ } ++ ++ @Override ++ public boolean canContinueToUse() { ++ return this.cooldown > 0; ++ } ++ ++ @Override ++ public void start() { ++ this.cooldown = 100; ++ this.irongolem.stopBeingAngry(); ++ this.irongolem.offerFlower(true); ++ } ++ ++ @Override ++ public void stop() { ++ this.irongolem.offerFlower(false); ++ this.target = null; ++ } ++ ++ @Override ++ public void tick() { ++ this.irongolem.getLookControl().setLookAt(this.target, 30.0F, 30.0F); ++ --this.cooldown; ++ } ++ ++ private InteractionHand getPoppyHand(ServerPlayer player) { ++ if (isPoppy(player.getMainHandItem())) { ++ return InteractionHand.MAIN_HAND; ++ } ++ if (isPoppy(player.getOffhandItem())) { ++ return InteractionHand.OFF_HAND; ++ } ++ return null; ++ } ++ ++ private void removeFlower(ServerPlayer player, InteractionHand hand) { ++ player.setItemInHand(hand, ItemStack.EMPTY); ++ } ++ ++ private boolean isPoppy(ItemStack item) { ++ return item.getItem() == Blocks.POPPY.asItem(); ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/gui/GUIColor.java b/src/main/java/org/purpurmc/purpur/gui/GUIColor.java +new file mode 100644 +index 0000000000000000000000000000000000000000..550222758bf0e7deff26a6e813a860b7be365e87 +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/gui/GUIColor.java +@@ -0,0 +1,58 @@ ++package org.purpurmc.purpur.gui; ++ ++import net.md_5.bungee.api.ChatColor; ++ ++import java.awt.Color; ++import java.util.HashMap; ++import java.util.Map; ++ ++public enum GUIColor { ++ BLACK(ChatColor.BLACK, new Color(0x000000)), ++ DARK_BLUE(ChatColor.DARK_BLUE, new Color(0x0000AA)), ++ DARK_GREEN(ChatColor.DARK_GREEN, new Color(0x00AA00)), ++ DARK_AQUA(ChatColor.DARK_AQUA, new Color(0x009999)), ++ DARK_RED(ChatColor.DARK_RED, new Color(0xAA0000)), ++ DARK_PURPLE(ChatColor.DARK_PURPLE, new Color(0xAA00AA)), ++ GOLD(ChatColor.GOLD, new Color(0xBB8800)), ++ GRAY(ChatColor.GRAY, new Color(0x888888)), ++ DARK_GRAY(ChatColor.DARK_GRAY, new Color(0x444444)), ++ BLUE(ChatColor.BLUE, new Color(0x5555FF)), ++ GREEN(ChatColor.GREEN, new Color(0x55FF55)), ++ AQUA(ChatColor.AQUA, new Color(0x55DDDD)), ++ RED(ChatColor.RED, new Color(0xFF5555)), ++ LIGHT_PURPLE(ChatColor.LIGHT_PURPLE, new Color(0xFF55FF)), ++ YELLOW(ChatColor.YELLOW, new Color(0xFFBB00)), ++ WHITE(ChatColor.WHITE, new Color(0xBBBBBB)); ++ ++ private final ChatColor chat; ++ private final Color color; ++ ++ private static final Map BY_CHAT = new HashMap<>(); ++ ++ GUIColor(ChatColor chat, Color color) { ++ this.chat = chat; ++ this.color = color; ++ } ++ ++ public Color getColor() { ++ return color; ++ } ++ ++ public ChatColor getChatColor() { ++ return chat; ++ } ++ ++ public String getCode() { ++ return chat.toString(); ++ } ++ ++ public static GUIColor getColor(ChatColor chat) { ++ return BY_CHAT.get(chat); ++ } ++ ++ static { ++ for (GUIColor color : values()) { ++ BY_CHAT.put(color.chat, color); ++ } ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/gui/JColorTextPane.java b/src/main/java/org/purpurmc/purpur/gui/JColorTextPane.java +new file mode 100644 +index 0000000000000000000000000000000000000000..d75fb5e77eff27d86135ed7d605dbc250b660f7d +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/gui/JColorTextPane.java +@@ -0,0 +1,83 @@ ++package org.purpurmc.purpur.gui; ++ ++import com.google.common.collect.Sets; ++import javax.swing.UIManager; ++import net.md_5.bungee.api.chat.BaseComponent; ++import net.md_5.bungee.api.chat.TextComponent; ++ ++import javax.swing.JTextPane; ++import javax.swing.Timer; ++import javax.swing.text.AttributeSet; ++import javax.swing.text.BadLocationException; ++import javax.swing.text.SimpleAttributeSet; ++import javax.swing.text.StyleConstants; ++import javax.swing.text.StyleContext; ++import java.util.Set; ++ ++public class JColorTextPane extends JTextPane { ++ private static final GUIColor DEFAULT_COLOR; ++ static { ++ DEFAULT_COLOR = UIManager.getSystemLookAndFeelClassName().equals("com.sun.java.swing.plaf.gtk.GTKLookAndFeel") ++ ? GUIColor.WHITE : GUIColor.BLACK; ++ } ++ ++ ++ public void append(String msg) { ++ // TODO: update to use adventure instead ++ BaseComponent[] components = TextComponent.fromLegacyText(DEFAULT_COLOR.getCode() + msg, DEFAULT_COLOR.getChatColor()); ++ for (BaseComponent component : components) { ++ String text = component.toPlainText(); ++ if (text == null || text.isEmpty()) { ++ continue; ++ } ++ ++ GUIColor guiColor = GUIColor.getColor(component.getColor()); ++ ++ StyleContext context = StyleContext.getDefaultStyleContext(); ++ AttributeSet attr = context.addAttribute(SimpleAttributeSet.EMPTY, StyleConstants.Foreground, guiColor.getColor()); ++ attr = context.addAttribute(attr, StyleConstants.CharacterConstants.Bold, component.isBold() || guiColor != DEFAULT_COLOR); ++ attr = context.addAttribute(attr, StyleConstants.CharacterConstants.Italic, component.isItalic()); ++ attr = context.addAttribute(attr, StyleConstants.CharacterConstants.Underline, component.isUnderlined()); ++ attr = context.addAttribute(attr, StyleConstants.CharacterConstants.StrikeThrough, component.isStrikethrough()); ++ //attr = context.addAttribute(attr, StyleConstants.CharacterConstants.Blink, component.isObfuscated()); // no such thing as Blink, sadly ++ ++ try { ++ int pos = getDocument().getLength(); ++ getDocument().insertString(pos, text, attr); ++ ++ if (component.isObfuscated()) { ++ // dirty hack to blink some text ++ Blink blink = new Blink(pos, text.length(), attr, context.addAttribute(attr, StyleConstants.Foreground, getBackground())); ++ BLINKS.add(blink); ++ } ++ } catch (BadLocationException ignore) { ++ } ++ } ++ } ++ ++ private static final Set BLINKS = Sets.newHashSet(); ++ private static boolean SYNC_BLINK; ++ ++ static { ++ new Timer(500, e -> { ++ SYNC_BLINK = !SYNC_BLINK; ++ BLINKS.forEach(Blink::blink); ++ }).start(); ++ } ++ ++ public class Blink { ++ private final int start, length; ++ private final AttributeSet attr1, attr2; ++ ++ private Blink(int start, int length, AttributeSet attr1, AttributeSet attr2) { ++ this.start = start; ++ this.length = length; ++ this.attr1 = attr1; ++ this.attr2 = attr2; ++ } ++ ++ private void blink() { ++ getStyledDocument().setCharacterAttributes(start, length, SYNC_BLINK ? attr1 : attr2, true); ++ } ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/item/GlowBerryItem.java b/src/main/java/org/purpurmc/purpur/item/GlowBerryItem.java +new file mode 100644 +index 0000000000000000000000000000000000000000..7f526883495b3222746de3d0442e9e4fb5107036 +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/item/GlowBerryItem.java +@@ -0,0 +1,26 @@ ++package org.purpurmc.purpur.item; ++ ++import net.minecraft.server.level.ServerPlayer; ++import net.minecraft.world.effect.MobEffectInstance; ++import net.minecraft.world.effect.MobEffects; ++import net.minecraft.world.entity.LivingEntity; ++import net.minecraft.world.item.ItemNameBlockItem; ++import net.minecraft.world.item.ItemStack; ++import net.minecraft.world.level.Level; ++import net.minecraft.world.level.block.Block; ++import org.bukkit.event.entity.EntityPotionEffectEvent; ++ ++public class GlowBerryItem extends ItemNameBlockItem { ++ public GlowBerryItem(Block block, Properties settings) { ++ super(block, settings); ++ } ++ ++ @Override ++ public ItemStack finishUsingItem(ItemStack stack, Level world, LivingEntity user) { ++ ItemStack result = super.finishUsingItem(stack, world, user); ++ if (world.purpurConfig.glowBerriesEatGlowDuration > 0 && user instanceof ServerPlayer player) { ++ player.addEffect(new MobEffectInstance(MobEffects.GLOWING, world.purpurConfig.glowBerriesEatGlowDuration), EntityPotionEffectEvent.Cause.FOOD); ++ } ++ return result; ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/item/SpawnerItem.java b/src/main/java/org/purpurmc/purpur/item/SpawnerItem.java +new file mode 100644 +index 0000000000000000000000000000000000000000..ed50cb2115401c9039df4136caf5a087a5f5991c +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/item/SpawnerItem.java +@@ -0,0 +1,40 @@ ++package org.purpurmc.purpur.item; ++ ++import net.minecraft.core.BlockPos; ++import net.minecraft.core.component.DataComponents; ++import net.minecraft.nbt.CompoundTag; ++import net.minecraft.world.entity.EntityType; ++import net.minecraft.world.entity.player.Player; ++import net.minecraft.world.item.BlockItem; ++import net.minecraft.world.item.ItemStack; ++import net.minecraft.world.item.component.CustomData; ++import net.minecraft.world.level.Level; ++import net.minecraft.world.level.block.Block; ++import net.minecraft.world.level.block.entity.BlockEntity; ++import net.minecraft.world.level.block.entity.SpawnerBlockEntity; ++import net.minecraft.world.level.block.state.BlockState; ++ ++public class SpawnerItem extends BlockItem { ++ ++ public SpawnerItem(Block block, Properties settings) { ++ super(block, settings); ++ } ++ ++ @Override ++ protected boolean updateCustomBlockEntityTag(BlockPos pos, Level level, Player player, ItemStack stack, BlockState state) { ++ boolean handled = super.updateCustomBlockEntityTag(pos, level, player, stack, state); ++ if (level.purpurConfig.silkTouchEnabled && player.getBukkitEntity().hasPermission("purpur.place.spawners")) { ++ BlockEntity blockEntity = level.getBlockEntity(pos); ++ if (blockEntity instanceof SpawnerBlockEntity spawner) { ++ CompoundTag customData = stack.getOrDefault(DataComponents.CUSTOM_DATA, CustomData.EMPTY).copyTag(); ++ if (customData.contains("Purpur.mob_type")) { ++ EntityType.byString(customData.getString("Purpur.mob_type")).ifPresent(type -> spawner.getSpawner().setEntityId(type, level, level.random, pos)); ++ } else if (customData.contains("Purpur.SpawnData")) { ++ net.minecraft.world.level.SpawnData.CODEC.parse(net.minecraft.nbt.NbtOps.INSTANCE, customData.getCompound("Purpur.SpawnData")).result() ++ .ifPresent(spawnData -> spawner.getSpawner().nextSpawnData = spawnData); ++ } ++ } ++ } ++ return handled; ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/network/ClientboundBeehivePayload.java b/src/main/java/org/purpurmc/purpur/network/ClientboundBeehivePayload.java +new file mode 100644 +index 0000000000000000000000000000000000000000..57e195fd2d457295cda6c366684be5577aeef071 +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/network/ClientboundBeehivePayload.java +@@ -0,0 +1,27 @@ ++package org.purpurmc.purpur.network; ++ ++import net.minecraft.core.BlockPos; ++import net.minecraft.network.FriendlyByteBuf; ++import net.minecraft.network.codec.StreamCodec; ++import net.minecraft.network.protocol.common.custom.CustomPacketPayload; ++import net.minecraft.resources.ResourceLocation; ++import org.jetbrains.annotations.NotNull; ++ ++public record ClientboundBeehivePayload(BlockPos pos, int numOfBees) implements CustomPacketPayload { ++ public static final StreamCodec STREAM_CODEC = CustomPacketPayload.codec(ClientboundBeehivePayload::write, ClientboundBeehivePayload::new); ++ public static final Type TYPE = new Type<>(new ResourceLocation("purpur", "beehive_s2c")); ++ ++ public ClientboundBeehivePayload(FriendlyByteBuf friendlyByteBuf) { ++ this(friendlyByteBuf.readBlockPos(), friendlyByteBuf.readInt()); ++ } ++ ++ private void write(FriendlyByteBuf friendlyByteBuf) { ++ friendlyByteBuf.writeBlockPos(this.pos); ++ friendlyByteBuf.writeInt(this.numOfBees); ++ } ++ ++ @Override ++ public @NotNull Type type() { ++ return TYPE; ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/network/ServerboundBeehivePayload.java b/src/main/java/org/purpurmc/purpur/network/ServerboundBeehivePayload.java +new file mode 100644 +index 0000000000000000000000000000000000000000..27689754565bf048d1206d540913495d7194a54d +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/network/ServerboundBeehivePayload.java +@@ -0,0 +1,26 @@ ++package org.purpurmc.purpur.network; ++ ++import net.minecraft.core.BlockPos; ++import net.minecraft.network.FriendlyByteBuf; ++import net.minecraft.network.codec.StreamCodec; ++import net.minecraft.network.protocol.common.custom.CustomPacketPayload; ++import net.minecraft.resources.ResourceLocation; ++import org.jetbrains.annotations.NotNull; ++ ++public record ServerboundBeehivePayload(BlockPos pos) implements CustomPacketPayload { ++ public static final StreamCodec STREAM_CODEC = CustomPacketPayload.codec(ServerboundBeehivePayload::write, ServerboundBeehivePayload::new); ++ public static final Type TYPE = new Type<>(new ResourceLocation("purpur", "beehive_c2s")); ++ ++ public ServerboundBeehivePayload(FriendlyByteBuf friendlyByteBuf) { ++ this(friendlyByteBuf.readBlockPos()); ++ } ++ ++ private void write(FriendlyByteBuf friendlyByteBuf) { ++ friendlyByteBuf.writeBlockPos(this.pos); ++ } ++ ++ @Override ++ public @NotNull Type type() { ++ return TYPE; ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/task/BeehiveTask.java b/src/main/java/org/purpurmc/purpur/task/BeehiveTask.java +new file mode 100644 +index 0000000000000000000000000000000000000000..56fc359ea32228c2589ac30c9d00a9c4bea30db7 +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/task/BeehiveTask.java +@@ -0,0 +1,67 @@ ++package org.purpurmc.purpur.task; ++ ++import io.netty.buffer.Unpooled; ++import net.minecraft.network.FriendlyByteBuf; ++import net.minecraft.server.level.ServerPlayer; ++import net.minecraft.world.level.block.entity.BeehiveBlockEntity; ++import net.minecraft.world.level.block.entity.BlockEntity; ++import org.bukkit.Bukkit; ++import org.bukkit.craftbukkit.entity.CraftPlayer; ++import org.bukkit.craftbukkit.scheduler.MinecraftInternalPlugin; ++import org.bukkit.entity.Player; ++import org.bukkit.plugin.PluginBase; ++import org.bukkit.plugin.messaging.PluginMessageListener; ++import org.jetbrains.annotations.NotNull; ++import org.purpurmc.purpur.network.ClientboundBeehivePayload; ++import org.purpurmc.purpur.network.ServerboundBeehivePayload; ++ ++public class BeehiveTask implements PluginMessageListener { ++ ++ private static BeehiveTask instance; ++ ++ public static BeehiveTask instance() { ++ if (instance == null) { ++ instance = new BeehiveTask(); ++ } ++ return instance; ++ } ++ ++ private final PluginBase plugin = new MinecraftInternalPlugin(); ++ ++ private BeehiveTask() { ++ } ++ ++ public void register() { ++ Bukkit.getMessenger().registerOutgoingPluginChannel(this.plugin, ClientboundBeehivePayload.TYPE.id().toString()); ++ Bukkit.getMessenger().registerIncomingPluginChannel(this.plugin, ServerboundBeehivePayload.TYPE.id().toString(), this); ++ } ++ ++ public void unregister() { ++ Bukkit.getMessenger().unregisterOutgoingPluginChannel(this.plugin, ClientboundBeehivePayload.TYPE.id().toString()); ++ Bukkit.getMessenger().unregisterIncomingPluginChannel(this.plugin, ServerboundBeehivePayload.TYPE.id().toString()); ++ } ++ ++ @Override ++ public void onPluginMessageReceived(@NotNull String channel, @NotNull Player player, byte[] bytes) { ++ FriendlyByteBuf byteBuf = new FriendlyByteBuf(Unpooled.copiedBuffer(bytes)); ++ ServerboundBeehivePayload payload = ServerboundBeehivePayload.STREAM_CODEC.decode(byteBuf); ++ ++ ServerPlayer serverPlayer = ((CraftPlayer) player).getHandle(); ++ ++ // targeted block info max range specified in client at net.minecraft.client.gui.hud.DebugHud#render ++ if (!payload.pos().getCenter().closerThan(serverPlayer.position(), 20)) return; // Targeted Block info max range is 20 ++ if (serverPlayer.level().getChunkIfLoaded(payload.pos()) == null) return; ++ ++ BlockEntity blockEntity = serverPlayer.level().getBlockEntity(payload.pos()); ++ if (!(blockEntity instanceof BeehiveBlockEntity beehive)) { ++ return; ++ } ++ ++ ClientboundBeehivePayload customPacketPayload = new ClientboundBeehivePayload(payload.pos(), beehive.getOccupantCount()); ++ FriendlyByteBuf friendlyByteBuf = new FriendlyByteBuf(Unpooled.buffer()); ++ ClientboundBeehivePayload.STREAM_CODEC.encode(friendlyByteBuf, customPacketPayload); ++ byte[] byteArray = new byte[friendlyByteBuf.readableBytes()]; ++ friendlyByteBuf.readBytes(byteArray); ++ player.sendPluginMessage(this.plugin, customPacketPayload.type().id().toString(), byteArray); ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/task/BossBarTask.java b/src/main/java/org/purpurmc/purpur/task/BossBarTask.java +new file mode 100644 +index 0000000000000000000000000000000000000000..114f273dd7f8b8a3c02f0651f6944859b33a65d4 +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/task/BossBarTask.java +@@ -0,0 +1,121 @@ ++package org.purpurmc.purpur.task; ++ ++import net.kyori.adventure.bossbar.BossBar; ++import net.minecraft.server.level.ServerPlayer; ++import org.bukkit.Bukkit; ++import org.bukkit.craftbukkit.scheduler.MinecraftInternalPlugin; ++import org.bukkit.entity.Player; ++import org.bukkit.scheduler.BukkitRunnable; ++ ++import java.util.HashMap; ++import java.util.HashSet; ++import java.util.Iterator; ++import java.util.Map; ++import java.util.UUID; ++ ++public abstract class BossBarTask extends BukkitRunnable { ++ private final Map bossbars = new HashMap<>(); ++ private boolean started; ++ ++ abstract BossBar createBossBar(); ++ ++ abstract void updateBossBar(BossBar bossbar, Player player); ++ ++ @Override ++ public void run() { ++ Iterator> iter = bossbars.entrySet().iterator(); ++ while (iter.hasNext()) { ++ Map.Entry entry = iter.next(); ++ Player player = Bukkit.getPlayer(entry.getKey()); ++ if (player == null) { ++ iter.remove(); ++ continue; ++ } ++ updateBossBar(entry.getValue(), player); ++ } ++ } ++ ++ @Override ++ public void cancel() { ++ super.cancel(); ++ new HashSet<>(this.bossbars.keySet()).forEach(uuid -> { ++ Player player = Bukkit.getPlayer(uuid); ++ if (player != null) { ++ removePlayer(player); ++ } ++ }); ++ this.bossbars.clear(); ++ } ++ ++ public boolean removePlayer(Player player) { ++ BossBar bossbar = this.bossbars.remove(player.getUniqueId()); ++ if (bossbar != null) { ++ player.hideBossBar(bossbar); ++ return true; ++ } ++ return false; ++ } ++ ++ public void addPlayer(Player player) { ++ removePlayer(player); ++ BossBar bossbar = createBossBar(); ++ this.bossbars.put(player.getUniqueId(), bossbar); ++ this.updateBossBar(bossbar, player); ++ player.showBossBar(bossbar); ++ } ++ ++ public boolean hasPlayer(UUID uuid) { ++ return this.bossbars.containsKey(uuid); ++ } ++ ++ public boolean togglePlayer(Player player) { ++ if (removePlayer(player)) { ++ return false; ++ } ++ addPlayer(player); ++ return true; ++ } ++ ++ public void start() { ++ stop(); ++ this.runTaskTimerAsynchronously(new MinecraftInternalPlugin(), 1, 1); ++ started = true; ++ } ++ ++ public void stop() { ++ if (started) { ++ cancel(); ++ } ++ } ++ ++ public static void startAll() { ++ RamBarTask.instance().start(); ++ TPSBarTask.instance().start(); ++ CompassTask.instance().start(); ++ } ++ ++ public static void stopAll() { ++ RamBarTask.instance().stop(); ++ TPSBarTask.instance().stop(); ++ CompassTask.instance().stop(); ++ } ++ ++ public static void addToAll(ServerPlayer player) { ++ Player bukkit = player.getBukkitEntity(); ++ if (player.ramBar()) { ++ RamBarTask.instance().addPlayer(bukkit); ++ } ++ if (player.tpsBar()) { ++ TPSBarTask.instance().addPlayer(bukkit); ++ } ++ if (player.compassBar()) { ++ CompassTask.instance().addPlayer(bukkit); ++ } ++ } ++ ++ public static void removeFromAll(Player player) { ++ RamBarTask.instance().removePlayer(player); ++ TPSBarTask.instance().removePlayer(player); ++ CompassTask.instance().removePlayer(player); ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/task/CompassTask.java b/src/main/java/org/purpurmc/purpur/task/CompassTask.java +new file mode 100644 +index 0000000000000000000000000000000000000000..bece7eefc8ba8822b433835526251d2fb916c025 +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/task/CompassTask.java +@@ -0,0 +1,68 @@ ++package org.purpurmc.purpur.task; ++ ++import net.kyori.adventure.bossbar.BossBar; ++import net.kyori.adventure.text.Component; ++import net.minecraft.server.MinecraftServer; ++import net.minecraft.world.item.Items; ++import org.bukkit.entity.Player; ++import org.purpurmc.purpur.PurpurConfig; ++ ++public class CompassTask extends BossBarTask { ++ private static CompassTask instance; ++ ++ private int tick = 0; ++ ++ public static CompassTask instance() { ++ if (instance == null) { ++ instance = new CompassTask(); ++ } ++ return instance; ++ } ++ ++ @Override ++ public void run() { ++ if (++tick < PurpurConfig.commandCompassBarTickInterval) { ++ return; ++ } ++ tick = 0; ++ ++ MinecraftServer.getServer().getAllLevels().forEach((level) -> { ++ if (level.purpurConfig.compassItemShowsBossBar) { ++ level.players().forEach(player -> { ++ if (!player.compassBar()) { ++ if (player.getMainHandItem().getItem() != Items.COMPASS && player.getOffhandItem().getItem() != Items.COMPASS) { ++ removePlayer(player.getBukkitEntity()); ++ } else if (!hasPlayer(player.getUUID())) { ++ addPlayer(player.getBukkitEntity()); ++ } ++ } ++ }); ++ } ++ }); ++ ++ super.run(); ++ } ++ ++ @Override ++ BossBar createBossBar() { ++ return BossBar.bossBar(Component.text(""), PurpurConfig.commandCompassBarProgressPercent, PurpurConfig.commandCompassBarProgressColor, PurpurConfig.commandCompassBarProgressOverlay); ++ } ++ ++ @Override ++ void updateBossBar(BossBar bossbar, Player player) { ++ float yaw = player.getLocation().getYaw(); ++ int length = PurpurConfig.commandCompassBarTitle.length(); ++ int pos = (int) ((normalize(yaw) * (length / 720F)) + (length / 2F)); ++ bossbar.name(Component.text(PurpurConfig.commandCompassBarTitle.substring(pos - 25, pos + 25))); ++ } ++ ++ private float normalize(float yaw) { ++ while (yaw < -180.0F) { ++ yaw += 360.0F; ++ } ++ while (yaw > 180.0F) { ++ yaw -= 360.0F; ++ } ++ return yaw; ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/task/RamBarTask.java b/src/main/java/org/purpurmc/purpur/task/RamBarTask.java +new file mode 100644 +index 0000000000000000000000000000000000000000..8e98c0ae73e2c40002a72b5d0d246ffa0c3ab38f +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/task/RamBarTask.java +@@ -0,0 +1,117 @@ ++package org.purpurmc.purpur.task; ++ ++import net.kyori.adventure.bossbar.BossBar; ++import net.kyori.adventure.text.Component; ++import net.kyori.adventure.text.minimessage.MiniMessage; ++import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; ++import org.bukkit.entity.Player; ++import org.purpurmc.purpur.PurpurConfig; ++ ++import java.lang.management.ManagementFactory; ++import java.lang.management.MemoryUsage; ++ ++public class RamBarTask extends BossBarTask { ++ private static RamBarTask instance; ++ private long allocated = 0L; ++ private long used = 0L; ++ private long xmx = 0L; ++ private long xms = 0L; ++ private float percent = 0F; ++ private int tick = 0; ++ ++ public static RamBarTask instance() { ++ if (instance == null) { ++ instance = new RamBarTask(); ++ } ++ return instance; ++ } ++ ++ @Override ++ BossBar createBossBar() { ++ return BossBar.bossBar(Component.text(""), 0.0F, instance().getBossBarColor(), PurpurConfig.commandRamBarProgressOverlay); ++ } ++ ++ @Override ++ void updateBossBar(BossBar bossbar, Player player) { ++ bossbar.progress(getBossBarProgress()); ++ bossbar.color(getBossBarColor()); ++ bossbar.name(MiniMessage.miniMessage().deserialize(PurpurConfig.commandRamBarTitle, ++ Placeholder.component("allocated", format(this.allocated)), ++ Placeholder.component("used", format(this.used)), ++ Placeholder.component("xmx", format(this.xmx)), ++ Placeholder.component("xms", format(this.xms)), ++ Placeholder.unparsed("percent", ((int) (this.percent * 100)) + "%") ++ )); ++ } ++ ++ @Override ++ public void run() { ++ if (++this.tick < PurpurConfig.commandRamBarTickInterval) { ++ return; ++ } ++ this.tick = 0; ++ ++ MemoryUsage heap = ManagementFactory.getMemoryMXBean().getHeapMemoryUsage(); ++ ++ this.allocated = heap.getCommitted(); ++ this.used = heap.getUsed(); ++ this.xmx = heap.getMax(); ++ this.xms = heap.getInit(); ++ this.percent = Math.max(Math.min((float) this.used / this.xmx, 1.0F), 0.0F); ++ ++ super.run(); ++ } ++ ++ private float getBossBarProgress() { ++ return this.percent; ++ } ++ ++ private BossBar.Color getBossBarColor() { ++ if (this.percent < 0.5F) { ++ return PurpurConfig.commandRamBarProgressColorGood; ++ } else if (this.percent < 0.75F) { ++ return PurpurConfig.commandRamBarProgressColorMedium; ++ } else { ++ return PurpurConfig.commandRamBarProgressColorLow; ++ } ++ } ++ ++ public Component format(long v) { ++ String color; ++ if (this.percent < 0.60F) { ++ color = PurpurConfig.commandRamBarTextColorGood; ++ } else if (this.percent < 0.85F) { ++ color = PurpurConfig.commandRamBarTextColorMedium; ++ } else { ++ color = PurpurConfig.commandRamBarTextColorLow; ++ } ++ String value; ++ if (v < 1024) { ++ value = v + "B"; ++ } else { ++ int z = (63 - Long.numberOfLeadingZeros(v)) / 10; ++ value = String.format("%.1f%s", (double) v / (1L << (z * 10)), "BKMGTPE".charAt(z)); ++ } ++ return MiniMessage.miniMessage().deserialize(color, Placeholder.unparsed("text", value)); ++ } ++ ++ public long getAllocated() { ++ return this.allocated; ++ } ++ ++ public long getUsed() { ++ return this.used; ++ } ++ ++ public long getXmx() { ++ return this.xmx; ++ } ++ ++ public long getXms() { ++ return this.xms; ++ } ++ ++ public float getPercent() { ++ return this.percent; ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/task/TPSBarTask.java b/src/main/java/org/purpurmc/purpur/task/TPSBarTask.java +new file mode 100644 +index 0000000000000000000000000000000000000000..8769993e7ca59da309087051a3cd38fc562c15d1 +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/task/TPSBarTask.java +@@ -0,0 +1,142 @@ ++package org.purpurmc.purpur.task; ++ ++import net.kyori.adventure.bossbar.BossBar; ++import net.kyori.adventure.text.Component; ++import net.kyori.adventure.text.minimessage.MiniMessage; ++import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; ++import org.purpurmc.purpur.PurpurConfig; ++import org.bukkit.Bukkit; ++import org.bukkit.entity.Player; ++ ++public class TPSBarTask extends BossBarTask { ++ private static TPSBarTask instance; ++ private double tps = 20.0D; ++ private double mspt = 0.0D; ++ private int tick = 0; ++ ++ public static TPSBarTask instance() { ++ if (instance == null) { ++ instance = new TPSBarTask(); ++ } ++ return instance; ++ } ++ ++ @Override ++ BossBar createBossBar() { ++ return BossBar.bossBar(Component.text(""), 0.0F, instance().getBossBarColor(), PurpurConfig.commandTPSBarProgressOverlay); ++ } ++ ++ @Override ++ void updateBossBar(BossBar bossbar, Player player) { ++ bossbar.progress(getBossBarProgress()); ++ bossbar.color(getBossBarColor()); ++ bossbar.name(MiniMessage.miniMessage().deserialize(PurpurConfig.commandTPSBarTitle, ++ Placeholder.component("tps", getTPSColor()), ++ Placeholder.component("mspt", getMSPTColor()), ++ Placeholder.component("ping", getPingColor(player.getPing())) ++ )); ++ } ++ ++ @Override ++ public void run() { ++ if (++tick < PurpurConfig.commandTPSBarTickInterval) { ++ return; ++ } ++ tick = 0; ++ ++ this.tps = Math.max(Math.min(Bukkit.getTPS()[0], 20.0D), 0.0D); ++ this.mspt = Bukkit.getAverageTickTime(); ++ ++ super.run(); ++ } ++ ++ private float getBossBarProgress() { ++ if (PurpurConfig.commandTPSBarProgressFillMode == FillMode.MSPT) { ++ return Math.max(Math.min((float) mspt / 50.0F, 1.0F), 0.0F); ++ } else { ++ return Math.max(Math.min((float) tps / 20.0F, 1.0F), 0.0F); ++ } ++ } ++ ++ private BossBar.Color getBossBarColor() { ++ if (isGood(PurpurConfig.commandTPSBarProgressFillMode)) { ++ return PurpurConfig.commandTPSBarProgressColorGood; ++ } else if (isMedium(PurpurConfig.commandTPSBarProgressFillMode)) { ++ return PurpurConfig.commandTPSBarProgressColorMedium; ++ } else { ++ return PurpurConfig.commandTPSBarProgressColorLow; ++ } ++ } ++ ++ private boolean isGood(FillMode mode) { ++ return isGood(mode, 0); ++ } ++ ++ private boolean isGood(FillMode mode, int ping) { ++ if (mode == FillMode.MSPT) { ++ return mspt < 40; ++ } else if (mode == FillMode.TPS) { ++ return tps >= 19; ++ } else if (mode == FillMode.PING) { ++ return ping < 100; ++ } else { ++ return false; ++ } ++ } ++ ++ private boolean isMedium(FillMode mode) { ++ return isMedium(mode, 0); ++ } ++ ++ private boolean isMedium(FillMode mode, int ping) { ++ if (mode == FillMode.MSPT) { ++ return mspt < 50; ++ } else if (mode == FillMode.TPS) { ++ return tps >= 15; ++ } else if (mode == FillMode.PING) { ++ return ping < 200; ++ } else { ++ return false; ++ } ++ } ++ ++ private Component getTPSColor() { ++ String color; ++ if (isGood(FillMode.TPS)) { ++ color = PurpurConfig.commandTPSBarTextColorGood; ++ } else if (isMedium(FillMode.TPS)) { ++ color = PurpurConfig.commandTPSBarTextColorMedium; ++ } else { ++ color = PurpurConfig.commandTPSBarTextColorLow; ++ } ++ return MiniMessage.miniMessage().deserialize(color, Placeholder.parsed("text", String.format("%.2f", tps))); ++ } ++ ++ private Component getMSPTColor() { ++ String color; ++ if (isGood(FillMode.MSPT)) { ++ color = PurpurConfig.commandTPSBarTextColorGood; ++ } else if (isMedium(FillMode.MSPT)) { ++ color = PurpurConfig.commandTPSBarTextColorMedium; ++ } else { ++ color = PurpurConfig.commandTPSBarTextColorLow; ++ } ++ return MiniMessage.miniMessage().deserialize(color, Placeholder.parsed("text", String.format("%.2f", mspt))); ++ } ++ ++ private Component getPingColor(int ping) { ++ String color; ++ if (isGood(FillMode.PING, ping)) { ++ color = PurpurConfig.commandTPSBarTextColorGood; ++ } else if (isMedium(FillMode.PING, ping)) { ++ color = PurpurConfig.commandTPSBarTextColorMedium; ++ } else { ++ color = PurpurConfig.commandTPSBarTextColorLow; ++ } ++ return MiniMessage.miniMessage().deserialize(color, Placeholder.parsed("text", String.format("%s", ping))); ++ } ++ ++ public enum FillMode { ++ TPS, MSPT, PING ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/tool/Actionable.java b/src/main/java/org/purpurmc/purpur/tool/Actionable.java +new file mode 100644 +index 0000000000000000000000000000000000000000..e18c37f06730da9d3055d5215e813b1477c1e70e +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/tool/Actionable.java +@@ -0,0 +1,24 @@ ++package org.purpurmc.purpur.tool; ++ ++import net.minecraft.world.item.Item; ++import net.minecraft.world.level.block.Block; ++ ++import java.util.Map; ++ ++public abstract class Actionable { ++ private final Block into; ++ private final Map drops; ++ ++ public Actionable(Block into, Map drops) { ++ this.into = into; ++ this.drops = drops; ++ } ++ ++ public Block into() { ++ return into; ++ } ++ ++ public Map drops() { ++ return drops; ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/tool/Flattenable.java b/src/main/java/org/purpurmc/purpur/tool/Flattenable.java +new file mode 100644 +index 0000000000000000000000000000000000000000..345d4ee4ff0b78bd1050959711a4f5d16a5e8aee +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/tool/Flattenable.java +@@ -0,0 +1,12 @@ ++package org.purpurmc.purpur.tool; ++ ++import net.minecraft.world.item.Item; ++import net.minecraft.world.level.block.Block; ++ ++import java.util.Map; ++ ++public class Flattenable extends Actionable { ++ public Flattenable(Block into, Map drops) { ++ super(into, drops); ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/tool/Strippable.java b/src/main/java/org/purpurmc/purpur/tool/Strippable.java +new file mode 100644 +index 0000000000000000000000000000000000000000..bf5402214f41af9c09bd6c5c4f45d330516d742e +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/tool/Strippable.java +@@ -0,0 +1,12 @@ ++package org.purpurmc.purpur.tool; ++ ++import net.minecraft.world.item.Item; ++import net.minecraft.world.level.block.Block; ++ ++import java.util.Map; ++ ++public class Strippable extends Actionable { ++ public Strippable(Block into, Map drops) { ++ super(into, drops); ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/tool/Tillable.java b/src/main/java/org/purpurmc/purpur/tool/Tillable.java +new file mode 100644 +index 0000000000000000000000000000000000000000..715f6dd44480347eebced43c11bc364e05727498 +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/tool/Tillable.java +@@ -0,0 +1,50 @@ ++package org.purpurmc.purpur.tool; ++ ++import net.minecraft.world.item.HoeItem; ++import net.minecraft.world.item.Item; ++import net.minecraft.world.item.context.UseOnContext; ++import net.minecraft.world.level.block.Block; ++ ++import java.util.HashMap; ++import java.util.Map; ++import java.util.function.Predicate; ++ ++public class Tillable extends Actionable { ++ private final Condition condition; ++ ++ public Tillable(Condition condition, Block into, Map drops) { ++ super(into, drops); ++ this.condition = condition; ++ } ++ ++ public Condition condition() { ++ return condition; ++ } ++ ++ public enum Condition { ++ AIR_ABOVE(HoeItem::onlyIfAirAbove), ++ ALWAYS((useOnContext) -> true); ++ ++ private final Predicate predicate; ++ ++ Condition(Predicate predicate) { ++ this.predicate = predicate; ++ } ++ ++ public Predicate predicate() { ++ return predicate; ++ } ++ ++ private static final Map BY_NAME = new HashMap<>(); ++ ++ static { ++ for (Condition condition : values()) { ++ BY_NAME.put(condition.name(), condition); ++ } ++ } ++ ++ public static Condition get(String name) { ++ return BY_NAME.get(name.toUpperCase(java.util.Locale.ROOT)); ++ } ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/tool/Waxable.java b/src/main/java/org/purpurmc/purpur/tool/Waxable.java +new file mode 100644 +index 0000000000000000000000000000000000000000..64adb13b29b6757dcf227a55588da70ecabe083f +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/tool/Waxable.java +@@ -0,0 +1,12 @@ ++package org.purpurmc.purpur.tool; ++ ++import net.minecraft.world.item.Item; ++import net.minecraft.world.level.block.Block; ++ ++import java.util.Map; ++ ++public class Waxable extends Actionable { ++ public Waxable(Block into, Map drops) { ++ super(into, drops); ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/tool/Weatherable.java b/src/main/java/org/purpurmc/purpur/tool/Weatherable.java +new file mode 100644 +index 0000000000000000000000000000000000000000..b7586f494528f30eb0da82420d3bcf5b83a1a902 +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/tool/Weatherable.java +@@ -0,0 +1,12 @@ ++package org.purpurmc.purpur.tool; ++ ++import net.minecraft.world.item.Item; ++import net.minecraft.world.level.block.Block; ++ ++import java.util.Map; ++ ++public class Weatherable extends Actionable { ++ public Weatherable(Block into, Map drops) { ++ super(into, drops); ++ } ++} +diff --git a/src/main/java/org/spigotmc/ActivationRange.java b/src/main/java/org/spigotmc/ActivationRange.java +index 3283ed99c35ffed6805567705e0518d9f84feedc..de2b469f06f6679aed1d20156052bfbef5e7c30b 100644 +--- a/src/main/java/org/spigotmc/ActivationRange.java ++++ b/src/main/java/org/spigotmc/ActivationRange.java +@@ -15,6 +15,7 @@ import net.minecraft.world.entity.ambient.AmbientCreature; + import net.minecraft.world.entity.animal.Animal; + import net.minecraft.world.entity.animal.Bee; + import net.minecraft.world.entity.animal.Sheep; ++import net.minecraft.world.entity.animal.Squid; + import net.minecraft.world.entity.animal.WaterAnimal; + import net.minecraft.world.entity.animal.horse.Llama; + import net.minecraft.world.entity.boss.EnderDragonPart; +@@ -167,7 +168,7 @@ public class ActivationRange + */ + public static void activateEntities(Level world) + { +- MinecraftTimings.entityActivationCheckTimer.startTiming(); ++ //MinecraftTimings.entityActivationCheckTimer.startTiming(); // Purpur + final int miscActivationRange = world.spigotConfig.miscActivationRange; + final int raiderActivationRange = world.spigotConfig.raiderActivationRange; + final int animalActivationRange = world.spigotConfig.animalActivationRange; +@@ -201,6 +202,7 @@ public class ActivationRange + continue; + } + ++ if (!player.level().purpurConfig.idleTimeoutTickNearbyEntities && player.isAfk()) continue; // Purpur + // Paper start + int worldHeight = world.getHeight(); + ActivationRange.maxBB = player.getBoundingBox().inflate( maxRange, worldHeight, maxRange ); +@@ -226,7 +228,7 @@ public class ActivationRange + } + // Paper end + } +- MinecraftTimings.entityActivationCheckTimer.stopTiming(); ++ //MinecraftTimings.entityActivationCheckTimer.stopTiming(); // Purpur + } + + /** +@@ -379,6 +381,7 @@ public class ActivationRange + */ + public static boolean checkIfActive(Entity entity) + { ++ if (entity.level().purpurConfig.squidImmuneToEAR && entity instanceof Squid) return true; // Purpur + // Never safe to skip fireworks or entities not yet added to chunk + if ( entity instanceof FireworkRocketEntity ) { + return true; +diff --git a/src/main/java/org/spigotmc/TicksPerSecondCommand.java b/src/main/java/org/spigotmc/TicksPerSecondCommand.java +index 9eb2823cc8f83bad2626fc77578b0162d9ed5782..f144a08e88f8268b84eb188a36bf470457f59958 100644 +--- a/src/main/java/org/spigotmc/TicksPerSecondCommand.java ++++ b/src/main/java/org/spigotmc/TicksPerSecondCommand.java +@@ -39,7 +39,7 @@ public class TicksPerSecondCommand extends Command + } + + net.kyori.adventure.text.TextComponent.Builder builder = net.kyori.adventure.text.Component.text(); +- builder.append(net.kyori.adventure.text.Component.text("TPS from last 1m, 5m, 15m: ", net.kyori.adventure.text.format.NamedTextColor.GOLD)); ++ builder.append(net.kyori.adventure.text.Component.text("TPS from last 5s, 1m, 5m, 15m: ", net.kyori.adventure.text.format.NamedTextColor.GOLD)); // Purpur + builder.append(net.kyori.adventure.text.Component.join(net.kyori.adventure.text.JoinConfiguration.commas(true), tpsAvg)); + sender.sendMessage(builder.asComponent()); + if (args.length > 0 && args[0].equals("mem") && sender.hasPermission("bukkit.command.tpsmemory")) { +diff --git a/src/main/java/org/spigotmc/WatchdogThread.java b/src/main/java/org/spigotmc/WatchdogThread.java +index 6db566e3111ec08a99aa429624979cb83a85e272..a353eb9f45af7b7f9bfd92a4a89403335b841840 100644 +--- a/src/main/java/org/spigotmc/WatchdogThread.java ++++ b/src/main/java/org/spigotmc/WatchdogThread.java +@@ -96,7 +96,7 @@ public final class WatchdogThread extends io.papermc.paper.util.TickThread // Pa + + private WatchdogThread(long timeoutTime, boolean restart) + { +- super( "Paper Watchdog Thread" ); ++ super( "Watchdog Thread" ); // Purpur - use a generic name + this.timeoutTime = timeoutTime; + this.restart = restart; + earlyWarningEvery = Math.min(io.papermc.paper.configuration.GlobalConfiguration.get().watchdog.earlyWarningEvery, timeoutTime); // Paper +@@ -155,14 +155,14 @@ public final class WatchdogThread extends io.papermc.paper.util.TickThread // Pa + if (isLongTimeout) { + // Paper end + log.log( Level.SEVERE, "------------------------------" ); +- log.log( Level.SEVERE, "The server has stopped responding! This is (probably) not a Paper bug." ); // Paper ++ log.log( Level.SEVERE, "The server has stopped responding! This is (probably) not a Purpur bug." ); // Paper // Purpur + log.log( Level.SEVERE, "If you see a plugin in the Server thread dump below, then please report it to that author" ); + log.log( Level.SEVERE, "\t *Especially* if it looks like HTTP or MySQL operations are occurring" ); + log.log( Level.SEVERE, "If you see a world save or edit, then it means you did far more than your server can handle at once" ); + log.log( Level.SEVERE, "\t If this is the case, consider increasing timeout-time in spigot.yml but note that this will replace the crash with LARGE lag spikes" ); +- log.log( Level.SEVERE, "If you are unsure or still think this is a Paper bug, please report this to https://github.com/PaperMC/Paper/issues" ); ++ log.log( Level.SEVERE, "If you are unsure or still think this is a Purpur bug, please report this to https://github.com/PurpurMC/Purpur/issues" ); // Purpur + log.log( Level.SEVERE, "Be sure to include ALL relevant console errors and Minecraft crash reports" ); +- log.log( Level.SEVERE, "Paper version: " + Bukkit.getServer().getVersion() ); ++ log.log( Level.SEVERE, "Purpur version: " + Bukkit.getServer().getVersion() ); // Purpur + // + if ( net.minecraft.world.level.Level.lastPhysicsProblem != null ) + { +@@ -184,12 +184,12 @@ public final class WatchdogThread extends io.papermc.paper.util.TickThread // Pa + // Paper end + } else + { +- log.log(Level.SEVERE, "--- DO NOT REPORT THIS TO PAPER - THIS IS NOT A BUG OR A CRASH - " + Bukkit.getServer().getVersion() + " ---"); ++ log.log(Level.SEVERE, "--- DO NOT REPORT THIS TO PURPUR - THIS IS NOT A BUG OR A CRASH - " + Bukkit.getServer().getVersion() + " ---"); // Purpur + log.log(Level.SEVERE, "The server has not responded for " + (currentTime - lastTick) / 1000 + " seconds! Creating thread dump"); + } + // Paper end - Different message for short timeout + log.log( Level.SEVERE, "------------------------------" ); +- log.log( Level.SEVERE, "Server thread dump (Look for plugins here before reporting to Paper!):" ); // Paper ++ log.log( Level.SEVERE, "Server thread dump (Look for plugins here before reporting to Purpur!):" ); // Paper // Purpur + io.papermc.paper.chunk.system.scheduling.ChunkTaskScheduler.dumpAllChunkLoadInfo(isLongTimeout); // Paper - rewrite chunk system + this.dumpTickingInfo(); // Paper - log detailed tick information + WatchdogThread.dumpThread( ManagementFactory.getThreadMXBean().getThreadInfo( MinecraftServer.getServer().serverThread.getId(), Integer.MAX_VALUE ), log ); +@@ -205,7 +205,7 @@ public final class WatchdogThread extends io.papermc.paper.util.TickThread // Pa + WatchdogThread.dumpThread( thread, log ); + } + } else { +- log.log(Level.SEVERE, "--- DO NOT REPORT THIS TO PAPER - THIS IS NOT A BUG OR A CRASH ---"); ++ log.log(Level.SEVERE, "--- DO NOT REPORT THIS TO PURPUR - THIS IS NOT A BUG OR A CRASH ---"); // Purpur + } + + log.log( Level.SEVERE, "------------------------------" ); +diff --git a/src/main/resources/log4j2.xml b/src/main/resources/log4j2.xml +index d2a75850af9c6ad2aca66a5f994f1b587d73eac4..a056aa167887abef9e6d531a9edd2cda433567d2 100644 +--- a/src/main/resources/log4j2.xml ++++ b/src/main/resources/log4j2.xml +@@ -2,7 +2,16 @@ + + + +- ++ ++ ++ ++ ++ ++ ++ ++ ++ + + + +diff --git a/src/main/resources/logo.png b/src/main/resources/logo.png +index 8b924977b7886df9ab8790b1e4ff9b1c04a2af45..518591dd83289e041a16e2c2e7d7e7640d4b2e1b 100644 +GIT binary patch +literal 9260 +zcmWk!Wmptl7+qlLS~|W3b}1=IK|*5b5@|#_l0AUP +zC6t~Ie$33hbMG7HJ?G9mbDv4n)lnlSVI~2AK;# +z4OMQt9~6LaN1#s_Fh>FQQNXigV2~Fm&;xWcfNA!dm*#+B)G7nz8bF6QFw6s>Er4EOK&%?bF$4;AfiW&% +zoCBC(2Ns!cigf^wAiz2NZqBnLz$Fx@R=#VuNdgNjK)EK+fB@F$fJ{?BsqoI$ED5Mo +z1rqE4Uti#zCSVl@%tLSX$$)HQ;Jq4~ho`CY^a7vP8(5UvMwUftX}AK<&Iwsa{VUUgUh +z2@c>vB_LZ0XlDV+Z-B?IZf;?2Q{C)P0>ZVx9Q3a7hXhz;0*3DaKX~ub_6P=EnF2ok +zcR7!90-gbPNmi@e3BV~F2=oSCMBV|pg>WmCTgLr-01Q8169d>q-)SS&0%(`GBd$p2 +zE<~Lo5bOy|!S8arrBXcyc!~mEJ_FLt@08Ob4a7eO9#jGH#Xy)lfU>@0405w;9)s13%z-g3FI09k4gZC@SEGh21MSdMw@%zE`WMpeH{Z3F$F%s4K+W4gOWCo +zM2#hp$Mq4nbl;~?VJ4luX8W7#1E`a&x!wY&zb88l +zN1p;u{w(({h5e>J1%Y6K8p;U6z`4mh7wrra#_v{h<9beb_Uu_ssLuklU5mIC>bM*t +zcnDp3!57GGcIWN~=GwhPk`|qj+4X29PCWJ%!mm8dv^)i!1KFLpM5tu4Zu)l4x1`+4 +zuool8`RpVod-L>kH&(y$T!#xcuSSC206r^ApC6SB-*ez^5f|E=>CVr`YArBlJ*aH= +zv9&F3YdaKfe*MZ~i5LpP{mi>QT}i><&#tzbf_0y?pPchedeE97X-j8))~zv`+-R^c +zXW+`~MJv5ye2+%Kvb|-$zve#6Fq3j>e(Z$inlS;)z#xljVJ?019I=wkGZKCb>mY`{ +zC8KIq#mXl@3vR}_b~1^fB_&=iJ$$n&3S*05tirE<8!l{ZZCzis{#|hXxvFTM`o}iR +zq}ASGAx!t@bKd6MjeH3iBEIB}%Yan>#e?6>$^PrcHJlA)z3Hwi9`j3t3g5m_#CTR2 +zJWL`g(S4CEDe2|vYOlPr-diIV^oy)GsZk29-%}jHck!wdB5f==uk13XY9^5>Drj(Jal(oLc`8 +z7D=J9cm#rVYK(uzdmUKaOiVbY*(WAOkmLNmxVRVy4%oluYjcE3ejCZtD>w+KsRMI; +z87X<1i$@!2V<|7#NM2W6ZEQ5}eehW7L)rSeirL`4wOgYhpON3G_`a-$K8jR7b1;WL +z!~?6_@enfHT1uN@893H{iZNi1|9834^3*)D`mGffWf%2a)wvAK&u>(jHANwD#zmPX +zb+%6usxsHvd2-@g4UX8e(%gfc=yCIMYT;LE&!w^Hm;hjMmbIls&Ko=!h)JjeBiK7j +zJg8+Wwf=llHxatzP37JYP2MYV!uiXB_G}g(g)9DJKdnb9^WJMK(TfEmW8+Fb{$ocI +zd_IJNIpT-3}l*&5tlx{G{r8LcDpgZGm&_LCN*L6GgC7m* +z9Ckq~9^@3|Q4>gQQNDOZ2HyVd9pW<)EoE)RyU2C+kH**{1?C-Xq|NDRXjCBSZu!dW +zkWoaF%%=W+tXxD?vrJiW1>QdCcU?IwSAAVKXSF08Ri&so^yCRhU8OJJ-7;U}%kOXh +z;uh`2naxh}@bu)!tqCu)Xf)+K3I8Q5)ZcBvHOp43NX8R{ud%yohLd|fFsJPE=-WnM +zy9)c2e5+K_=dT7ym;4dn(y2MkPN*C+c_fH{ZfAWh`@@h_TYs$2*8y!HC~F*!SDduQi((Yakpx9^)x +zJx6lr@C*IFi6C32_c%iv6B`G$;M6Q37L;6Uui3x9r{WSzT7DwjzcfM?D1LbD#A$qd +zu4>LOEHC&2!$+8)#A2;xEg(p|Jdy(=RRcM>kUoYG*Qz;+&oyU511wbY(v-jRqxsns +z%#|?EAim6T>_zCj2<-iPVp$G)<1~m`qSX7m3`(@)$3!@_Il;X3RXlkq+00DbtS7gY +z>#I3K0|TFc5y9cN<3t=oP_kgwm69oEvtnYz&nRnvO7Njr5W~StAj9NIY?n)l*H$Iz +z2YURl{gS{bBrJXar($8yfT4n#w=5%{AEV-@Grkiy0_J +z=#2;?ULHsk`lZB!Pq?X9XYS_xO{hASO}zKeLGkV?N1%z +z>{sH;{En>vxtZU%K&AU5%Gwj}B-+Pkl=AbytOz{;6ed;B5v9!jh)%X6>$P18-6BW0 +z9}ExHska~pvMsOcE?mMPyWj`s*pbbrIhx_ij%6Esl?wROHn#vuvN1k)+M*(8X8`A{ +zS4o(sjn)kE#;i6MtveA?5(&g98!Mcr!J5=}Qa@!L1e14aJEQmcLzXKl5nn2bAJ$tZtP$P8Z1 +zZ>}d+7jWYR^n<^KOqhO^N==PYrD)O9EEFNs=im5ICPNsHVP!Zx`PfqbpT-5=sn650 +zHSeafmr~){Zr!JcC1%$s2t+!}=}Ip+y3BYtETmoWiR}1P><_9ZZ0FT%gEZStrgdbp +zI0aw6jiD;PvU!g!GH_nZQDTX%5h%9pk4-fdm}0u~yzd;=Cni~sWY3r;O}XL;q&8-M +z47pHP%S*mY4oBx}PU^}#j%4iThs7lM7N=#@Fdn{XdiIW0L(=C2~Fu=G5&Vq%Yr +zY&qWfrH35E^L#BnOgMwRc8B)xMX+GlbUbtr`nPrR=jS(Tb=kQ6o7eVqUZd`0fo9D@ +z`vyo59#*A!saE#!$G7Cxfh7n>ZWX!0gkro``0W{b9=XxNGqu#u6^e-7Kk1lUvA@1| +zbiNNGZAv0UHvN5m{KO8QNS~$+u-B>s_5L`L`jBdrke`Xe{0N +zj*umKyN|_A>O3@@8y|M457h5tzNEqIDV$3|c%QOiW7;H(RNLM9W0najA-;HEwdD>u +zkW^fo^J0f?_u(^NWw(nT>JS~~smIbnC8QxFcuQkHNQUJyj;eYMTmvVN^O|RPDvczq +z>kslY@C$0YBmZR=Y@IEsCWJ9}K@HOu`c4n=$kr9!;n+9We=f8unlK5@c*o|I7(=zG +zS}lWvH_fp;i}4qt9>h_?QzN~ap@7nLh*7DJwfvLZAS*_S4BW3*FwwCv)lD1Vg1t7>@!DLp5Su@bc$d;D_VX! +z57eDlJm5IkuTyc>aw+Wt#V5hkX-B+t+|!Fa@@x7=`8`3dZm3$aHF*y2VcD$(D>J3y +z%YwpO#gY%mqyin8q9uH?7OBC}Uv-@)Hl0)Z2+y3x`{XluljPt{<4TU-m$XDvYU9HDGKSg&cT(@MVe23D +z<wCdy-3tTl}j4kzaCmrni9Diizn#a{@0 +zu;<>ZBofe$`#ML)Z*Aj8mbPS3-(VgSdF3LA8LJgBPj{3IaB~uLBuylKO*>ev*i|tuS(Wff3`Ogj^{GsE +zb-&NcY_)d|4#vv+AW}skSWrN{p$KOp?oug)BPoM*O}*h=wrr-PGYCt{qwP-&I!~hn +z&+*{EI5^09slk8i%kFcTjCcO6Oc}M9iiS}cuLLw0fyMPfjZ_M`;HWDHP^ke=f*5^! +z(0!2p!N9nZ8J+AP5sfunmbw64l@2)3@53_`Q@g&4FYKeDLbz2F-Pl@Er#INAtM()n +z<;MFT;osC}L2=Y4P1?cm)V{7nauSgh +zx4)k@#lLT}(uM?{Z&NvzT8pnJk@-MDvv4wOKsY+l9S8OdOw}cex#$t*B6ppAEloQy +z8u`9L`Ky$*$*G}A=)h6BjVn=_YuPie5epXe0vLKZP=Pxps%a&uGrdg4y#R{YobnDn +zWlMl5J;9|5ev`f`BO3Uh(yuK%@i^`+fAimq+_>*)z(;wrFdXn2nVJw1y_>PcV%p-) +zt#ZCU`}~DIE5y1BiI$qSd5XIebq(uRB-FnL#^x=bDHO-ls3({4R}-PgePOPH8ggGN +z^EAdT|N11qATWCZQ}ZY#=hum&;_xeyp*|O{VN?%jM$?&+DK=8LzLP4?U!YQ_JT0`M +z`m!W@TCB5mlr9&jJ{^cX4Ye=uf(7g!BDG1LQfZM#P5u-^$L1K~rOMk7oWI24W>ZJVoZ8!*NX>jC%2pgw@7$v*amGL8JnA`*TjN)y=JPix$ +zHom^xjfr~ZFvhO?xbJHg&jfEZgboIqN@pk*%G-#&cX5DM>_>dMo{P<(#6bOBlgEyG +zzi7pMrVsz<*TT+n%Xo*+rUVN +ztJqe<9EEg`YU)i$PQ+gn%oA5sG>b*6QUjZVf+Ju4QKWr32^B^>#t1pQ*dNT#SxuC8 +zGNJ9mH6R-92O+m#!DECXMrB|3+|cZ$?^qoag;w8-vr)Z5hx@2MVoazOwqdLo2-lu( +ziwF(a3SNH{+U_=d6KFp}-N^eInrfTZ{p%Yc;K{NXO^Sl0qy(%)%sUXL +zA6Ic}&$4}O+ulD-?|#%otX@aO$zD_RW;|WXeL`u5;iz`2Ja937zabkT*wwyB&2HIU +z%>tpNcvDWCsG=IjRr(V|Z7N`9^tphbhiC}fIrVcS>=>)uMStm;g&z-HK{fFz2ud&X +zKo;y87PhB3IZGZ&D%bJ2dZ<-Z7I$fCrPWl6O0ir})>f~+rM8k@!Q(EpWCD9f!k>me +zRhs@Q3_eZ!{s3QHUMaN0uShhn$y!N+)a +z6F#K#O21}#Wz2NGar1LUq3S|3=0~&VTjxVeqcysO1QIGwgglMcjXc3^9R9i556MW! +zA%G1+Y)mPX16RcVB#B?R~Xl +zIeR6G9EBEoU+Sk|=Q=4gQdT~j9ecG~G45m|E+IkhfuBxT`M%^wZkNkXpL0AjS!$%u +zg-={lr6QW@$p<#-f}T{y|0rAEpY~$9`Ffd=BP+`1#;?ge=@mLGkdmI~u?Dai2i+}> +zr-d}7M&xTHsK7@<3$tPd^Yg3fLzxDa!oOJ_N!hGt&$}5!9&TOIuzhIP>)^&CM1CIF +zY>A-293)fzq2lbB(1wKk=>#3=G1eTT&8(iBSJab=V@AGnDSwOhxKg{m8sa~TR||^x +zH?*-f-l|Zq;GkYEt~~O`56i(Z6gi`*^TxMZuc-f=NMlvry~%u2a>kz*@R^tJ@{7%-A@wfa)Z +z0~dL|Z*tTjEX7ED(M%2gZ|Zi_L4TWr%=BR=y4%;?-COo)8t(xaW)#f?Uhc9^d-1?K +z=UDW-cu8J#%~t1$>T7o{K5NXC7Z^ +z;*V>fuSp>3%L~xxUi%mG*w7fJS9FTtSX8!B_=C>_+{-q^3-7Yf_esz?&oN2)zkaPH +z=7a>>$VgQh6LDY?h_Px)L~(27=d%@0UX!M7G~G|aMRJuU!|xkIYM`l6jEi407=MtW +z4YA)qqP8SmCj;2g*%y+BObhJICRimEtC(yhX|B>fl9#O|OsP0h{YfJM(l{DjCSf;_ +zp3jZ#S5YfJ*b;u0&zd}UOl+UMYMCH3kOBnUGziK +zo__#J<(BObj|aew5%LI%9K>!}5UVhW*2b`jvkYQXN*iuj#{{Oatl}s!dyhU&jWEAS +zdKj4YrSTiJ_b6i{@5nk1r@W#B=YNdtri~y>StL>N%B6Gj6O_fwN*T-2>1Q1KuQAXE +zF|^na>CtL$iCMfa_^Bio0%XY3)>F9Uqzf-cky+Lb_H)#4=L+?00yMptx;^o9v}fkd +zWN%~1Sj%)#ggvPi=IpG67#q)qrgFi21qV^a=2vb1s|%}692RR2)zei^W-5JD7Y!4^ +zKfyt1dP(!slOA11Of$_QuYE4W{Uu+DO027{)!7+#Y;Ih{M{`=>H7?QlKD!&7BO7X`Q1_b071zS3lNp +z@&Iv~QHe@3e;q{$b7pE51fo%ee_qC4)CR?kSh?lvkF}D2UuE>>^dhlmktVcN14leO +zB6JLU^U}S?*F=`1q~sTqli@HC^RzgoXWg7-*R1R~2xL-K^`XjV%3c-JMf~;^ILFoK +z%NIwJAU)}8T@nI{XRp;rWaD7cuvC1dj!R2oZ(K-<6)PFEe1C`3{NVSlqPF<~-&+*O +z8>_z7E5JY^sY(1!jgGp<)OE9!*nx9!RX3U}7tvu5m2XXS(V8#x+t;nz?=xkI(DDsz +z;3foX=tMqziX%?kztoj_MYP1$@9}OUi0@Z>26`$9-KBzz0d+H_Z`PU9?gSA=7?H}0 +z{c+|*bet;11)bfWS<)JER^z_5PKJN$@9unBRX+W*oX^32ly +zKo%s>e!Q4Gb1DeiV*R&fzDz|t8*WBSo1ove3somHjybczT^qp^D^Tpx97n^9WuuqI +zTCFUlzMc<9(@Ng!L9ebpnk>0!&~jV#PR~Lz8p-9KECK6 +z;70`vR#D6@#_MoD+$Wl*AAbY|FVo2J6vOlP={WYAqT#$Q!S^VW9|! +zl45qcN$prx36zxggrb_z0Ri=i%$I(SJ6jHx(uR<;b(=zb>GDa-cZTt=EWQ$^+AKKp +z!qtgInkt}{k=PN-erHn(dHX?T{g44@;}a%oYTE7q*K<=mU`H~sCkX#6pyH?M+0W>N +zUr<(Whv)VbXit9iJY4V&;_6xGNVK9d_P*yRzW_7PgbR$4WPdDP8K}{V;@u;E04A#lw1fxY9qPYX%78 +z?2?m&UC)5IREFbfd%r<*8}KZbL-)2J(s!Ni>DER#ah4jLH0*2GPn +zFP?pTI`d2+=5;~B0pYU=P03Hk<$aGh?7&|CDV>N4L&S*{xjJ6{dn!jPj@;!U64ZH$ +zxcv;Jen{NFvhysj-qs<-RN>Q}{r7$Q(|cMV#FVvG1ygSsC^0)`=DwB(4Z7$pIxu3h +zGr}-!!|aUp$DUaVeC9=coIL@I)g|Fm7a7u6JRy|q_3SIEsEkSTn?R2}SIm-}g0D#x +zbFUgC%_08XTvJ@!3#F5}f?b~Rz6CpeASaHdWnNs?a*HD&&M#AO;^>?RDV?gRN(Q&_ +zi^f)H(H|$Fg#yiJBfM*7wTkz_6un^+@bW^qdO0rV +z#80Zot!buo$fS$UtrjI&1`2u(g>;~*Z@I;ZrS@=@)pCyAT-4)ZtWEg^;2QgIBuYCv +zCOT@SpdJ=?l}vNgolDevUl8e7VBri&69qGAFHzdn>vz;Z2PIH&X0IdJxq5&G|lD{8;FN) +zyoId^NoH`da-U0H$$EV_2u9QwI2NQ6xr#xsr4!7>PZTS-+^Ser28(?H>pkyr(t|p)Hl*Rcc?7hPzry4)mHP +zWLX9>Jcr0gs2&(aNg9b2@BIl*r~2X;kn=eI%P577nIX=auf8fXGcC+->CfU?wHoJM +z@{%ht0!KC%-tamvUWKvNx!08PvLF(w-EK-u?Lgd+WfX$LOYI=5t00MKD|Q)#@9mL5 +zWZQ?eQkgCCqwANoAm&K4|fw*k7ipV +zB1v2pR!seE-oV-2@pAb2ne;%k;&0+LB7z16@)VH1MO9)MHU9kSp)dCNVB91j?4mmO +zv3NP2%#IeX=+W7Ni#8IjoTpc(!3T*dt7!EX8VlAzQq6uvFcs%W{$71OuAvW8Wpseg+*PVYLv?WMN=C|H@$;E_S5fVp$L;GyupU7jZ$kfvC58X?uLqEZijH!v +HqBZh=dic2S + +literal 16900 +zcmaicV|1Ng^k&Q(Hn!T>_Kj`Zc4OO48rwD-Gw*(Xv_UIGaL4*?7e3`t5-R2lSh^xqd84Cs4}W^FDQn9zijsF13M{zVR~<`0dB +z;hww3Rk_uLO*yyZ^N(arMN#SjFcHEi60E_fZug`IjtJ^LVtno=lKj+Jze{_WszRIN1X*HUTCH>C_wc;+D)6YYT +z*RWmTUi`Puu_Uwkj6-qwu_Ue*kO&$%=o%J?6*rej_Ock3znkGIb6 +zWm&yS2Z9LS7slFgUx+?ilDgQBdj7`ruw|IVzJ@wV{&tD)G@SPTMW@9Wl5lcsuU~6` +z7raw|%Or|@Pnlh`7!!rA1H$`p;zz}+92Tp2bFmKDAL`nrC>)<{qBHso +zvJ6|o^vMxL?frh4XZ`3WdH7s_NI0p@{EElbnX*!yp;Vtx&K&w$&to`sW +z79>enm;xWhu;ZKKIN}-h!eBKZM6j$9~*Q(SlE*i_bHS0o#tPY +z5-j+ww|x>h9%`RLUixM!e%f0qVAe5GH83X6?!#^_j-M@lO@*-aD%NMF2;Hg^Wgh@}elrPA3o_&(- +zeNyws4es~%;K1o+pfG(Z!G-nFWzl7)ejRNxY?M~uI=I&MYuz@4>GLH*ptjlQJ`LYr +z*KIIVzBhKHIDwe`X2hc@gsdjzXxX%b<_#kc$vIHFi2)-XM1=fs(`g?0)M{lcJXwp< +zBgIdDXM&n-=+_%;1a?sE$oeN{r%w=8tFfAlQopAk +z%wrVN=r>)oZ0w7^M~Xi~qp6lEaABgF(ck7V3Un;@cg|ODuD7@fw~OZ;^TQV +z$&4AiUj}-4;o`6JV$Y4C2G +z8hVweUdzl78hWzD|&J_)oRr2JdJP +zA&lca);^P(q@hQb9-kqNXVo9An7Q3NoAtyRQw-@JUDD$oluryjE +z3{zzbZhStP-K;xw@Yxf-B=4h(p=4f`k8p2DH$>qQLPR!szD!2|vJ}J`C6=EoRwG^+ +z;`ZDv1SGVO+?IqSxpxSM^_V~@2E+~dZQdl+oz;TP1MX+XXwugMy?Z5AoZ7#R33Y@T +zM)w4;9L0szO3>6i#4fV3q49@wu&`zcvQ!d8!m*dpn&7pp0Y=;QbiyOzhC7)Ki7tDt +zXaIqysWqx53ZgHlO)|YRDG**$7&F{0a8VEECY`3;yx)F>2;4Xr&gC;Iqiqx;orWkF +z8xk0Ty-mK&z`^~Fbs#S;;Qd@1ZFJh4R`+H>Wx$xgn>^oka;w9~QfR>rS7lYHG?D#o +z6Jo`Qg_-DP +zX@kdURs~L5?afF*73QF!=HQ?vIysP;FNCMBfA*}*&%$eDHh5L|y~D=C^v8(wdtcYZ +z)8Q|56BuZ~3~KpF-oKg|5Uf@Ac15Z>sP<9hpm(E>^cgr8dMxGhn7mnWA+JPK+EGR; +zCfK+V1&Xi1M6CUFIA+oJqr(aF3W_=ph7h;IVlqq&xJ=d(CqczQwL>f*A$gJW_|iZw +z&>!^cGyI)UH(_%jFMta0ci8K;?^D#C4_`@%@wP6R4qvs8y@ecdj|*ia7Exg3*BpG4 +z%Dqav(-_hWolzv04-3Ygs)Z~U$`R?hQq2Is2`RWS%z4?!GF2CryzMjCEFg_Y%K+yz +zG8tm;0X{;XG5?BBT|pMZ296(fGUtoF_$Ryrso&s;Cc!g3a;pYOn-tjPvW+1)iAQ)I +zaPyG(wl0MZUqz_Z!4+oEh$t>QIaiZ+J1|fQdfugliOCAg+6D!~3<-k#gA8N#Rk3@5 +z&u3Yevetsi3m`sm2Ntt>FV(PfME~wR=LFu+2@Noy&wr###hgP3mjy&H03re#97OQ% +zsZ;NtktNoC?s@G44Num-@G1zw*?jMf)dA`SWJHyI-Lp=m +zyv8V97L8$~?>Sf(&Ee27TQvEf=-_%~EL56_n`*ZRVS`=4Ka4&HGjr9P8e3rf;8BK& +z&0s~H!Z|V-mPt9vUj?5&%Sa@;XK~`TS$ylgW4|1h&I!<9c6_zoDdR2)FLErHw%Sow +zwc_2ZKizcAMchMvZ^6OY8)uiUt&RwA(`3@dzgihQ1MSrNi;ruq-C+?oVa@U0x +z(>^4ei3Bedg+!LX52G(u@W4P&3sdv45%OawU(*aQat~OuEf?Hi6Zi>__qCd)nw0_j +zvUwA_6WQ5tnFsl_AZNz8L8L*=L4?0A>inj9l&C`AC71u=H +z?bu{Q_=al@1+|F&El|te2eQB@?#+g(D(LjFx>w=0X;CJ|CQc@tuin_)Rd$KH$Y9P9 +z${MAq+Ns2`>_SLAfKm9~%?U2bK6>hiDEbdUD#NMd$hR*wFx8TxWVY3Za +zM&tRPhR$htT-*KlZT-SGBy4YD;6aZfAz^Jt1`=ABifztn#D_;u)2WTa-Bo^EKL;=o +zDc6Ov2x3ybU1B6gkFjv-UvyFl^(EFkIb4ht2Z(*io4 +zW(6^Rp7OMxVh73mYH?bkbxgXB=+TL>U^8OY>=P$oXPkGAmF?6#80T +z+e?24uzuJC8?nCu`7)ef&Nu8x+`0%wOB9wmZ^(+|&$!T80~3uj?NRH)aNhf~#vN9e +zem1VW#bKd$SZ4ufS0-pzoJ%P7UWdT@8yg`1+kpYLV153t;UJy~P8@7sO+#{ePIXcSgw}v2XayA<>Jxh}D)tMOGRgJY0QEJs` +z{>aB;ssVeqKi-6L#(PnBpPuOu<4Rf*GWVk8BdMCd} +zc^_!LU3n2YWBEk1?0<%f@MkB;t#h0%&cixNCZn@Lft$eDVl6z=l@Ga}k<7cF5n!!o +zXet^Q3;AyG!j)+$=3U>7D5cEf)=YMZ)jSZ?)!6EoSa3kU!3W2Xn`K`PqR|ML`Ju!A)|K2`l1>ErJG>o*qIC72B&jHYe36od@P! +zi)qQ9Y7g*>N;Y4;sSLlPxvM;q-Tzw2m;Zx=x>{mk0;Ed5zA?Hb1FrDGc6-;m+iSFU +zc22aC&R^-iyw5vE$D?GWWo7A5o@@>d3_uD92sGM_-tlsdQ?ZbAnF4LsSxDj&0TFgO +zFbB*@;0<;Y0es>tB&~M12_up)gRS(Ce{seFR$9$~MC8~S%gCTV+2AIiH`gndEW2~H +z`z|RK5KuxIccy|!;Bkm8puw0EcWFE{ij71G*o4( +z0~y!3%z_nq1kdh3x<;XVQS{_v?Q3|H1so1Z#CL|Zm2Z&7-mTO?&1?U-oogOAE4Cm{ +z`d4o(XCnWH-J^hx&?7X^xHns&B`u2*skUy`s~w=0252bVaZy(}U?e5?u>fG!UbYaS +z4Gz$YBX|~|U$??YUR+zxw2g5F_OJB7viI^}qx|ouEswnc0o{D4T~~|912EVr9)4P& +zS=*@uBmgy>GC)sz_8A$Iga2y-R#LKP$zyVe7P=4Vrn@Q)Fp6mG;Nall=^07<{OPT~ +zPDD~5M}Py>^H&ikOMCrXaXjFMyNuyNg$gXaPOE4z3=$o3Jt(guFuvAQbA?*MR;Dx}r~+zsgJ +zzCtQ*$r?UAKNl$E39K|(pdcV17*;zU{VtG7{)QDicnC&XAit07AxkJs2xbNxkEh-l +ztI=-hZ#0{5e0{huHk5pMKFXUdk-_HT=8j~#**>ze%L-Vq--ELbc7OqlEqqgfDL$7| +z^zia3^m~7il#>&4bK{s6W!C%o9eQ_nw_LRXoq&)qk2e`~Carh!_+@C+^?4E@nB?8v +zrP(B~aF_-3_5wx4#3EgX2f|T2iDX6dBot9e+}zxz-+7y;fop?^#LWumnJ%(ER<|F> +z44(0)x_-m7iZI17bV#w5<;|{V>IZ-R+z|XI2d!L0M$z{_~PzI|b} +z_>I9TkwT-USfkDEyuoB7YJe7^SUeW*JCd>d31w)Viag>w +zE)Hcnu_U(A@CEh^w;UM0IVsDf+yNUB)lCpiM=a>2dMSVx95URpuHBLGh>h8fgM&77%eeba~6*@>lA8=;7iEw2QP4d^IvP +z8fpiWc?lq5kxp*C)nS|HY^i2ov(x?A!{1u(mk%xyJ_nmAsx{Zt=LV=Ta0-O}2|y4O +z5yIAhMw5|xp3lvw|Ps$0W*KZd^Wlj=W@{AaG=^es3_){Y~Jis`IYYiWN~ho|DLil1qRD5 +zN6xAlvXG=U-8`VKVHr!k-;5Bi)EfnJRTtvY$;jR$#e%~lxMV?xboY;JA{IT_^y}D0 +zw1mJ8tVoSO-(}absB6M8b$Zqe)Ok0$OkaA#I +z48@e8TAlv;PmB6dbP|{7<%qt@Ea>I;PRL4)=M`_G!A40Y$Xy1Mum)I0#!3<77H4)u +zI6c{)TUsy&o^*@2H9Bp>QJA#S8$`zN?+@z^IIQL|VxYEQfVw~Oc}Wq!FS`G2T=aDu +z-DMYe(1$x=331oN(i#yV%?Q)lcY`}FpGRp*74@@$fX%pE+dAGOh5QRhJ&mcaXOhk4 +zLi_pirw^Zws;d9n^#IE8T1ypZDX|crNABquU?iL2;Ql%4Vg5cNBt}OJdbLKnEi|`g2q%v70%eM&7 +z5gdFefu8Ix3n54MC +zW40SGT11ajrrm5AI24T?-2$|VMsU%VX}AMmt>Pr~B}#An{>%QG>_1FQYV^)CExzx2 +z&7E_9c!fpiCLci|F3H*eM2DQQRtQp4>V2RP=KX3ZVw#OXuFxj$VDmM&HQD{*dc7301976VQyI69%EFvxxn>qC&Lo-`%ImvM +zCv>AXKPcD26Z_;m`1pw)uF6Mp=RnShU^yM81!?jbl!v#-kSa#RLhSOG0?yp1YB6Jr +zW=GrO|0zIRSHiH?DYiO+$EpdMkwz#4I6V(J12-W0+dAo4J*?nDQrFI<*}a92Y%1bU +z`RC_4tyg7>R(8{ +zA8*g?PWv##WoF+p0bJe>whg#+(1_+A+)9HS$|n?k;(r=Le*vR;57rn)2& +zEkD8KBSZm#3Drt?t!*#s#>0+yUNysIKRg=t`KSOcSHieiUP0z8F_$tZ(ciPnq_o~@ +z%-{zhbs{i7 +zt~8q8%WO|MF(FE_ye*bl_-@NcA!S9$IMb6x0`e_oNF!hy5a)H^H)5)t(}ek4a1Nc~FF4@f;5aO%aB&3O%B8NuMWWCzYb`d> +zQ-&3)G|5M|pzcLy>pA(p=?3&XKn+v0^`HNsS?M0eb+60BxF|&Y{?>MI^x``)Vp}1V +z;<0N$BUc(0=p=y>zD3k_I~ +zMC>T|rn!T!wN%lqT@ +z&Afsj|04$m&CH2M?F|6yeqb+e`&JWTP^~~z(;c>5;z6RuFKe)%3j|YzeZB9c)5E08 +zvX9?L9%?PT7Vu(RAIXR}s*=I*@Qp<*vA{&7B2uwdBH$_I`33U5di9weG|3 +zx-Iy`1L`R>G-q<+w-{f5qc<7ls}^cT4Y^Qi+meHXFIDgqkt0wpdBZGY?LB+q9&o`T +zd18L5%R+44Ml^UNbEw58BXP#{+I#J1$;VGO`#6Grd<=RWgP+T+ktE6H^>C;%(}szj +zK;wt^oW!yG4Fz=zm4zKw@$Wdo`VJm=879kp$F&$uMP_qiKSB4L@SV)g55F9Rb=3ocrK>iqIRR9n!X0Do*Ldi{9M&^sg&T_TZz~>`tbXc$p%%BI% +z#MahUA?U0t#2ZA4_41*w&52#TXU^_G4)$#uGOnpIb{Gs?Bge_xP|beH;cUSBec^gk +zu;a`And#3j5LZ)LALL9lQ0{$A?tzx&K6M(;#M))7n&`7KTkT>KvjI7O4?mTa;X`81yn7WAir6 +z^Dv#2{~#3{X=5gyP*2v`3yoLJl)--n2rC2}*3n8(L~4ohHzT6QbyEu{!K3q#&p9Lp +z?3#RrZR0JWoh5V%Au%m2?uSB&RO!i99khjDd#7P;NaxJ<_f>mYXQOtXqBZifoWn1d5WC&hmG;&Gv(>!l)|)selJ-m-pz9Og@*rA +z%Xl~n+gHI_Rjy513U_dEaq-~ZLm%H7RpVbREoW=Zu*D?n%JFyy6(v}{RCOy +z>_wu--o5bv-4rRuWG0oN3a2+(f)C6nR0%>9HdI1mB`d{jE6Q4vSf>>{@~N-bGMc6~ +zn=1MB2?XIjZuOC!s@-pN5{60UUw-L4f1L-3Ohud?4)I$4Y&#w^A*ij(1$$3|Vskv} +z#YKCOBnHKh5QN8fd|k)wI{^HZj_1!`{L&>R(m@P^tYk*J)5>eCrio9{j>kWLDCGrM +z*O<)utCbjQiH>aHzD!~>SNyzV|B?uyizaR*!v`(g6N5ks=aSqWHk#wzbQOx2Ehc(>s +zfl`oSK+EzLOKDeK?n#pu;5qF1g-8bXyN##%K`x2R14CxOh8w&P-kz4U}>3Q=A& +zwAa>sCXe?|fR^Y+S9_jW;=!_GK`1Bc2HY6Y)*s}A##+#}239~LV&Q~wL&4n_6^@vW +z;nGUYJ$5-C#kJr2EtD&Ty$t-H)#GyT->}39LWB1gdo%LwqR8{YbRBL*-FCEc5iY{; +z#TpZ~y8yolNKuWi&enqz%<*)Y)j#ff)9q1ezkI|N7|zr3b=T|b>+m?)d% +zKJ;1@L~w8ZQn0MxZS*{ew-;Ohn^Jl!+U{m|QvgB~tai**t#d>0E=CMjN*SZ+36QnO +z4NrSN!Cd>9SLf?=!Hjh+ek}c}ND_U`vvi9(MS>7nGZ*lPm%4(7(bhfuTHod8y%;N{YO_KMV}N<7D)x5snD;XG +zzCOH#WK2$4mAvQWFCCZW#F8TRInJ+=$6eR`V~dES6+!6-=6lkVCHyCW^Bb-$@=b%3 +zi%hxQwAp^EOp|zR61~UikJsM89qE@P3@X5J>+K)hO6K`Z$80UqhLV&|mVt3wQ#G4H +zi4>T}s*jr9pkN+B@=LbuMW8^kzEFQde*yOdnXiUws9u#OD8dYzm?0F`qCm7pBCNNz +zOJB@PR!5?2&9Zw_Jg~i=TwmStKiYq1_@$ +zZKB*^u}y2o({7rV#Nl+8$2T5 +zthMF3X`+*;4Q-~&-*4NzrU=7>#}h=jB}<^tsAch7Ac~Vq;V7 +ziknpCHOP}_P8F&VE%6e`WG~EVa?$ra`knKZrYWbIZ_w@4vO+{B!(Pb&!YhY8pCfe= +zjxF8x>Zh3;#gw`fu})grVJcf=Ohg_Xc9m?(57$!NXQ#N%;Q{V}EjtmA$m<@Ie2(h2j9T2Xq=0<2R#daW&$ +z85=lCIqjn+?h$SF4u|?#DOOKg9>2c{9GSdlh{<(WR;Mb+bxH>u95roevUiqSmcdG* +zEL`{Qv+mA#hjLxuC*l?ROBgDsPYkDNU%;m09$2^ni=SVA=kS_) +z_h->URCbhQr89T-a-Gg9Dk?P`CT8-=f%@A28AYMmma&Ks#DNDsr^|eI%nHBQ0Nps* +z<{@u^G-9krSD|^{Vm?_nRkW_T!;E*n95To#4sxn;9FH2W%&T043S^Vg_Bk^^&J9*H +z=-^Zd6GYUG(CMkA?hy<&4Tc5fn4$3ys+ZiGw!07qHH1zPDzAJY;{8Oj#B1-LTAZ>D +zKqX)c%j0#o|H%z2zdkxYKaV6<&nEMgP`q%2&v+2dsa++rFeWoOnf$VkCAY6|8|kw{ +zdwe(maC?oeGlx#HVClH?)W&QZ`+=l3PIeQ%9cb~nWxJ9)YD|MPt`v?0-3bMcbZ<2Z +zG7xSnH{QoOr#C@?R{C$168|JMfCxcPAVuEhewgQpYO@AfbP3Fw+|Vi7h~L@$6ydj5 +zyf7_h9Rp$0Gii0mkT9xddqw>hIVCXV203~$D~swIj_)TV=zX)@-tK6Hb66mM;EywH +zsMV;{!i^8fvae3b)iz7_f6$4yU2i-b%Bh|o@eU2$RD^G(AtWlyl0^8dxd<9 +zCi_xU0%&wFugtmc%-uOk=xMY?lR%{7BQRZ~b8}1<=DQI)v2*#3|70VNVV*?SK4O}0 +z-HEICfCoyTwy@{F=Ac>4KISQEgQLDcj|>j}hzn(*RSn +zZw&u6!^Z2~7ae&u`+{IHYm_vxJJ@RRZ!LoCjQ2ecK6E;AqeyJZxfuAC +zaFBgBIQO4DawgA~vN)BCS%`;S38kn@9kWOTMq)$V$+z&4nDQvH*{(1#N58$C)v2#; +zJW|ch#FaXRBNNj6mX)HNV{_ScADWB7#Jn(Th}B15lvrI|-2fj-=SL1AY +zQrI&y#`tyxRIyenc$G7)m}|d;5&h;8q8?ap1~7v{vEXIAhojO|^XI$6=K!f+>;5yx +zJJXiq*Z?mW;Ak{?4<=)9$$a@6Q*=1_%}Nx&bGA3oqS%{I)k3y{#DALAzrPw)h(FU +zj}8a8Xte($dBpT +z_ZLeg50aO#zhmy?M*+dS#c4NyP>CZSyS+OOi>@2;)lr;&A$)(OEO;kV+bz6O57by +zyW>9>Ij2^Du|A83(r~$46%S7?Ancv(6R +zJK?TL+k$9p$KMJgY}hdrTzyS}0it==hvU?8YM**7M}l@-W{&s26~NM6 +z#U8(RCX-=6Lw%{$D&=aKSfE%aJ<__RASP1DaZcJPva<-yi3NH#t$OuNk6wlp&CD~1 +zanJ|7AhF;l{a^)Qhr_9Bo;2ZG8=}0whx#r7zZ6W`Fs5 +zJEbvhZVJVsORu$w4Y1HyT1E4?Vka&kS*mSpBuKM>OAT~3W;g7KLGzfQWF~QJ1)H6S +zFCOXwP_auqzKSygLBPB}EH;Q1gXb@Wm*lZWfM<8NWGZM_*$8Ze)0+^IpqCyco5T+P +z>!edzc-RMsx%H6~4%a*u{&6!V2Xf)f8oOKEEtBAhvI#TkSv+Ago-TMSQ(2q}=S0FP +zL(1v}1vp6Ya1@zfO!}Dq3ke|~@mmFXu2dHEQWpO$6X$;c8V@V*w>NACSkmSKF-THX +zXc85Wu2(uhx0b@}vaeA-YhO(oJ!8ZlugSxzOn{tnI7h@dCB`UVE~EEY_ww_|qDlb| +zQh0>qvDy{uar91x0J$!N&ch{3*B*?y730`NAZJT0IXU?T1Oo1Zc+QnB&!+ZYLh%_v +zV;)6DQs1sEzvoxu0r{lou-yG%CgwotYzFK>vqr!e>KRehvaz@y)fTge`_wgV2*|2H +zVl|vbxEx$3ymn~uGqN65%FYqJ<_)*Uqs49;KY2h*(Xa?Tk7AFfl-xf>irJoUyL*;0 +z19&1GQV*5Ni~#kTnaq0ymCiLjk_=0q&=&|cG{r57n*6NwV6zJl5K*ED&DsZy8iEL_rr +zgsLXr6cN9-S7dCo0TeKI3ByoGNNBIG{4b4m4=LB^FstU0B?!6TBZ1v~zn%e*Xk=B) +z@_rySE6iHcIxSfbe^sRAkjZKFfR!7A5uNa|Q%HSV{);)`X_I$=Rz#g9)RV +zjIuDE+A6IDHt@Noy^%sCnU|?kL3tCMU12QN7688MFeYr;%^{CT)BqX<4rY8gFNo(^2<+x6~@> +z0Y;8%xJK3sk3si!JoTyNPRqf>i>%mkw_b{g-~}-aAljQww_S1L53kdn=uMDZM5$#ndk +z&22o*u=b&^trc3UMGkzzrL*~$;t?gd{w8WCC+z$)6{fY`v4CL%;?|JZtR3}&oLz8* +zT?G#HsX)xAYvWho@h=pJpzsjcWp0%LD4s08onG)Nb4)MY=8K^XfVvcKVvP||0{idF +zr>Wx=dX&);ID@-|u5Y#BAa0c8rW_t)Xfo4c@By|jKCCPsr7DjJ6t;eTIrmF;CpM`~(ysWB=S@seY-cC;IYp7eGp3%$l} +z)oc?3jDrN<0qs>+yfj#>o^%eHp8`K^wUK{qUM_Xl#K;;VHK+>&$DqLQV1~BoxLuBrt&0}DAhEKn_^ER` +zz-29QNvC|8F%an87xNYKcn*LCu89T8nVkc&?~&O83)5GbY)slt*#=)i7s;A_C=2r7N7+fk`X1KngTDCyUEafq@X5m_z1=DeiD@Q38P{+Ou8AdwgrjC5 +zajlbj!7Ae^jZ~9GGnmvF%|dV*Siz7~1$lG}zFHP5%BV8TD09lQN!w79WRZ;`=PM(z +z0;YT`0PcRb5SM~SQ_OKjwTc~?W_G_IPe||U$;Um2U%fe+7X>%Nvy!xcXUbbT1miw0 +z=$X7_W&m0ay!h~`ae>C68mu@al*ia7R0saqO=sn$tE@ww372nWLhU^>%{WE>Eoln8 +zaeH(5Zly+xlW1Z@B{Z2HqS52V*oh`BC}k&quf19RS}N6$l#0qGWzl9DQkZ@85(#UMH4E) +z!&hPrOmR$HRF*}2C{e3A#U3h9d)gN68^|>O9=TO4Ga~u#5kl0}_*QP9IxEl~Ce;Vj +zS3zvyQ+p-TKYiV8z>J$akDBH=i$W7}&)8|aN%_17$7$H|;eKWRKgAtrMwoyE;#kJp +z>iJ{R+d4p$2q2;Y5EBQ7>@E&mk*MzVW>!EDsQ9Pd1Icl|=0d^U2HU!hP6MLe0bwp2 +zA=U!|OQM?{{^8dU?o^&w|I~Y5fw~zw)IT&*mzBRUy1Ljo^-=Z`fvN|N_JgxG~k*Hc%03VftQZkoi*AD{-11-bt2%}_=-R;7ZY`jOzsFyAEWb! +zVJNLPL#@4|8iv-c@m4Lu!^Uc7?VOsDWty>@T6^QN67|~9P?w&boWVpR2)d)gI@s*$ +zT0uPct)H#x^_Y(_q2El&g2<(pF8niAzCde(;c)XAp3awn@Z)3{qMO$l1?#O_cXL+a +zB+yS96Q;w{xIBw9%-h2xp$%a(D0`Noi$$31BbukCM_lu$4sG_+rWsH9U`eD0eY3t3 +z@`vkyB5OW$_NhyNPE(&_JPvYO1XVd%SiaJPVza|ZguGogD*p`OzJ!Odk4wR7o=G7; +zQFEN*_9WQcO`Vliy5G@VCnZ;Qb~fJ44e1$o^Tw=L_lA;Z-8Dw0CC}X_m5Q_J*xP61 +z2tVQGAnU9PA@k;{9QL{c=-~c_joC`W*8qxTI)7}foE-)SU;g6SD;S1P5oGCta0DrC +zGXz?khB$Fn{Ycwuk%t&RTyJ!Mz8mnC0U+AYu}PkaA-t-gE*25%;RVKNKyWz!scpu6 +zZDKFBX5S4#lCQK!Ip%UxMsP%cC4T!8d`;mo#M{(B)h;Ilk3UVA`-O^+JuQDuUnt-K +z=jEH2NuzvVs7mGT0rJ;Nz54;;pVk-{O`o<8h5~yAG9cx)%sJ+#d0-B8j!9{+{>1@9 +zYiz-m^g@6wE8^*umZD0JhIN!|&Ok-?2XhJ@B|oI&FfS^$rs90JhlZBoJW`e5b9j^- +zWO>uD9oB-o4QKEBn$akVeT1MeUX-s%#m~lPXZR!_h7SU~%Y_rx{QlrO`$o+{oUb!PIS+x5N +z+{O+YLa6?IE1#&A?RMZ&J}!O!vj>Os^y>J_BMi^Cu8;>FP)!5eagStg`4k8`f<9)s +zLv>uniXJHc5tD}2a*xO+UycHT8lGykAS#tq7H&?$Q|yXO#aH{77;M;}%#Rn*u_i#Q#=kFoCjB +zxM)O)sW@_wx=K{lJ|iyESH0iv9Nr111eP3eEA!SenTb%U12{RS*7qj0=;%^Kd#QiJ +ziYTEU=jFY{zWsSqmqmw<7L@5T1o7NxWhht`9gu$(b|QZnjVAE)D;lyC=>~hv=8piE3T9#-QVKCSaq-q&xr*zuRbfKtru+;Kkp5Si5+<6{tz}rp +zigZWmiiYYR#xdxCbhhJz=wN$k9zPcR8H;AJErv2><3*Bm51h&CEJlpT9yo5`1`w{pnaAJ%0k=ISmg0E +zo$J6^H1-w0!^WV5w|yx36dtal`WN}DGpD-gqYjDTfjIaLtR}xxCDSo6v=}KHRM^9@ +z&T;nw5x5ee(K3%Z3QQF%sMId_cIRpr&3g$f><9ZoX7X_c7g4f{y)mf(?;`TLI@jLv +z?N)ryzDJ)LsBZU+VnRH0X1E}KJ!}%#n_-hEY9w +z`8(=7Fd9^wGY;{_ggJK@ZR?yW!1!^^d;F^x%}=DG(7K8XMm$L~K*Np|t>vZmA5%Y| +zINrWxnZFq_J7&ksTGEluekfNRCX$8u^xk+?w8Q1iII^7LA8Wc=uh=>E34C14fN(+~ +zjb&LKSzG|ur8^cG=n*d|U)DK;5`-D7c>o{;1qb8{cYdL5^ll*Y29ag^ZWs(}{Dq?& +z7Vt6fu%BVSoqvD;RYW!I!KS^e-kCz_2@FvAByt<`2mpvxlE{aWp)% +z7->KZs4&!M+Z9|_;(QrbPRGNC2zLU&;bq*v@zaDlNR7 +zR!OB(0w7?XvMI3w1tc_A&fY$=RO&K>9q)K{?KeL9#X2nl`k!ouFF)XFC@Tui*%L4~ +zwNvTu3}=K5TH;uDS!^k3d+!l_hx$f?(hkYU(6NBYx@mz*Y6dZ7D@JF^5^p{aiT5zv +z;Xjc--#|sw407DGZz<4^FBXBq5F)zwTQ|65$~FTfyft2wOiY&QG(ydKoz#wa?YKny +z)9C@EX0c#XN}}K5dNFdMNo^+Os>0sS^c;E5Ky4zm)q;>J{J+z3sdUj)7tN@@gZSf7 +zJ|wiD$oI`e{Xe-gDV9P_(x}i7AaPVJn&m~NMi(84-RGbXy6@{lY?h66ze7!6Ee=i! +zInre-6PCHrI9+8v4+)Zge*esLVEy0*)t)o|)801Zf98hgQ=EZH2bpZ=)5NN_2yjw# +zP8Ewr(5WN{8DJpt*e!|G(gvZ5Pxywag$Agdns%%4+IH>|FMw9b +zKb<-v)*Cb*Ao~hb;B*`Ee&trZYBi`{$ru%gmKbuXcPNb3lD3H3Jimki7;BEFp{bxX +zFJ7Rk<~$d5(AGs1%w=$DDrj&3=?C4wX`U{m8^^=Z8R3YTB_A>ZAOkmldWl +zwo0ZyTNCB`dfUZA+chm*()HWtA2!JQ3>g${8%Vr% +zasf==&095e)fG}M%iIsk{PaQ>2|D59ppz^2pExvb9Ou9EI^`kN!0aXr*u3p0ex0b4 +z=AnHH#@v>`#o*LjN-yB0^^l)H2Nm=yD3|>1aNigv$f`s680kxF8B%d>SUG)YF0R~W +z$TI5rvll2~&q4RSwu3})*@1!~z4l}@NsY#MwV(2Y=hbLZh-ce*Eq3<#rZ +zxra}au9h@`-JaCDeW|)St?N40z`g~4rjZ?xu=?#W;cJyHNPXCV2DuxD%N1A2hAlFH +zwTJm(6XPn#dA&{dq>&yd{5Lp=pa<%$*em=~TdQ%rn_v#5`>I!IS>M^uNpl#N|wC@HMBcRTMT#SL;d7 +z<(&BuA6dLkkx|8fWw@PXzCeCBgDx@HJs@)L+j8y~gZ)7)${p-|O7{G? +z&|M6FI|A*^d_U+Of-3`+w(c~-YsQby|NH)g|G7xv|Nek^|Jex)g~z+)I0xPC0460S +LFIp>X81%mY^Bg|U + +diff --git a/src/test/java/io/papermc/paper/permissions/MinecraftCommandPermissionsTest.java b/src/test/java/io/papermc/paper/permissions/MinecraftCommandPermissionsTest.java +index 2f3ff50bf3f70b6b404d02d5ffcc079162a63bc1..4e57fdf21d4b7789cd7c3d3a18ddc6227bc77792 100644 +--- a/src/test/java/io/papermc/paper/permissions/MinecraftCommandPermissionsTest.java ++++ b/src/test/java/io/papermc/paper/permissions/MinecraftCommandPermissionsTest.java +@@ -48,6 +48,7 @@ public class MinecraftCommandPermissionsTest extends AbstractTestingBase { + if ("bukkit.command.paper.pgive".equals(vanillaPerm)) { // skip our custom give command + continue; + } ++ if (TO_SKIP.contains(vanillaPerm)) continue; // Purpur + if (!perms.contains(vanillaPerm)) { + missing.add("Missing permission for " + child.getName() + " (" + vanillaPerm + ") command"); + } else { +@@ -60,6 +61,25 @@ public class MinecraftCommandPermissionsTest extends AbstractTestingBase { + } + + private static final List TO_SKIP = List.of( ++ // Purpur start ++ "minecraft.command.compass", ++ "minecraft.command.credits", ++ "minecraft.command.demo", ++ "minecraft.command.ping", ++ "minecraft.command.ram", ++ "minecraft.command.rambar", ++ "minecraft.command.tpsbar", ++ "minecraft.command.uptime", ++ "minecraft.command.debug", ++ "minecraft.command.gamemode.adventure", ++ "minecraft.command.gamemode.adventure.other", ++ "minecraft.command.gamemode.creative", ++ "minecraft.command.gamemode.creative.other", ++ "minecraft.command.gamemode.spectator", ++ "minecraft.command.gamemode.spectator.other", ++ "minecraft.command.gamemode.survival", ++ "minecraft.command.gamemode.survival.other", ++ // Purpur end + "minecraft.command.selector" + ); + diff --git a/patches/0002-Use-Gradle-Version-Catalogs.patch b/patches/0002-Use-Gradle-Version-Catalogs.patch new file mode 100644 index 0000000..4f1b09e --- /dev/null +++ b/patches/0002-Use-Gradle-Version-Catalogs.patch @@ -0,0 +1,89 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: AlphaKR93 +Date: Sun, 14 Jan 2024 19:51:19 +0900 +Subject: [PATCH] Use Gradle Version Catalogs + + +diff --git a/build.gradle.kts b/build.gradle.kts +index 81996f00384674b29368e8bea944bdd14d631da3..176f07b1ce1d037880ea7b56d354281d3f89b11a 100644 +--- a/build.gradle.kts ++++ b/build.gradle.kts +@@ -18,57 +18,29 @@ dependencies { + exclude("io.papermc.paper", "paper-api") + } + // Purpur end +- // Paper start +- implementation("org.jline:jline-terminal-jansi:3.21.0") +- implementation("net.minecrell:terminalconsoleappender:1.3.0") +- implementation("net.kyori:adventure-text-serializer-ansi:4.16.0") // Keep in sync with adventureVersion from Paper-API build file +- /* +- Required to add the missing Log4j2Plugins.dat file from log4j-core +- which has been removed by Mojang. Without it, log4j has to classload +- all its classes to check if they are plugins. +- Scanning takes about 1-2 seconds so adding this speeds up the server start. +- */ +- implementation("org.apache.logging.log4j:log4j-core:2.19.0") // Paper - implementation +- log4jPlugins.annotationProcessorConfigurationName("org.apache.logging.log4j:log4j-core:2.19.0") // Paper - Needed to generate meta for our Log4j plugins ++ ++ // Plazma start - Use Gradle Version Catalogs ++ implementation(project(":plazma-api")) ++ implementation(project(":plazma-mojangapi")) ++ implementation(server.bundles.implementation) ++ implementation(common.asm.commons) ++ implementation(common.log4j.iostreams) ++ implementation(common.commons.lang2) ++ implementation(server.velocity) { isTransitive = false } ++ ++ runtimeOnly(common.maven.provider) ++ runtimeOnly(common.bundles.maven) ++ runtimeOnly(server.bundles.runtime) ++ ++ implementation(common.log4j.core) // Paper - implementation ++ log4jPlugins.annotationProcessorConfigurationName(common.log4j.core) // Paper - Needed to generate meta for our Log4j plugins + runtimeOnly(log4jPlugins.output) + alsoShade(log4jPlugins.output) +- implementation("io.netty:netty-codec-haproxy:4.1.97.Final") // Paper - Add support for proxy protocol +- // Paper end +- implementation("org.apache.logging.log4j:log4j-iostreams:2.22.1") // Paper - remove exclusion +- implementation("org.ow2.asm:asm-commons:9.7") +- implementation("org.spongepowered:configurate-yaml:4.2.0-SNAPSHOT") // Paper - config files +- implementation("commons-lang:commons-lang:2.6") +- runtimeOnly("org.xerial:sqlite-jdbc:3.45.3.0") +- runtimeOnly("com.mysql:mysql-connector-j:8.3.0") +- runtimeOnly("com.lmax:disruptor:3.4.4") // Paper +- // Paper start - Use Velocity cipher +- implementation("com.velocitypowered:velocity-native:3.1.2-SNAPSHOT") { +- isTransitive = false +- } +- // Paper end - Use Velocity cipher +- +- runtimeOnly("org.apache.maven:maven-resolver-provider:3.9.6") +- runtimeOnly("org.apache.maven.resolver:maven-resolver-connector-basic:1.9.18") +- runtimeOnly("org.apache.maven.resolver:maven-resolver-transport-http:1.9.18") +- +- implementation("org.mozilla:rhino-runtime:1.7.14") // Purpur +- implementation("org.mozilla:rhino-engine:1.7.14") // Purpur +- implementation("dev.omega24:upnp4j:1.0") // Purpur +- +- testImplementation("io.github.classgraph:classgraph:4.8.47") // Paper - mob goal test +- testImplementation("org.junit.jupiter:junit-jupiter:5.10.2") +- testImplementation("org.hamcrest:hamcrest:2.2") +- testImplementation("org.mockito:mockito-core:5.11.0") +- testImplementation("org.ow2.asm:asm-tree:9.7") +- testImplementation("org.junit-pioneer:junit-pioneer:2.2.0") // Paper - CartesianTest +- implementation("net.neoforged:srgutils:1.0.9") // Paper - mappings handling +- implementation("net.neoforged:AutoRenamingTool:2.0.3") // Paper - remap plugins +- // Paper start - Remap reflection +- val reflectionRewriterVersion = "0.0.1" +- implementation("io.papermc:reflection-rewriter:$reflectionRewriterVersion") +- implementation("io.papermc:reflection-rewriter-runtime:$reflectionRewriterVersion") +- implementation("io.papermc:reflection-rewriter-proxy-generator:$reflectionRewriterVersion") +- // Paper end - Remap reflection ++ ++ testImplementation(server.bundles.test) ++ testImplementation(common.bundles.test) ++ // Plazma end - Use Gradle Version Catalogs ++ + } + + paperweight { diff --git a/patches/server/0003-Rebrand.patch b/patches/server/0004-Rebrand.patch similarity index 86% rename from patches/server/0003-Rebrand.patch rename to patches/server/0004-Rebrand.patch index 4537509..ce1423a 100644 --- a/patches/server/0003-Rebrand.patch +++ b/patches/server/0004-Rebrand.patch @@ -5,10 +5,10 @@ Subject: [PATCH] Rebrand diff --git a/build.gradle.kts b/build.gradle.kts -index 176f07b1ce1d037880ea7b56d354281d3f89b11a..e2c178e4136fa99427f4e394da363caf7872edcd 100644 +index b97182b806fddf53ddcdbe57a43500d36e97e2a4..055627fc478a3e64b0e0644cbd9ef4cf5438c696 100644 --- a/build.gradle.kts +++ b/build.gradle.kts -@@ -12,12 +12,6 @@ configurations.named(log4jPlugins.compileClasspathConfigurationName) { +@@ -13,12 +13,10 @@ configurations.named(log4jPlugins.compileClasspathConfigurationName) { val alsoShade: Configuration by configurations.creating dependencies { @@ -18,20 +18,24 @@ index 176f07b1ce1d037880ea7b56d354281d3f89b11a..e2c178e4136fa99427f4e394da363caf - exclude("io.papermc.paper", "paper-api") - } - // Purpur end - - // Plazma start - Use Gradle Version Catalogs - implementation(project(":plazma-api")) -@@ -59,7 +53,7 @@ tasks.jar { ++ // Plazma start - Branding ++ implementation(project(":plazma-api")) ++ implementation(project(":plazma-mojangapi")) ++ // Plazma end - Branding + // Plazma start - Use Gradle version catalogs + /* + // Paper start +@@ -118,7 +116,7 @@ tasks.jar { attributes( "Main-Class" to "org.bukkit.craftbukkit.Main", "Implementation-Title" to "CraftBukkit", - "Implementation-Version" to "git-Purpur-$implementationVersion", // Pufferfish // Purpur -+ "Implementation-Version" to "git-Plazma-$implementationVersion", // Pufferfish // Purpur // Plazma - Rebrand ++ "Implementation-Version" to "git-Plazma-$implementationVersion", // Pufferfish // Purpur // Plazma - Setup Gradle Project "Implementation-Vendor" to date, // Paper "Specification-Title" to "Bukkit", "Specification-Version" to project.version, diff --git a/src/main/java/com/destroystokyo/paper/Metrics.java b/src/main/java/com/destroystokyo/paper/Metrics.java -index 8cde30544e14f8fc2dac32966ae3c21f8cf3a551..1de03b16c513d83550077bab46b52ae6e40eb98e 100644 +index 8cde30544e14f8fc2dac32966ae3c21f8cf3a551..0b3d87d2d43148e6370e7a03e0bb6074891ac800 100644 --- a/src/main/java/com/destroystokyo/paper/Metrics.java +++ b/src/main/java/com/destroystokyo/paper/Metrics.java @@ -593,7 +593,7 @@ public class Metrics { @@ -39,7 +43,7 @@ index 8cde30544e14f8fc2dac32966ae3c21f8cf3a551..1de03b16c513d83550077bab46b52ae6 // Only start Metrics, if it's enabled in the config if (config.getBoolean("enabled", true)) { - Metrics metrics = new Metrics("Purpur", serverUUID, logFailedRequests, Bukkit.getLogger()); // Pufferfish // Purpur -+ Metrics metrics = new Metrics("Plazma", serverUUID, logFailedRequests, Bukkit.getLogger()); // Pufferfish // Purpur // Plazma - Rebrand ++ Metrics metrics = new Metrics("Plazma", serverUUID, logFailedRequests, Bukkit.getLogger()); // Pufferfish // Purpur // Plazma - Branding metrics.addCustomChart(new Metrics.SimplePie("minecraft_version", () -> { String minecraftVersion = Bukkit.getVersion(); @@ -48,12 +52,12 @@ index 8cde30544e14f8fc2dac32966ae3c21f8cf3a551..1de03b16c513d83550077bab46b52ae6 metrics.addCustomChart(new Metrics.SingleLineChart("players", () -> Bukkit.getOnlinePlayers().size())); metrics.addCustomChart(new Metrics.SimplePie("online_mode", () -> Bukkit.getOnlineMode() ? "online" : (io.papermc.paper.configuration.GlobalConfiguration.get().proxies.isProxyOnlineMode() ? "bungee" : "offline"))); // Purpur - metrics.addCustomChart(new Metrics.SimplePie("purpur_version", () -> (org.bukkit.craftbukkit.Main.class.getPackage().getImplementationVersion() != null) ? org.bukkit.craftbukkit.Main.class.getPackage().getImplementationVersion() : "unknown")); // Purpur -+ metrics.addCustomChart(new Metrics.SimplePie("plazma_version", () -> (org.bukkit.craftbukkit.Main.class.getPackage().getImplementationVersion() != null) ? org.bukkit.craftbukkit.Main.class.getPackage().getImplementationVersion() : "unknown")); // Purpur // Plazma // Plazma - Rebrand ++ metrics.addCustomChart(new Metrics.SimplePie("plazma_version", () -> (org.bukkit.craftbukkit.Main.class.getPackage().getImplementationVersion() != null) ? org.bukkit.craftbukkit.Main.class.getPackage().getImplementationVersion() : "unknown")); // Purpur // Plazma // Plazma - Branding metrics.addCustomChart(new Metrics.DrilldownPie("java_version", () -> { Map> map = new HashMap<>(); diff --git a/src/main/java/com/destroystokyo/paper/PaperVersionFetcher.java b/src/main/java/com/destroystokyo/paper/PaperVersionFetcher.java -index 462a6eed350fd660ddaf25d567bb6e97b77d0b2b..523b87063a5fde4f212222710fbcf14c37abd232 100644 +index 462a6eed350fd660ddaf25d567bb6e97b77d0b2b..b04d3eea789f77b2435cb0192635f1be1bab597d 100644 --- a/src/main/java/com/destroystokyo/paper/PaperVersionFetcher.java +++ b/src/main/java/com/destroystokyo/paper/PaperVersionFetcher.java @@ -20,10 +20,11 @@ import java.util.stream.StreamSupport; @@ -61,7 +65,7 @@ index 462a6eed350fd660ddaf25d567bb6e97b77d0b2b..523b87063a5fde4f212222710fbcf14c private static final java.util.regex.Pattern VER_PATTERN = java.util.regex.Pattern.compile("^([0-9\\.]*)\\-.*R"); // R is an anchor, will always give '-R' at end // Purpur start - private static final String DOWNLOAD_PAGE = "https://purpurmc.org/downloads"; -+ private static final String DOWNLOAD_PAGE = "https://github.com/PlazmaMC/Plazma/releases"; // Plazma // Plazma - Rebrand ++ private static final String DOWNLOAD_PAGE = "https://github.com/PlazmaMC/Plazma/releases"; // Plazma // Plazma - Branding private static int distance = -2; public int distance() { return distance; } // Purpur end private static @Nullable String mcVer; @@ -75,8 +79,8 @@ index 462a6eed350fd660ddaf25d567bb6e97b77d0b2b..523b87063a5fde4f212222710fbcf14c public Component getVersionMessage(@Nonnull String serverVersion) { - String[] parts = serverVersion.substring("git-Purpur-".length()).split("[-\\s]"); // Purpur - final Component updateMessage = getUpdateStatusMessage("PurpurMC/Purpur", "ver/" + getMinecraftVersion(), parts[0]); // Purpur -+ String[] parts = serverVersion.substring("git-Plazma-".length()).split("[-\\s]"); // Purpur // Plazma // Plazma - Rebrand -+ final Component updateMessage = getUpdateStatusMessage("PlazmaMC/PlazmaBukkit", (DEVELOPMENT ? "dev/" : "ver/") + getMinecraftVersion(), parts[0]); // Purpur // Plazma // Plazma - Rebrand ++ String[] parts = serverVersion.substring("git-Plazma-".length()).split("[-\\s]"); // Purpur // Plazma // Plazma - Branding ++ final Component updateMessage = getUpdateStatusMessage("PlazmaMC/PlazmaBukkit", (DEVELOPMENT ? "dev/" : "ver/") + getMinecraftVersion(), parts[0]); // Purpur // Plazma // Plazma - Branding final Component history = getHistory(); return history != null ? Component.join(net.kyori.adventure.text.JoinConfiguration.separator(Component.newline()), history, updateMessage) : updateMessage; // Purpur @@ -85,7 +89,7 @@ index 462a6eed350fd660ddaf25d567bb6e97b77d0b2b..523b87063a5fde4f212222710fbcf14c mcVer = result.substring(0, result.length() - 2); // strip 'R' anchor and trailing '-' } else { - org.bukkit.Bukkit.getLogger().warning("Unable to match version to pattern! Report to Purpur!"); // Purpur -+ org.bukkit.Bukkit.getLogger().warning("Unable to match version to pattern! Report to Plazma!"); // Purpur // Plazma // Plazma - Rebrand ++ org.bukkit.Bukkit.getLogger().warning("Unable to match version to pattern! Report to Plazma!"); // Purpur // Plazma // Plazma - Branding org.bukkit.Bukkit.getLogger().warning("Pattern: " + VER_PATTERN.toString()); org.bukkit.Bukkit.getLogger().warning("Version: " + org.bukkit.Bukkit.getBukkitVersion()); } @@ -126,7 +130,7 @@ index 462a6eed350fd660ddaf25d567bb6e97b77d0b2b..523b87063a5fde4f212222710fbcf14c // Contributed by Techcable in GH-65 private static int fetchDistanceFromGitHub(@Nonnull String repo, @Nonnull String branch, @Nonnull String hash) { diff --git a/src/main/java/com/destroystokyo/paper/console/PaperConsole.java b/src/main/java/com/destroystokyo/paper/console/PaperConsole.java -index 3cb56595822799926a8141e60a42f5d1edfc6de5..aa5f7dbaa62d83168055f2d389ba1ed512d1741b 100644 +index 3cb56595822799926a8141e60a42f5d1edfc6de5..3e32c28e1d4c157a2f00dbc6d6e9d71cb3b8f6b7 100644 --- a/src/main/java/com/destroystokyo/paper/console/PaperConsole.java +++ b/src/main/java/com/destroystokyo/paper/console/PaperConsole.java @@ -17,7 +17,7 @@ public final class PaperConsole extends SimpleTerminalConsole { @@ -134,12 +138,12 @@ index 3cb56595822799926a8141e60a42f5d1edfc6de5..aa5f7dbaa62d83168055f2d389ba1ed5 protected LineReader buildReader(LineReaderBuilder builder) { builder - .appName("Purpur") // Purpur -+ .appName("Plazma") // Purpur // Plazma - Rebrand ++ .appName("Plazma") // Purpur // Plazma - Branding .variable(LineReader.HISTORY_FILE, java.nio.file.Paths.get(".console_history")) .completer(new ConsoleCommandCompleter(this.server)) .option(LineReader.Option.COMPLETE_IN_WORD, true); diff --git a/src/main/java/net/minecraft/CrashReport.java b/src/main/java/net/minecraft/CrashReport.java -index c366d84518979e842a6f10f969a5951539ecac93..570b379596cc3088745d3f42b3d917f94c2d9c29 100644 +index f91ea723a1c85f6cf8c4f6dd7f182b948c2f2e81..c5a3f9c2daf3da135cccecb757353534e1688821 100644 --- a/src/main/java/net/minecraft/CrashReport.java +++ b/src/main/java/net/minecraft/CrashReport.java @@ -37,7 +37,7 @@ public class CrashReport { @@ -147,7 +151,7 @@ index c366d84518979e842a6f10f969a5951539ecac93..570b379596cc3088745d3f42b3d917f9 this.title = message; this.exception = cause; - this.systemReport.setDetail("CraftBukkit Information", new org.bukkit.craftbukkit.CraftCrashReport()); // CraftBukkit -+ this.systemReport.setDetail("Plazma Information", new org.bukkit.craftbukkit.CraftCrashReport()); // CraftBukkit // Plazma - Rebrand ++ this.systemReport.setDetail("Plazma Information", new org.bukkit.craftbukkit.CraftCrashReport()); // CraftBukkit // Plazma - Branding } public String getTitle() { @@ -156,19 +160,19 @@ index c366d84518979e842a6f10f969a5951539ecac93..570b379596cc3088745d3f42b3d917f9 // Purpur start stringbuilder.append("// "); - stringbuilder.append("// DO NOT REPORT THIS TO PAPER! REPORT TO PURPUR INSTEAD!"); -+ stringbuilder.append("// DO NOT REPORT THIS TO PAPER OR PURPUR! REPORT TO PLAZMA INSTEAD!"); // Plazma - Rebrand ++ stringbuilder.append("// DO NOT REPORT THIS TO PAPER OR PURPUR! REPORT TO PLAZMA INSTEAD!"); // Plazma - Branding // Purpur end stringbuilder.append("// "); stringbuilder.append(CrashReport.getErrorComment()); diff --git a/src/main/java/net/minecraft/server/Main.java b/src/main/java/net/minecraft/server/Main.java -index 411f1f8c6be072cfc5ba88cbec38dbc4300a41d1..d06b81b7c72f4cd7f6212c470fa0e7d620054122 100644 +index 4ef8eaad4485a2ee920f80556f9dda04e59d2b2a..b91c3b8ca8f840335ba6470658d82c5d71bb75e1 100644 --- a/src/main/java/net/minecraft/server/Main.java +++ b/src/main/java/net/minecraft/server/Main.java -@@ -107,6 +107,16 @@ public class Main { +@@ -105,6 +105,18 @@ public class Main { */ // CraftBukkit end try { -+ // Plazma start - Rebrand ++ // Plazma start - Branding + System.out.println(""" + \033[38;2;236;61;151m┏\033[38;2;236;61;157m━\033[38;2;237;62;163m━\033[38;2;237;62;169m━\033[38;2;238;62;175m━\033[38;2;238;63;181m━\033[38;2;239;63;187m┓\033[38;2;239;63;193m \033[38;2;239;64;200m┏\033[38;2;240;64;206m━\033[38;2;240;64;212m┓\033[38;2;241;65;218m \033[38;2;241;65;224m \033[38;2;242;65;230m \033[38;2;242;66;236m \033[38;2;242;66;242m \033[38;2;237;66;243m \033[38;2;232;67;243m┏\033[38;2;227;67;244m━\033[38;2;221;67;244m━\033[38;2;216;68;244m━\033[38;2;211;68;245m━\033[38;2;205;69;245m┓\033[38;2;200;69;246m \033[38;2;195;69;246m┏\033[38;2;189;70;246m━\033[38;2;184;70;247m━\033[38;2;179;70;247m━\033[38;2;173;71;247m━\033[38;2;168;71;248m━\033[38;2;163;72;248m━\033[38;2;157;72;249m┓\033[38;2;152;72;249m┏\033[38;2;147;73;249m━\033[38;2;141;73;250m━\033[38;2;136;74;250m┓\033[38;2;131;74;250m \033[38;2;125;74;251m \033[38;2;120;75;251m \033[38;2;115;75;251m┏\033[38;2;109;76;252m━\033[38;2;104;76;252m━\033[38;2;99;77;252m┓\033[38;2;94;77;253m \033[38;2;88;77;253m┏\033[38;2;83;78;253m━\033[38;2;78;79;254m━\033[38;2;79;85;254m━\033[38;2;79;91;254m━\033[38;2;80;97;255m┓\033[38;2;80;103;255m\s + \033[38;2;236;61;151m┃\033[38;2;236;61;157m┏\033[38;2;237;62;163m━\033[38;2;237;62;169m━\033[38;2;238;62;175m━\033[38;2;238;63;181m┓\033[38;2;239;63;187m┗\033[38;2;239;63;193m┓\033[38;2;239;64;200m┃\033[38;2;240;64;206m \033[38;2;240;64;212m┃\033[38;2;241;65;218m \033[38;2;241;65;224m \033[38;2;242;65;230m \033[38;2;242;66;236m \033[38;2;242;66;242m \033[38;2;237;66;243m┏\033[38;2;232;67;243m┛\033[38;2;227;67;244m┏\033[38;2;221;67;244m━\033[38;2;216;68;244m━\033[38;2;211;68;245m┓\033[38;2;205;69;245m┗\033[38;2;200;69;246m┓\033[38;2;195;69;246m┗\033[38;2;189;70;246m━\033[38;2;184;70;247m━\033[38;2;179;70;247m┓\033[38;2;173;71;247m \033[38;2;168;71;248m \033[38;2;163;72;248m┏\033[38;2;157;72;249m┛\033[38;2;152;72;249m┃\033[38;2;147;73;249m \033[38;2;141;73;250m \033[38;2;136;74;250m┗\033[38;2;131;74;250m┓\033[38;2;125;74;251m \033[38;2;120;75;251m┏\033[38;2;115;75;251m┛\033[38;2;109;76;252m \033[38;2;104;76;252m \033[38;2;99;77;252m┃\033[38;2;94;77;253m┏\033[38;2;88;77;253m┛\033[38;2;83;78;253m┏\033[38;2;78;79;254m━\033[38;2;79;85;254m━\033[38;2;79;91;254m┓\033[38;2;80;97;255m┗\033[38;2;80;103;255m┓ @@ -177,38 +181,40 @@ index 411f1f8c6be072cfc5ba88cbec38dbc4300a41d1..d06b81b7c72f4cd7f6212c470fa0e7d6 + \033[38;2;236;61;151m┃\033[38;2;236;61;157m \033[38;2;237;62;163m┃\033[38;2;237;62;169m \033[38;2;238;62;175m \033[38;2;238;63;181m \033[38;2;239;63;187m \033[38;2;239;63;193m \033[38;2;239;64;200m┃\033[38;2;240;64;206m \033[38;2;240;64;212m┗\033[38;2;241;65;218m━\033[38;2;241;65;224m━\033[38;2;242;65;230m━\033[38;2;242;66;236m━\033[38;2;242;66;242m┓\033[38;2;237;66;243m┃\033[38;2;232;67;243m \033[38;2;227;67;244m┃\033[38;2;221;67;244m \033[38;2;216;68;244m \033[38;2;211;68;245m┃\033[38;2;205;69;245m \033[38;2;200;69;246m┃\033[38;2;195;69;246m┏\033[38;2;189;70;246m┛\033[38;2;184;70;247m \033[38;2;179;70;247m \033[38;2;173;71;247m┗\033[38;2;168;71;248m━\033[38;2;163;72;248m━\033[38;2;157;72;249m┓\033[38;2;152;72;249m┃\033[38;2;147;73;249m \033[38;2;141;73;250m┃\033[38;2;136;74;250m \033[38;2;131;74;250m┗\033[38;2;125;74;251m━\033[38;2;120;75;251m┛\033[38;2;115;75;251m \033[38;2;109;76;252m┃\033[38;2;104;76;252m \033[38;2;99;77;252m┃\033[38;2;94;77;253m┃\033[38;2;88;77;253m \033[38;2;83;78;253m┃\033[38;2;78;79;254m \033[38;2;79;85;254m \033[38;2;79;91;254m┃\033[38;2;80;97;255m \033[38;2;80;103;255m┃ + \033[38;2;236;61;151m┗\033[38;2;236;61;157m━\033[38;2;237;62;163m┛\033[38;2;237;62;169m \033[38;2;238;62;175m \033[38;2;238;63;181m \033[38;2;239;63;187m \033[38;2;239;63;193m \033[38;2;239;64;200m┗\033[38;2;240;64;206m━\033[38;2;240;64;212m━\033[38;2;241;65;218m━\033[38;2;241;65;224m━\033[38;2;242;65;230m━\033[38;2;242;66;236m━\033[38;2;242;66;242m┛\033[38;2;237;66;243m┗\033[38;2;232;67;243m━\033[38;2;227;67;244m┛\033[38;2;221;67;244m \033[38;2;216;68;244m \033[38;2;211;68;245m┗\033[38;2;205;69;245m━\033[38;2;200;69;246m┛\033[38;2;195;69;246m┗\033[38;2;189;70;246m━\033[38;2;184;70;247m━\033[38;2;179;70;247m━\033[38;2;173;71;247m━\033[38;2;168;71;248m━\033[38;2;163;72;248m━\033[38;2;157;72;249m┛\033[38;2;152;72;249m┗\033[38;2;147;73;249m━\033[38;2;141;73;250m┛\033[38;2;136;74;250m \033[38;2;131;74;250m \033[38;2;125;74;251m \033[38;2;120;75;251m \033[38;2;115;75;251m \033[38;2;109;76;252m┗\033[38;2;104;76;252m━\033[38;2;99;77;252m┛\033[38;2;94;77;253m┗\033[38;2;88;77;253m━\033[38;2;83;78;253m┛\033[38;2;78;79;254m \033[38;2;79;85;254m \033[38;2;79;91;254m┗\033[38;2;80;97;255m━\033[38;2;80;103;255m┛\033[0m + """); -+ // Plazma end - Rebrand ++ if (!org.plazmamc.plazma.Options.iKnowWhatIAmDoing) ++ LOGGER.warn("Warning! Plazma may cause unexpected problems, so be sure to test it thoroughly before using it on a public server."); ++ // Plazma end Path path = (Path) optionset.valueOf("pidFile"); // CraftBukkit diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java -index dfeae138e830e95ab823b6349a91160b02622208..e121cc57ec5bf6f5b1d81e2fd4f551063ac60d64 100644 +index 60b5e0643d933393b5473681ac9261db29fe2416..ac12ccf60b45b150982e79f32d3cdd21c4017cc8 100644 --- a/src/main/java/net/minecraft/server/MinecraftServer.java +++ b/src/main/java/net/minecraft/server/MinecraftServer.java -@@ -963,7 +963,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop { - this.chunkToIndex.defaultReturnValue(Integer.MIN_VALUE); - } - -- protected static final LevelChunk[] EMPTY_LIST = new LevelChunk[0]; -+ protected static final LevelChunk[] EMPTY_LIST = new LevelChunk[0]; // Plazma - Reduce allocations (mark on diff) - - protected LevelChunk[] chunks = EMPTY_LIST; - protected int count; diff --git a/src/main/java/com/destroystokyo/paper/util/maplist/EntityList.java b/src/main/java/com/destroystokyo/paper/util/maplist/EntityList.java index 0133ea6feb1ab88f021f66855669f58367e7420b..a04049bc7738225633ac0b01c470cfbfde86c032 100644 --- a/src/main/java/com/destroystokyo/paper/util/maplist/EntityList.java @@ -102,7 +89,7 @@ index 190c5f0b02a3d99054704ae1afbffb3498ddffe1..042ea49ec61ee327c0f67ddcf0807740 public int size() { diff --git a/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkTaskScheduler.java b/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkTaskScheduler.java -index 049e20407033073b06fcdeb46c38485f4926d778..bbd0b944cef07b2c2a1b1cbecd24fda7a7adec72 100644 +index 17ce14f2dcbf900890efbc2351782bc6f8867068..a8d4acc1dbb3e8e5cd8d181fbd8335f84e252d0c 100644 --- a/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkTaskScheduler.java +++ b/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkTaskScheduler.java @@ -42,6 +42,7 @@ import java.util.function.Consumer; @@ -135,21 +122,8 @@ index ae60bd96b5284d54676d8e7e4dd5d170b526ec1e..359c4b080bd47234e569dce7055da03d } return true; } -diff --git a/src/main/java/io/papermc/paper/plugin/manager/DummyBukkitPluginLoader.java b/src/main/java/io/papermc/paper/plugin/manager/DummyBukkitPluginLoader.java -index aef19b44075a3b2e8696315baa89117dd8ebb513..60adf262a01933b3d38601f7664111ac7d72465f 100644 ---- a/src/main/java/io/papermc/paper/plugin/manager/DummyBukkitPluginLoader.java -+++ b/src/main/java/io/papermc/paper/plugin/manager/DummyBukkitPluginLoader.java -@@ -30,7 +30,7 @@ import java.util.regex.Pattern; - @ApiStatus.Internal - public class DummyBukkitPluginLoader implements PluginLoader { - -- private static final Pattern[] PATTERNS = new Pattern[0]; -+ private static final Pattern[] PATTERNS = new Pattern[0]; // Plazma - Reduce allocations (mark on diff) - - @Override - public @NotNull Plugin loadPlugin(@NotNull File file) throws InvalidPluginException, UnknownDependencyException { diff --git a/src/main/java/io/papermc/paper/plugin/manager/PaperPluginInstanceManager.java b/src/main/java/io/papermc/paper/plugin/manager/PaperPluginInstanceManager.java -index 3e82ea07ca4194844c5528446e2c4a46ff4acee5..1f432767841756905521b3c3000f1e027b3fffe8 100644 +index eedbf46e04b5ae420f9bedcbc2bbb10643ba7e22..1ac70b965f5dd9441658156e19a38419cbcc47d6 100644 --- a/src/main/java/io/papermc/paper/plugin/manager/PaperPluginInstanceManager.java +++ b/src/main/java/io/papermc/paper/plugin/manager/PaperPluginInstanceManager.java @@ -47,6 +47,8 @@ import java.util.logging.Level; @@ -170,16 +144,7 @@ index 3e82ea07ca4194844c5528446e2c4a46ff4acee5..1f432767841756905521b3c3000f1e02 } public boolean isPluginEnabled(@NotNull String name) { -@@ -133,7 +135,7 @@ class PaperPluginInstanceManager { - this.server.getLogger().log(Level.SEVERE, "Unknown error occurred while loading plugins through PluginManager.", e); - } - -- return runtimePluginEntrypointHandler.getPluginProviderStorage().getLoaded().toArray(new JavaPlugin[0]); -+ return runtimePluginEntrypointHandler.getPluginProviderStorage().getLoaded().toArray(EMPTY_JPLUGIN); // Plazma - Reduce allocations - } - - // The behavior of this is that all errors are logged instead of being thrown -@@ -150,7 +152,7 @@ class PaperPluginInstanceManager { +@@ -136,7 +138,7 @@ class PaperPluginInstanceManager { this.server.getLogger().log(Level.SEVERE, "Unknown error occurred while loading plugins through PluginManager.", e); } @@ -189,7 +154,7 @@ index 3e82ea07ca4194844c5528446e2c4a46ff4acee5..1f432767841756905521b3c3000f1e02 // Plugins are disabled in order like this inorder to "rougly" prevent diff --git a/src/main/java/io/papermc/paper/util/CollisionUtil.java b/src/main/java/io/papermc/paper/util/CollisionUtil.java -index ee8e9c0e3690e78f3cc621ddfca89ea4256d4803..2b27f73896ce11908ff7b488fa65d6251f791e99 100644 +index ee0331a6bc40cdde08d926fd8eb1dc642630c2e5..bafa781ea77afa159576afca3449bacb01ab387d 100644 --- a/src/main/java/io/papermc/paper/util/CollisionUtil.java +++ b/src/main/java/io/papermc/paper/util/CollisionUtil.java @@ -1149,7 +1149,7 @@ public final class CollisionUtil { @@ -197,7 +162,7 @@ index ee8e9c0e3690e78f3cc621ddfca89ea4256d4803..2b27f73896ce11908ff7b488fa65d625 private static final MergedVoxelCoordinateList EMPTY = new MergedVoxelCoordinateList( - new double[] { 0.0 }, 0.0, new int[0], new int[0], 0 -+ new double[] { 0.0 }, 0.0, org.plazmamc.plazma.constants.Empty.INT, org.plazmamc.plazma.constants.Empty.INT, 0 ++ new double[] { 0.0 }, 0.0, org.plazmamc.plazma.constants.Empty.INT, org.plazmamc.plazma.constants.Empty.INT, 0 // Plazma - Reduce allocations ); private static int[] getIndices(final int length) { @@ -233,7 +198,7 @@ index c78cbec447032de9fe69748591bef6be300160ed..8ca248e844e73685a8d44a9661446573 } else { this.storage = Arrays.copyOf(this.storage, this.storage.length * 2); diff --git a/src/main/java/net/minecraft/CrashReport.java b/src/main/java/net/minecraft/CrashReport.java -index 570b379596cc3088745d3f42b3d917f94c2d9c29..b96d89f5ff10e3866b9e4d3566cdb0cf56ab9c1c 100644 +index c5a3f9c2daf3da135cccecb757353534e1688821..e5585c6befeef62ecf130e8dabbe6b78f9e90a65 100644 --- a/src/main/java/net/minecraft/CrashReport.java +++ b/src/main/java/net/minecraft/CrashReport.java @@ -30,7 +30,7 @@ public class CrashReport { @@ -259,10 +224,10 @@ index 2176171954609fd88f97f93408e14e018c1d6eaa..2a5576dc66a9b1f56e06ba47bef4fe88 public CrashReportCategory(String title) { this.title = title; diff --git a/src/main/java/net/minecraft/Util.java b/src/main/java/net/minecraft/Util.java -index 0bd367235f80c1f0d319a6aa5130d82ad82d895c..ed44bf95ce6e39961af77d045a1f69ffeee62d1b 100644 +index 01a12f4d6f3c2f09bffc78692443b9fd391db45a..9a6e4538dd09263544f39c93f5f1480dc589fb4d 100644 --- a/src/main/java/net/minecraft/Util.java +++ b/src/main/java/net/minecraft/Util.java -@@ -458,7 +458,7 @@ public class Util { +@@ -409,7 +409,7 @@ public class Util { } else if (futures.size() == 1) { return futures.get(0).thenApply(List::of); } else { @@ -285,7 +250,7 @@ index 06648f9751fd8a322d0809ffebf6a544596ee1a4..b0ea87e2ed8f9bf04b33c2ff8a827d4f @Override diff --git a/src/main/java/net/minecraft/nbt/CompoundTag.java b/src/main/java/net/minecraft/nbt/CompoundTag.java -index 4e005b7b062e3231f564d284887ea1c2783a4e7d..059a1e49f0607a4ecd9d558d9423f2f5a71129c6 100644 +index 23916b011ed0645ab284fb080c9555921290d875..2abe6c8dcbf20bf550bb790fd4e18ccc848d1065 100644 --- a/src/main/java/net/minecraft/nbt/CompoundTag.java +++ b/src/main/java/net/minecraft/nbt/CompoundTag.java @@ -409,7 +409,7 @@ public class CompoundTag implements Tag { @@ -329,10 +294,10 @@ index ff13d67151c50ea11a45117e524c7524e2b1a202..5048ec707c147b9a5b2dd8736d518d93 @Override diff --git a/src/main/java/net/minecraft/nbt/ListTag.java b/src/main/java/net/minecraft/nbt/ListTag.java -index 154bffd341e43be0a0fa710cfbed1a2094f249a3..83c36d4520dfebbf1c2d8d3cec7bd41356804464 100644 +index 24ad8d22b5180cd7d7f793e3074e438f9192448f..c902b478d331a1cad9ac66a6eeb66c19fbcd8789 100644 --- a/src/main/java/net/minecraft/nbt/ListTag.java +++ b/src/main/java/net/minecraft/nbt/ListTag.java -@@ -258,7 +258,7 @@ public class ListTag extends CollectionTag { +@@ -279,7 +279,7 @@ public class ListTag extends CollectionTag { } } @@ -341,7 +306,7 @@ index 154bffd341e43be0a0fa710cfbed1a2094f249a3..83c36d4520dfebbf1c2d8d3cec7bd413 } public long[] getLongArray(int index) { -@@ -269,7 +269,7 @@ public class ListTag extends CollectionTag { +@@ -291,7 +291,7 @@ public class ListTag extends CollectionTag { } } @@ -364,10 +329,10 @@ index 2e5c34ebb94a1536cf09d71bdf052a49ecb9159d..144d3bbe80fc0f459a06017a19929e3e @Override diff --git a/src/main/java/net/minecraft/nbt/NbtIo.java b/src/main/java/net/minecraft/nbt/NbtIo.java -index 9f659af04a5362ae3645d072caa090682760dc9f..d2969c1ce98a958bd25cb8b9c38f6a57a3311701 100644 +index c2044d2e8ce2d4747aa73ba90e5b975b1b7d2c19..b4e043379ece86ce39eacd4c1eb9114a2b6727ca 100644 --- a/src/main/java/net/minecraft/nbt/NbtIo.java +++ b/src/main/java/net/minecraft/nbt/NbtIo.java -@@ -277,7 +277,7 @@ public class NbtIo { +@@ -249,7 +249,7 @@ public class NbtIo { @Nullable public static CompoundTag read(Path path) throws IOException { @@ -392,20 +357,42 @@ index a2920b8a9eff77d9c5d1d7f70ad3abdacba8f0fa..70d776d5cfdb0612f65d92333d6f872a protected CipherBase(Cipher cipher) { this.cipher = cipher; diff --git a/src/main/java/net/minecraft/network/Connection.java b/src/main/java/net/minecraft/network/Connection.java -index 9f274048be29ed54dd91983447beadf076cf7438..ba54c16f194127e3d6affeaa5a75616467e4e6a2 100644 +index 9e31954212b1d6162dca2fbc91d373e908560335..5e7b4363e6f83d4145954a96d6b6b610d392c7d1 100644 --- a/src/main/java/net/minecraft/network/Connection.java +++ b/src/main/java/net/minecraft/network/Connection.java -@@ -316,7 +316,7 @@ public class Connection extends SimpleChannelInboundHandler> { +@@ -322,7 +322,7 @@ public class Connection extends SimpleChannelInboundHandler> { } - private void validateListener(ProtocolInfo state, PacketListener listener) { -- Validate.notNull(listener, "packetListener", new Object[0]); -+ Validate.notNull(listener, "packetListener"/*, new Object[0]*/); // Plazma - Reduce allocations - PacketFlow enumprotocoldirection = listener.flow(); - String s; + public void setListener(PacketListener packetListener) { +- Validate.notNull(packetListener, "packetListener", new Object[0]); ++ Validate.notNull(packetListener, "packetListener"/*, new Object[0]*/); // Plazma - Reduce allocations + PacketFlow enumprotocoldirection = packetListener.flow(); + if (enumprotocoldirection != this.receiving) { +diff --git a/src/main/java/net/minecraft/network/chat/Component.java b/src/main/java/net/minecraft/network/chat/Component.java +index d3a80d0a23be762c05931ae8001d98e43cab2b4a..a94feef330b1836a2fc009405c52945551b7590a 100644 +--- a/src/main/java/net/minecraft/network/chat/Component.java ++++ b/src/main/java/net/minecraft/network/chat/Component.java +@@ -191,7 +191,7 @@ public interface Component extends Message, FormattedText, Iterable { + } + + static MutableComponent translatable(String key) { +- return MutableComponent.create(new TranslatableContents(key, (String) null, TranslatableContents.NO_ARGS)); ++ return MutableComponent.create(new TranslatableContents(key, (String) null, org.plazmamc.plazma.constants.Empty.OBJECT)); // Plazma - Reduce allocations + } + + static MutableComponent translatable(String key, Object... args) { +@@ -211,7 +211,7 @@ public interface Component extends Message, FormattedText, Iterable { + } + + static MutableComponent translatableWithFallback(String key, @Nullable String fallback) { +- return MutableComponent.create(new TranslatableContents(key, fallback, TranslatableContents.NO_ARGS)); ++ return MutableComponent.create(new TranslatableContents(key, fallback, org.plazmamc.plazma.constants.Empty.OBJECT)); // Plazma - Reduce allocations + } + + static MutableComponent translatableWithFallback(String key, @Nullable String fallback, Object... args) { diff --git a/src/main/java/net/minecraft/network/chat/contents/TranslatableContents.java b/src/main/java/net/minecraft/network/chat/contents/TranslatableContents.java -index 4aa6232bf0f72fcde32d257100bd15b1c5192aaa..2bf2ea4242af7d94bbc1a6fa4b255576588608f0 100644 +index 18e53db59082bae94922edc4baa812aa6f089576..c922919e15a4157b7a46728caa4a7df0b5fcce1f 100644 --- a/src/main/java/net/minecraft/network/chat/contents/TranslatableContents.java +++ b/src/main/java/net/minecraft/network/chat/contents/TranslatableContents.java @@ -29,7 +29,7 @@ import net.minecraft.util.ExtraCodecs; @@ -414,7 +401,7 @@ index 4aa6232bf0f72fcde32d257100bd15b1c5192aaa..2bf2ea4242af7d94bbc1a6fa4b255576 public class TranslatableContents implements ComponentContents { - public static final Object[] NO_ARGS = new Object[0]; + // public static final Object[] NO_ARGS = new Object[0]; // Plazma - Reduce allocations - private static final Codec PRIMITIVE_ARG_CODEC = ExtraCodecs.JAVA.validate(TranslatableContents::filterAllowedArguments); + private static final Codec PRIMITIVE_ARG_CODEC = ExtraCodecs.validate(ExtraCodecs.JAVA, TranslatableContents::filterAllowedArguments); private static final Codec ARG_CODEC = Codec.either(PRIMITIVE_ARG_CODEC, ComponentSerialization.CODEC) .xmap( @@ -69,7 +69,7 @@ public class TranslatableContents implements ComponentContents { @@ -422,15 +409,15 @@ index 4aa6232bf0f72fcde32d257100bd15b1c5192aaa..2bf2ea4242af7d94bbc1a6fa4b255576 private static Object[] adjustArgs(Optional> args) { - return args.map(list -> list.isEmpty() ? NO_ARGS : list.toArray()).orElse(NO_ARGS); -+ return args.filter(list -> !list.isEmpty()).map(List::toArray).orElse(org.plazmamc.plazma.constants.Empty.OBJECT); // Plazma - Reduce allocations ++ return args.map(list -> list.isEmpty() ? org.plazmamc.plazma.constants.Empty.OBJECT : list.toArray()).orElse(org.plazmamc.plazma.constants.Empty.OBJECT); } private static TranslatableContents create(String key, Optional fallback, Optional> args) { diff --git a/src/main/java/net/minecraft/network/protocol/game/ClientboundSectionBlocksUpdatePacket.java b/src/main/java/net/minecraft/network/protocol/game/ClientboundSectionBlocksUpdatePacket.java -index 1a37654aff9a9c86c9f7af10a1cf721371f0c5ec..0e61f8cb323b2941460cda5bb94d63a126877e5a 100644 +index ccdc2345465313991f065e1176b58fb7d5e8722f..bb50e69a4d84e4753db77be071cc6fc73b08a981 100644 --- a/src/main/java/net/minecraft/network/protocol/game/ClientboundSectionBlocksUpdatePacket.java +++ b/src/main/java/net/minecraft/network/protocol/game/ClientboundSectionBlocksUpdatePacket.java -@@ -66,7 +66,7 @@ public class ClientboundSectionBlocksUpdatePacket implements Packet blockChanges) { this.sectionPos = sectionPos; this.positions = blockChanges.keySet().toShortArray(); @@ -440,10 +427,10 @@ index 1a37654aff9a9c86c9f7af10a1cf721371f0c5ec..0e61f8cb323b2941460cda5bb94d63a1 // Paper end - Multi Block Change API diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java -index e121cc57ec5bf6f5b1d81e2fd4f551063ac60d64..7b27af22a55d8455b25f80f783dfc3be97f68c25 100644 +index ac12ccf60b45b150982e79f32d3cdd21c4017cc8..1032916def98f0607fabb1bbb550ba2ff70b3019 100644 --- a/src/main/java/net/minecraft/server/MinecraftServer.java +++ b/src/main/java/net/minecraft/server/MinecraftServer.java -@@ -1541,13 +1541,10 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop loadStatusIcon() { @@ -462,7 +449,7 @@ index e121cc57ec5bf6f5b1d81e2fd4f551063ac60d64..7b27af22a55d8455b25f80f783dfc3be return optional.flatMap((path) -> { try { diff --git a/src/main/java/net/minecraft/server/PlayerAdvancements.java b/src/main/java/net/minecraft/server/PlayerAdvancements.java -index ef520d1dd00ae9473c1f34e2df4d8b064fe4d6ea..b3b0c18b48ad8cb1fff320b516f1026fd2dbb1ab 100644 +index ba8a8575af92541cef2e116743d51cd68d1e794a..140079be22acbb898720c538c4ffb06194da0860 100644 --- a/src/main/java/net/minecraft/server/PlayerAdvancements.java +++ b/src/main/java/net/minecraft/server/PlayerAdvancements.java @@ -130,7 +130,7 @@ public class PlayerAdvancements { @@ -507,31 +494,31 @@ index bae0d208b31aa0a6977c30f2f8484ab3c316bc71..981c3023044f3cc6dc22ada20cd4bedc } ); diff --git a/src/main/java/net/minecraft/server/level/ChunkMap.java b/src/main/java/net/minecraft/server/level/ChunkMap.java -index 4e6fccec4f5ca14562bf5bae495ac36c14982d85..3181459952634fc74f1b5b07b99d4adb888efa08 100644 +index bb412ca874b85d777c0e3565fcefcee15b23182b..9116b3d298a9bb6e550d299f76ff1243da824cbe 100644 --- a/src/main/java/net/minecraft/server/level/ChunkMap.java +++ b/src/main/java/net/minecraft/server/level/ChunkMap.java -@@ -122,6 +122,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - public static final int MIN_VIEW_DISTANCE = 2; - public static final int MAX_VIEW_DISTANCE = 32; - public static final int FORCED_TICKET_LEVEL = ChunkLevel.byStatus(FullChunkStatus.ENTITY_TICKING); -+ private static final ServerPlayerConnection[] EMPTY = new ServerPlayerConnection[0]; // Plazma - Reduce allocations - // Paper - rewrite chunk system - public final ServerLevel level; - private final ThreadedLevelLightEngine lightEngine; -@@ -1361,7 +1362,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider +@@ -109,6 +109,7 @@ import org.bukkit.craftbukkit.generator.CustomChunkGenerator; + + public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider { + ++ private static final ServerPlayerConnection[] EMPTY_CONNECTION = new ServerPlayerConnection[0]; // Plazma - Reduce allocations + private static final byte CHUNK_TYPE_REPLACEABLE = -1; + private static final byte CHUNK_TYPE_UNKNOWN = 0; + private static final byte CHUNK_TYPE_FULL = 1; +@@ -1347,7 +1348,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider // stuff could have been removed, so we need to check the trackedPlayers set // for players that were removed - for (ServerPlayerConnection conn : this.seenBy.toArray(new ServerPlayerConnection[0])) { // avoid CME -+ for (ServerPlayerConnection conn : this.seenBy.toArray(EMPTY)) { // avoid CME // Plazma - Reduce allocations ++ for (ServerPlayerConnection conn : this.seenBy.toArray(EMPTY_CONNECTION)) { // avoid CME // Plazma - Reduce allocations if (newTrackerCandidates == null || !newTrackerCandidates.contains(conn.getPlayer())) { this.updatePlayer(conn.getPlayer()); } diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java -index f72af2feb74626abbdfbfd090c15357457810240..0aaa0e63f7c7ea8fe6cedd5b0c63fbed2fb3e168 100644 +index 00ac2902be93327c7dd1bf78ee5922d7954f1b26..2ac8c77684c2e4cfc30bc0d39bcaf853dcd3bfb9 100644 --- a/src/main/java/net/minecraft/server/level/ServerLevel.java +++ b/src/main/java/net/minecraft/server/level/ServerLevel.java -@@ -1439,7 +1439,7 @@ public class ServerLevel extends Level implements WorldGenLevel { +@@ -1449,7 +1449,7 @@ public class ServerLevel extends Level implements WorldGenLevel { public static List getCurrentlyTickingEntities() { Entity ticking = currentlyTickingEntity.get(); @@ -541,25 +528,21 @@ index f72af2feb74626abbdfbfd090c15357457810240..0aaa0e63f7c7ea8fe6cedd5b0c63fbed return ret; } diff --git a/src/main/java/net/minecraft/server/network/ServerLoginPacketListenerImpl.java b/src/main/java/net/minecraft/server/network/ServerLoginPacketListenerImpl.java -index 3b4fadb37eafb2f7b0ce4d6b276d2fdaa8287521..f276fe09f12fd69b09339706faaf2bafd53bc749 100644 +index 8b62f992ec61d0a66a3856b4928ee2d705548291..c0729d3a38e0b9924711c0ca96afd138fa69fa3b 100644 --- a/src/main/java/net/minecraft/server/network/ServerLoginPacketListenerImpl.java +++ b/src/main/java/net/minecraft/server/network/ServerLoginPacketListenerImpl.java -@@ -165,12 +165,12 @@ public class ServerLoginPacketListenerImpl implements ServerLoginPacketListener, +@@ -138,8 +138,8 @@ public class ServerLoginPacketListenerImpl implements ServerLoginPacketListener, @Override public void handleHello(ServerboundHelloPacket packet) { - Validate.validState(this.state == ServerLoginPacketListenerImpl.State.HELLO, "Unexpected hello packet", new Object[0]); +- if (io.papermc.paper.configuration.GlobalConfiguration.get().proxies.isProxyOnlineMode() && io.papermc.paper.configuration.GlobalConfiguration.get().unsupportedSettings.performUsernameValidation && !this.iKnowThisMayNotBeTheBestIdeaButPleaseDisableUsernameValidation) Validate.validState(Player.isValidUsername(packet.name()), "Invalid characters in username", new Object[0]); // Paper - config username validation + Validate.validState(this.state == ServerLoginPacketListenerImpl.State.HELLO, "Unexpected hello packet"/*, new Object[0]*/); // Plazma - Reduce allocations - // Paper start - Validate usernames - if (io.papermc.paper.configuration.GlobalConfiguration.get().proxies.isProxyOnlineMode() - && io.papermc.paper.configuration.GlobalConfiguration.get().unsupportedSettings.performUsernameValidation - && !this.iKnowThisMayNotBeTheBestIdeaButPleaseDisableUsernameValidation) { -- Validate.validState(StringUtil.isReasonablePlayerName(packet.name()), "Invalid characters in username", new Object[0]); -+ Validate.validState(StringUtil.isReasonablePlayerName(packet.name()), "Invalid characters in username"/*, new Object[0]*/); // Plazma - Reduce allocations - } - // Paper end - Validate usernames ++ if (io.papermc.paper.configuration.GlobalConfiguration.get().proxies.isProxyOnlineMode() && io.papermc.paper.configuration.GlobalConfiguration.get().unsupportedSettings.performUsernameValidation && !this.iKnowThisMayNotBeTheBestIdeaButPleaseDisableUsernameValidation) Validate.validState(Player.isValidUsername(packet.name()), "Invalid characters in username"/*, new Object[0]*/); // Paper - config username validation // Plazma - Reduce allocations this.requestedUsername = packet.name(); -@@ -268,7 +268,7 @@ public class ServerLoginPacketListenerImpl implements ServerLoginPacketListener, + GameProfile gameprofile = this.server.getSingleplayerProfile(); + +@@ -223,7 +223,7 @@ public class ServerLoginPacketListenerImpl implements ServerLoginPacketListener, @Override public void handleKey(ServerboundKeyPacket packet) { @@ -568,17 +551,38 @@ index 3b4fadb37eafb2f7b0ce4d6b276d2fdaa8287521..f276fe09f12fd69b09339706faaf2baf final String s; -@@ -448,7 +448,7 @@ public class ServerLoginPacketListenerImpl implements ServerLoginPacketListener, +@@ -403,7 +403,7 @@ public class ServerLoginPacketListenerImpl implements ServerLoginPacketListener, @Override public void handleLoginAcknowledgement(ServerboundLoginAcknowledgedPacket packet) { - Validate.validState(this.state == ServerLoginPacketListenerImpl.State.PROTOCOL_SWITCHING, "Unexpected login acknowledgement packet", new Object[0]); + Validate.validState(this.state == ServerLoginPacketListenerImpl.State.PROTOCOL_SWITCHING, "Unexpected login acknowledgement packet"/*, new Object[0]*/); // Plazma - Reduce allocations - this.connection.setupOutboundProtocol(ConfigurationProtocols.CLIENTBOUND); - CommonListenerCookie commonlistenercookie = CommonListenerCookie.createInitial((GameProfile) Objects.requireNonNull(this.authenticatedProfile), this.transferred); + CommonListenerCookie commonlistenercookie = CommonListenerCookie.createInitial((GameProfile) Objects.requireNonNull(this.authenticatedProfile)); ServerConfigurationPacketListenerImpl serverconfigurationpacketlistenerimpl = new ServerConfigurationPacketListenerImpl(this.server, this.connection, commonlistenercookie, this.player); // CraftBukkit + +diff --git a/src/main/java/net/minecraft/server/players/PlayerList.java b/src/main/java/net/minecraft/server/players/PlayerList.java +index ac1e0c66f167218306504db6037cc1d6509072a0..60aa2984e490374ab2659f9d0a4821f1ea17c700 100644 +--- a/src/main/java/net/minecraft/server/players/PlayerList.java ++++ b/src/main/java/net/minecraft/server/players/PlayerList.java +@@ -133,6 +133,7 @@ public abstract class PlayerList { + private static final Logger LOGGER = LogUtils.getLogger(); + private static final int SEND_PLAYER_INFO_INTERVAL = 600; + private static final SimpleDateFormat BAN_DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd 'at' HH:mm:ss z"); ++ private static final org.bukkit.event.player.PlayerRespawnEvent.RespawnFlag[] EMPTY_FLAG = new org.bukkit.event.player.PlayerRespawnEvent.RespawnFlag[0]; // Plazma - Reduce allocations + private final MinecraftServer server; + public final List players = new java.util.concurrent.CopyOnWriteArrayList(); // CraftBukkit - ArrayList -> CopyOnWriteArrayList: Iterator safety + private final Map playersByUUID = Maps.newHashMap(); +@@ -816,7 +817,7 @@ public abstract class PlayerList { + + public ServerPlayer respawn(ServerPlayer entityplayer, ServerLevel worldserver, boolean flag, Location location, boolean avoidSuffocation, RespawnReason reason) { + // Paper start - Expand PlayerRespawnEvent +- return respawn(entityplayer, worldserver, flag, location, avoidSuffocation, reason, new org.bukkit.event.player.PlayerRespawnEvent.RespawnFlag[0]); ++ return respawn(entityplayer, worldserver, flag, location, avoidSuffocation, reason, EMPTY_FLAG); // Plazma - Reduce allocations + } + + public ServerPlayer respawn(ServerPlayer entityplayer, ServerLevel worldserver, boolean flag, Location location, boolean avoidSuffocation, RespawnReason reason, org.bukkit.event.player.PlayerRespawnEvent.RespawnFlag...respawnFlags) { diff --git a/src/main/java/net/minecraft/server/players/StoredUserList.java b/src/main/java/net/minecraft/server/players/StoredUserList.java -index c038da20b76c0b7b1c18471b20be01e849d29f3a..c76404dfce176c02b769968de5a5ecc647059ae1 100644 +index 7e133752ccb1ea7c0b4fa781feb1a88e2cfdcf6d..1289e8e9c54a584f5037ea8e852df37376af093d 100644 --- a/src/main/java/net/minecraft/server/players/StoredUserList.java +++ b/src/main/java/net/minecraft/server/players/StoredUserList.java @@ -76,7 +76,7 @@ public abstract class StoredUserList> { @@ -586,7 +590,7 @@ index c038da20b76c0b7b1c18471b20be01e849d29f3a..c76404dfce176c02b769968de5a5ecc6 public String[] getUserList() { - return (String[]) this.map.keySet().toArray(new String[0]); -+ return (String[]) this.map.keySet().toArray(org.plazmamc.plazma.constants.Empty.STRING); // Plazma - Reduce allocations ++ return this.map.keySet().toArray(org.plazmamc.plazma.constants.Empty.STRING); // Plazma - Reduce allocations } public boolean isEmpty() { @@ -624,24 +628,11 @@ index 01f5b946fabbe34f31110e75973dab9f39897346..2564c21900df7ca3c58872741ec8f68b } @Override -diff --git a/src/main/java/net/minecraft/util/monitoring/jmx/MinecraftServerStatistics.java b/src/main/java/net/minecraft/util/monitoring/jmx/MinecraftServerStatistics.java -index a371f685534bf161f476ccea431fec6a80aca9c1..3ea10630deeb6624afd33c25a7ef023361312b30 100644 ---- a/src/main/java/net/minecraft/util/monitoring/jmx/MinecraftServerStatistics.java -+++ b/src/main/java/net/minecraft/util/monitoring/jmx/MinecraftServerStatistics.java -@@ -43,7 +43,7 @@ public final class MinecraftServerStatistics implements DynamicMBean { - .map(MinecraftServerStatistics.AttributeDescription::asMBeanAttributeInfo) - .toArray(MBeanAttributeInfo[]::new); - this.mBeanInfo = new MBeanInfo( -- MinecraftServerStatistics.class.getSimpleName(), "metrics for dedicated server", mBeanAttributeInfos, null, null, new MBeanNotificationInfo[0] -+ MinecraftServerStatistics.class.getSimpleName(), "metrics for dedicated server", mBeanAttributeInfos, null, null, new MBeanNotificationInfo[0] // Plazma - Reduce allocations - ); - } - diff --git a/src/main/java/net/minecraft/world/entity/animal/Bee.java b/src/main/java/net/minecraft/world/entity/animal/Bee.java -index 539170813921de2dfcd7ef84dd7512d73cd27e68..104b89bf617138c301e671d76be8897a41d3b9e0 100644 +index 997ab942be9f742804041b07d607e7dd6473ba96..14a61e58987464d7865caf98e6232fb1334c6d88 100644 --- a/src/main/java/net/minecraft/world/entity/animal/Bee.java +++ b/src/main/java/net/minecraft/world/entity/animal/Bee.java -@@ -248,7 +248,7 @@ public class Bee extends Animal implements NeutralMob, FlyingAnimal { +@@ -251,7 +251,7 @@ public class Bee extends Animal implements NeutralMob, FlyingAnimal { this.goalSelector.addGoal(8, new Bee.BeeWanderGoal()); this.goalSelector.addGoal(9, new FloatGoal(this)); this.targetSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur @@ -651,10 +642,10 @@ index 539170813921de2dfcd7ef84dd7512d73cd27e68..104b89bf617138c301e671d76be8897a this.targetSelector.addGoal(3, new ResetUniversalAngerTargetGoal<>(this, true)); } diff --git a/src/main/java/net/minecraft/world/entity/animal/IronGolem.java b/src/main/java/net/minecraft/world/entity/animal/IronGolem.java -index 12bc57d36d76f49596df0004fda31a6a678be60c..4968459a1620e8023bba07800e233e172a54b082 100644 +index 442eb602f5c82550a87e218e2013171b718abd62..b68f7d1fba18cf08a1844a179f07cb6d873c236b 100644 --- a/src/main/java/net/minecraft/world/entity/animal/IronGolem.java +++ b/src/main/java/net/minecraft/world/entity/animal/IronGolem.java -@@ -117,7 +117,7 @@ public class IronGolem extends AbstractGolem implements NeutralMob { +@@ -122,7 +122,7 @@ public class IronGolem extends AbstractGolem implements NeutralMob { this.goalSelector.addGoal(8, new RandomLookAroundGoal(this)); this.targetSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur this.targetSelector.addGoal(1, new DefendVillageTargetGoal(this)); @@ -664,10 +655,10 @@ index 12bc57d36d76f49596df0004fda31a6a678be60c..4968459a1620e8023bba07800e233e17 this.targetSelector.addGoal(3, new NearestAttackableTargetGoal<>(this, Mob.class, 5, false, false, (entityliving) -> { return entityliving instanceof Enemy && !(entityliving instanceof Creeper); diff --git a/src/main/java/net/minecraft/world/entity/animal/Panda.java b/src/main/java/net/minecraft/world/entity/animal/Panda.java -index 7bd81d073ce4a8d5981f256415d3e99e13da79ba..2dd3feee4b96305549dc65ee4388ceee501b832a 100644 +index adfa18c941b5070692ed855d1d609993ca49a01d..b44e752b4fa18d25ca7ac989b1eaacc138512848 100644 --- a/src/main/java/net/minecraft/world/entity/animal/Panda.java +++ b/src/main/java/net/minecraft/world/entity/animal/Panda.java -@@ -347,7 +347,7 @@ public class Panda extends Animal { +@@ -340,7 +340,7 @@ public class Panda extends Animal { this.goalSelector.addGoal(13, new FollowParentGoal(this, 1.25D)); this.goalSelector.addGoal(14, new WaterAvoidingRandomStrollGoal(this, 1.0D)); this.targetSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur @@ -677,23 +668,23 @@ index 7bd81d073ce4a8d5981f256415d3e99e13da79ba..2dd3feee4b96305549dc65ee4388ceee public static AttributeSupplier.Builder createAttributes() { diff --git a/src/main/java/net/minecraft/world/entity/animal/Rabbit.java b/src/main/java/net/minecraft/world/entity/animal/Rabbit.java -index 02702390c0b336762ce8c0d38d804e6f24ebbfd4..819fb1da353d36c594023f99f5c8123bc6d77326 100644 +index eae2488f2a46e543b496b7a2919aabbb55dcb825..3383d61551f28b44943c0e935aedfa3cc9ac7e93 100644 --- a/src/main/java/net/minecraft/world/entity/animal/Rabbit.java +++ b/src/main/java/net/minecraft/world/entity/animal/Rabbit.java -@@ -458,7 +458,7 @@ public class Rabbit extends Animal implements VariantHolder { +@@ -461,7 +461,7 @@ public class Rabbit extends Animal implements VariantHolder { if (variant == Rabbit.Variant.EVIL) { this.getAttribute(Attributes.ARMOR).setBaseValue(8.0D); this.goalSelector.addGoal(4, new MeleeAttackGoal(this, 1.4D, true)); - this.targetSelector.addGoal(1, (new HurtByTargetGoal(this, new Class[0])).setAlertOthers()); -+ this.targetSelector.addGoal(1, (new HurtByTargetGoal(this/*, new Class[0]*/)).setAlertOthers()); // Plazma - Reduce allocations ++ this.targetSelector.addGoal(1, (new HurtByTargetGoal(this/*,new Class[0]*/)).setAlertOthers()); // Plazma - Reduce allocations this.targetSelector.addGoal(2, new NearestAttackableTargetGoal<>(this, Player.class, true)); this.targetSelector.addGoal(2, new NearestAttackableTargetGoal<>(this, Wolf.class, true)); if (!this.hasCustomName()) { diff --git a/src/main/java/net/minecraft/world/entity/animal/Wolf.java b/src/main/java/net/minecraft/world/entity/animal/Wolf.java -index b12544d1280f39b6c365317a0f4965c8d65b6497..efc7504a01cb0d32a4b6665a5521c8f7f6da25d9 100644 +index a90055fe8819a32180754b6060a0f88e81d1a3b6..68825a2ad5a6ea1b0784a935206bd150d29e25f9 100644 --- a/src/main/java/net/minecraft/world/entity/animal/Wolf.java +++ b/src/main/java/net/minecraft/world/entity/animal/Wolf.java -@@ -248,7 +248,7 @@ public class Wolf extends TamableAnimal implements NeutralMob, VariantHolder(this, Animal.class, false, Wolf.PREY_SELECTOR)); // Purpur - moved to updatePathfinders() this.targetSelector.addGoal(6, new NonTameRandomTargetGoal<>(this, Turtle.class, false, Turtle.BABY_ON_LAND_SELECTOR)); diff --git a/src/main/java/net/minecraft/world/entity/animal/allay/Allay.java b/src/main/java/net/minecraft/world/entity/animal/allay/Allay.java -index bca7b7192debb3a34a08047010a2438e7b7e2a78..0070e4b4eec5bd211155695e456fd2c403548c57 100644 +index c783ce59ea766e6c46a3313628b961f27e01ee8b..518660609566ffe943103b305472590ee9f0a257 100644 --- a/src/main/java/net/minecraft/world/entity/animal/allay/Allay.java +++ b/src/main/java/net/minecraft/world/entity/animal/allay/Allay.java @@ -84,7 +84,7 @@ public class Allay extends PathfinderMob implements InventoryCarrier, VibrationS @@ -715,24 +706,11 @@ index bca7b7192debb3a34a08047010a2438e7b7e2a78..0070e4b4eec5bd211155695e456fd2c4 public static final ImmutableList THROW_SOUND_PITCHES = ImmutableList.of(0.5625F, 0.625F, 0.75F, 0.9375F, 1.0F, 1.0F, 1.125F, 1.25F, 1.5F, 1.875F, 2.0F, 2.25F, new Float[]{2.5F, 3.0F, 3.75F, 4.0F}); private final DynamicGameEventListener dynamicVibrationListener; private VibrationSystem.Data vibrationData; -diff --git a/src/main/java/net/minecraft/world/entity/boss/enderdragon/phases/EnderDragonPhase.java b/src/main/java/net/minecraft/world/entity/boss/enderdragon/phases/EnderDragonPhase.java -index 48826eb0a960f7af6dd2ef184a8aed744a1d8f83..32c3ff7331681af1d8dc583cf5208d72035ea150 100644 ---- a/src/main/java/net/minecraft/world/entity/boss/enderdragon/phases/EnderDragonPhase.java -+++ b/src/main/java/net/minecraft/world/entity/boss/enderdragon/phases/EnderDragonPhase.java -@@ -5,7 +5,7 @@ import java.util.Arrays; - import net.minecraft.world.entity.boss.enderdragon.EnderDragon; - - public class EnderDragonPhase { -- private static EnderDragonPhase[] phases = new EnderDragonPhase[0]; -+ private static EnderDragonPhase[] phases = new EnderDragonPhase[0]; // Plazma - Reduce allocations (mark on diff) - public static final EnderDragonPhase HOLDING_PATTERN = create(DragonHoldingPatternPhase.class, "HoldingPattern"); - public static final EnderDragonPhase STRAFE_PLAYER = create(DragonStrafePlayerPhase.class, "StrafePlayer"); - public static final EnderDragonPhase LANDING_APPROACH = create(DragonLandingApproachPhase.class, "LandingApproach"); diff --git a/src/main/java/net/minecraft/world/entity/boss/wither/WitherBoss.java b/src/main/java/net/minecraft/world/entity/boss/wither/WitherBoss.java -index 4a98027a12c2535d1df3a9f6390eb85146398403..de5154d6e8abf4c8d1e74870470de5df2f8f8b0f 100644 +index 1d896c6c49705acd87416dc11a1d8ce205f7844e..6d5c60cf9851fc358f29d25bf5d0de98ff7aeb40 100644 --- a/src/main/java/net/minecraft/world/entity/boss/wither/WitherBoss.java +++ b/src/main/java/net/minecraft/world/entity/boss/wither/WitherBoss.java -@@ -258,7 +258,7 @@ public class WitherBoss extends Monster implements PowerableMob, RangedAttackMob +@@ -255,7 +255,7 @@ public class WitherBoss extends Monster implements PowerableMob, RangedAttackMob this.goalSelector.addGoal(6, new LookAtPlayerGoal(this, Player.class, 8.0F)); this.goalSelector.addGoal(7, new RandomLookAroundGoal(this)); this.targetSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur @@ -742,10 +720,10 @@ index 4a98027a12c2535d1df3a9f6390eb85146398403..de5154d6e8abf4c8d1e74870470de5df } diff --git a/src/main/java/net/minecraft/world/entity/monster/AbstractSkeleton.java b/src/main/java/net/minecraft/world/entity/monster/AbstractSkeleton.java -index e80307198b051cbcd9f72b36e459276848dcb4c9..7a20324210accd29f495c6c31d3d9eefe6ef0fc0 100644 +index a00646bc8a9caefe56e48b7682e8fb0c464b81fa..a2b5d2f1be493148a0ed102f10fd8b762b8d3981 100644 --- a/src/main/java/net/minecraft/world/entity/monster/AbstractSkeleton.java +++ b/src/main/java/net/minecraft/world/entity/monster/AbstractSkeleton.java -@@ -78,7 +78,7 @@ public abstract class AbstractSkeleton extends Monster implements RangedAttackMo +@@ -79,7 +79,7 @@ public abstract class AbstractSkeleton extends Monster implements RangedAttackMo this.goalSelector.addGoal(6, new LookAtPlayerGoal(this, Player.class, 8.0F)); this.goalSelector.addGoal(6, new RandomLookAroundGoal(this)); this.targetSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur @@ -755,10 +733,10 @@ index e80307198b051cbcd9f72b36e459276848dcb4c9..7a20324210accd29f495c6c31d3d9eef this.targetSelector.addGoal(3, new NearestAttackableTargetGoal<>(this, IronGolem.class, true)); this.targetSelector.addGoal(3, new NearestAttackableTargetGoal<>(this, Turtle.class, 10, true, false, Turtle.BABY_ON_LAND_SELECTOR)); diff --git a/src/main/java/net/minecraft/world/entity/monster/Creeper.java b/src/main/java/net/minecraft/world/entity/monster/Creeper.java -index 1829bedfa8084c4932a0e67c36f48f19993e22b6..a45a113bcb7f8d28300f7d31331972550bd96287 100644 +index 54315fb84e3289f0ad8305c2c2cec980a5b2c627..5adcb3cb643025de833935ad0b962cf557d354aa 100644 --- a/src/main/java/net/minecraft/world/entity/monster/Creeper.java +++ b/src/main/java/net/minecraft/world/entity/monster/Creeper.java -@@ -155,7 +155,7 @@ public class Creeper extends Monster implements PowerableMob { +@@ -154,7 +154,7 @@ public class Creeper extends Monster implements PowerableMob { this.goalSelector.addGoal(6, new RandomLookAroundGoal(this)); this.targetSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur this.targetSelector.addGoal(1, new NearestAttackableTargetGoal<>(this, Player.class, true)); @@ -768,10 +746,10 @@ index 1829bedfa8084c4932a0e67c36f48f19993e22b6..a45a113bcb7f8d28300f7d3133197255 public static AttributeSupplier.Builder createAttributes() { diff --git a/src/main/java/net/minecraft/world/entity/monster/EnderMan.java b/src/main/java/net/minecraft/world/entity/monster/EnderMan.java -index 5b49a6b1884c33bedafca5cff0214cdfccd87302..045efcbb61c0c492f7dac076b0b63f7e9396523e 100644 +index 13bc6389652f8868d7539676ee6d85f37ba78f67..6e4dca9074132fc0fb8c90878bf5028729b72367 100644 --- a/src/main/java/net/minecraft/world/entity/monster/EnderMan.java +++ b/src/main/java/net/minecraft/world/entity/monster/EnderMan.java -@@ -133,7 +133,7 @@ public class EnderMan extends Monster implements NeutralMob { +@@ -138,7 +138,7 @@ public class EnderMan extends Monster implements NeutralMob { this.goalSelector.addGoal(11, new EnderMan.EndermanTakeBlockGoal(this)); this.targetSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur this.targetSelector.addGoal(1, new EnderMan.EndermanLookForPlayerGoal(this, this::isAngryAt)); @@ -780,37 +758,24 @@ index 5b49a6b1884c33bedafca5cff0214cdfccd87302..045efcbb61c0c492f7dac076b0b63f7e this.targetSelector.addGoal(3, new NearestAttackableTargetGoal<>(this, Endermite.class, 10, true, false, (entityliving) -> entityliving.level().purpurConfig.endermanAggroEndermites && entityliving instanceof Endermite endermite && (!entityliving.level().purpurConfig.endermanAggroEndermitesOnlyIfPlayerSpawned || endermite.isPlayerSpawned()))); // Purpur this.targetSelector.addGoal(4, new ResetUniversalAngerTargetGoal<>(this, false)); } -diff --git a/src/main/java/net/minecraft/world/entity/monster/Endermite.java b/src/main/java/net/minecraft/world/entity/monster/Endermite.java -index 14d6796a124a85b8cbf5f3b719d89d99e0cf8db5..61f65018d25f450bf9a8b07559e4ca9e600acdb4 100644 ---- a/src/main/java/net/minecraft/world/entity/monster/Endermite.java -+++ b/src/main/java/net/minecraft/world/entity/monster/Endermite.java -@@ -89,7 +89,7 @@ public class Endermite extends Monster { - this.goalSelector.addGoal(7, new LookAtPlayerGoal(this, Player.class, 8.0F)); - this.goalSelector.addGoal(8, new RandomLookAroundGoal(this)); - this.targetSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur -- this.targetSelector.addGoal(1, (new HurtByTargetGoal(this, new Class[0])).setAlertOthers()); -+ this.targetSelector.addGoal(1, (new HurtByTargetGoal(this/*, new Class[0]*/)).setAlertOthers()); // Plazma - Reduce allocations - this.targetSelector.addGoal(2, new NearestAttackableTargetGoal<>(this, Player.class, true)); - } - diff --git a/src/main/java/net/minecraft/world/entity/monster/Silverfish.java b/src/main/java/net/minecraft/world/entity/monster/Silverfish.java -index c713e0b9b90784ad5f043f3a06ef50b5a1769ed1..f8d2c555ba5d268a6b6788863b352f77ee224c69 100644 +index 931412a5ab315d4080a9f5209d3e85d78642f4c2..0678c916eb6c7fe4a96d6716980892f80e77b3d8 100644 --- a/src/main/java/net/minecraft/world/entity/monster/Silverfish.java +++ b/src/main/java/net/minecraft/world/entity/monster/Silverfish.java -@@ -85,7 +85,7 @@ public class Silverfish extends Monster { +@@ -90,7 +90,7 @@ public class Silverfish extends Monster { this.goalSelector.addGoal(4, new MeleeAttackGoal(this, 1.0D, false)); this.goalSelector.addGoal(5, new Silverfish.SilverfishMergeWithStoneGoal(this)); this.targetSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur - this.targetSelector.addGoal(1, (new HurtByTargetGoal(this, new Class[0])).setAlertOthers()); -+ this.targetSelector.addGoal(1, (new HurtByTargetGoal(this/*, new Class[0]*/)).setAlertOthers()); // Plazma - Reduce allocations ++ this.targetSelector.addGoal(1, (new HurtByTargetGoal(this/*,new Class[0]*/)).setAlertOthers()); // Plazma - Reduce allocations this.targetSelector.addGoal(2, new NearestAttackableTargetGoal<>(this, Player.class, true)); } diff --git a/src/main/java/net/minecraft/world/entity/monster/Spider.java b/src/main/java/net/minecraft/world/entity/monster/Spider.java -index 159740069aba59bffff444d933af32aaf752ba48..e8177af1722f15069f67fd36c9ce150625ba6077 100644 +index b44ffeb4cc0ef63fdd25683f60c5a20fcdeb9135..284aa2340079a4bdf9f1916b28596ea4a06a53ba 100644 --- a/src/main/java/net/minecraft/world/entity/monster/Spider.java +++ b/src/main/java/net/minecraft/world/entity/monster/Spider.java -@@ -96,7 +96,7 @@ public class Spider extends Monster { +@@ -95,7 +95,7 @@ public class Spider extends Monster { this.goalSelector.addGoal(6, new LookAtPlayerGoal(this, Player.class, 8.0F)); this.goalSelector.addGoal(6, new RandomLookAroundGoal(this)); this.targetSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur @@ -820,10 +785,10 @@ index 159740069aba59bffff444d933af32aaf752ba48..e8177af1722f15069f67fd36c9ce1506 this.targetSelector.addGoal(3, new Spider.SpiderTargetGoal<>(this, IronGolem.class)); } diff --git a/src/main/java/net/minecraft/world/entity/monster/Zombie.java b/src/main/java/net/minecraft/world/entity/monster/Zombie.java -index d48d22157a89f98c1bbabc70b0bb31187038176d..f2abb8bc0113ff82bb7938b748a9ab2b3b423f26 100644 +index c531d830f4d6b2d2213e160d7e1a5b50b80dbea5..18f33b417573417d5e3eb06768d9b59362c2dcf1 100644 --- a/src/main/java/net/minecraft/world/entity/monster/Zombie.java +++ b/src/main/java/net/minecraft/world/entity/monster/Zombie.java -@@ -163,7 +163,7 @@ public class Zombie extends Monster { +@@ -166,7 +166,7 @@ public class Zombie extends Monster { this.goalSelector.addGoal(2, new ZombieAttackGoal(this, 1.0D, false)); this.goalSelector.addGoal(6, new MoveThroughVillageGoal(this, 1.0D, true, 4, this::canBreakDoors)); this.goalSelector.addGoal(7, new WaterAvoidingRandomStrollGoal(this, 1.0D)); @@ -833,10 +798,10 @@ index d48d22157a89f98c1bbabc70b0bb31187038176d..f2abb8bc0113ff82bb7938b748a9ab2b // Purpur start if ( this.level().spigotConfig.zombieAggressiveTowardsVillager ) this.targetSelector.addGoal(3, new NearestAttackableTargetGoal<>(this, AbstractVillager.class, false) { // Spigot diff --git a/src/main/java/net/minecraft/world/entity/monster/ZombifiedPiglin.java b/src/main/java/net/minecraft/world/entity/monster/ZombifiedPiglin.java -index 5bae3215ee0bf222c3bd77b3131f3d01ac6c9c41..cae9562468fd8ea96ddee93ba5403be52e99d3aa 100644 +index feba8a264bae656244f60296d0511a8046297f73..37dbf8ab18710fdd889073468c52cc0c6fc3ced2 100644 --- a/src/main/java/net/minecraft/world/entity/monster/ZombifiedPiglin.java +++ b/src/main/java/net/minecraft/world/entity/monster/ZombifiedPiglin.java -@@ -118,7 +118,7 @@ public class ZombifiedPiglin extends Zombie implements NeutralMob { +@@ -120,7 +120,7 @@ public class ZombifiedPiglin extends Zombie implements NeutralMob { protected void addBehaviourGoals() { this.goalSelector.addGoal(2, new ZombieAttackGoal(this, 1.0D, false)); this.goalSelector.addGoal(7, new WaterAvoidingRandomStrollGoal(this, 1.0D)); @@ -845,57 +810,58 @@ index 5bae3215ee0bf222c3bd77b3131f3d01ac6c9c41..cae9562468fd8ea96ddee93ba5403be5 this.targetSelector.addGoal(2, new NearestAttackableTargetGoal<>(this, Player.class, 10, true, false, this::isAngryAt)); this.targetSelector.addGoal(3, new ResetUniversalAngerTargetGoal<>(this, true)); } -diff --git a/src/main/java/net/minecraft/world/inventory/SlotRanges.java b/src/main/java/net/minecraft/world/inventory/SlotRanges.java -index 7700b63b2ad8a7dcf20f5e0dc8e543ea098cddc0..0adda6c14ab119430240025167058436eef9ba3e 100644 ---- a/src/main/java/net/minecraft/world/inventory/SlotRanges.java -+++ b/src/main/java/net/minecraft/world/inventory/SlotRanges.java -@@ -44,8 +44,8 @@ public class SlotRanges { - addSingleSlot(list, "player.cursor", 499); - addSlotRange(list, "player.crafting.", 500, 4); - }); -- public static final Codec CODEC = StringRepresentable.fromValues(() -> SLOTS.toArray(new SlotRange[0])); -- private static final Function NAME_LOOKUP = StringRepresentable.createNameLookup(SLOTS.toArray(new SlotRange[0]), name -> name); -+ public static final Codec CODEC = StringRepresentable.fromValues(() -> SLOTS.toArray(new SlotRange[0])); // Plazma - Reduce allocations (mark on diff) -+ private static final Function NAME_LOOKUP = StringRepresentable.createNameLookup(SLOTS.toArray(new SlotRange[0]), name -> name); // Plazma - Reduce allocations (mark on diff) +diff --git a/src/main/java/net/minecraft/world/item/crafting/ShapelessRecipe.java b/src/main/java/net/minecraft/world/item/crafting/ShapelessRecipe.java +index a097cfc528f709c80575f35483b6878314ea2717..61840d78c4e99e706de56613d13d5741576ad660 100644 +--- a/src/main/java/net/minecraft/world/item/crafting/ShapelessRecipe.java ++++ b/src/main/java/net/minecraft/world/item/crafting/ShapelessRecipe.java +@@ -27,6 +27,7 @@ public class ShapelessRecipe extends io.papermc.paper.inventory.recipe.RecipeBoo + final ItemStack result; + final NonNullList ingredients; + private final boolean isBukkit; // Pufferfish ++ private static final Ingredient[] EMPTY_INGREDIENT = new Ingredient[0]; // Plazma - Reduce allocations - private static SlotRange create(String name, int slotId) { - return SlotRange.of(name, IntLists.singleton(slotId)); -diff --git a/src/main/java/net/minecraft/world/item/crafting/ShapedRecipePattern.java b/src/main/java/net/minecraft/world/item/crafting/ShapedRecipePattern.java -index 90a38babcc87a789e4955f1505fa645780d14011..86d38496c101ef60896a1876f006d37654f0c1fd 100644 ---- a/src/main/java/net/minecraft/world/item/crafting/ShapedRecipePattern.java -+++ b/src/main/java/net/minecraft/world/item/crafting/ShapedRecipePattern.java -@@ -88,7 +88,7 @@ public record ShapedRecipePattern(int width, int height, NonNullList - } - - if (pattern.size() == l) { -- return new String[0]; -+ return org.plazmamc.plazma.constants.Empty.STRING; // Plazma - Reduce allocations - } else { - String[] strings = new String[pattern.size() - l - k]; + // Pufferfish start + public ShapelessRecipe(String group, CraftingBookCategory category, ItemStack result, NonNullList ingredients) { +@@ -84,7 +85,7 @@ public class ShapelessRecipe extends io.papermc.paper.inventory.recipe.RecipeBoo + public boolean matches(CraftingContainer inventory, Level world) { + // Pufferfish start + if (!this.isBukkit) { +- java.util.List ingredients = com.google.common.collect.Lists.newArrayList(this.ingredients.toArray(new Ingredient[0])); ++ java.util.List ingredients = com.google.common.collect.Lists.newArrayList(this.ingredients.toArray(EMPTY_INGREDIENT)); // Plazma - Reduce allocations + inventory: for (int index = 0; index < inventory.getContainerSize(); index++) { + ItemStack itemStack = inventory.getItem(index); diff --git a/src/main/java/net/minecraft/world/level/block/ComposterBlock.java b/src/main/java/net/minecraft/world/level/block/ComposterBlock.java -index f34159f8d6c51af2341bf49db0d6d6f0417919cf..4c48d0c61860701b688c722594645f36cc799fc7 100644 +index 47b7baa41f341087bcd5dfec1d2a13b96f8357ca..72e410ad6bed528b7a1febd30079ab39e10fd5ed 100644 --- a/src/main/java/net/minecraft/world/level/block/ComposterBlock.java +++ b/src/main/java/net/minecraft/world/level/block/ComposterBlock.java -@@ -458,7 +458,7 @@ public class ComposterBlock extends Block implements WorldlyContainerHolder { +@@ -62,6 +62,7 @@ public class ComposterBlock extends Block implements WorldlyContainerHolder { + + avoxelshape[8] = avoxelshape[7]; + }); ++ private static final int[] ZERO_INT = new int[]{0}; // Plazma - Reduce allocations // Plazma - Reduce allocations + + @Override + public MapCodec codec() { +@@ -450,7 +451,7 @@ public class ComposterBlock extends Block implements WorldlyContainerHolder { @Override public int[] getSlotsForFace(Direction side) { - return side == Direction.DOWN ? new int[]{0} : new int[0]; -+ return side == Direction.DOWN ? org.plazmamc.plazma.constants.Empty.ZINT : org.plazmamc.plazma.constants.Empty.INT; // Plazma - Reduce allocations ++ return side == Direction.DOWN ? ZERO_INT : org.plazmamc.plazma.constants.Empty.INT; // Plazma - Reduce allocations } @Override -@@ -507,7 +507,7 @@ public class ComposterBlock extends Block implements WorldlyContainerHolder { +@@ -499,7 +500,7 @@ public class ComposterBlock extends Block implements WorldlyContainerHolder { @Override public int[] getSlotsForFace(Direction side) { - return side == Direction.UP ? new int[]{0} : new int[0]; -+ return side == Direction.UP ? org.plazmamc.plazma.constants.Empty.ZINT : org.plazmamc.plazma.constants.Empty.INT; // Plazma - Reduce allocations ++ return side == Direction.UP ? ZERO_INT : org.plazmamc.plazma.constants.Empty.INT; // Plazma - Reduce allocations } @Override -@@ -549,7 +549,7 @@ public class ComposterBlock extends Block implements WorldlyContainerHolder { +@@ -541,7 +542,7 @@ public class ComposterBlock extends Block implements WorldlyContainerHolder { @Override public int[] getSlotsForFace(Direction side) { @@ -904,33 +870,33 @@ index f34159f8d6c51af2341bf49db0d6d6f0417919cf..4c48d0c61860701b688c722594645f36 } @Override -diff --git a/src/main/java/net/minecraft/world/level/block/entity/AbstractFurnaceBlockEntity.java b/src/main/java/net/minecraft/world/level/block/entity/AbstractFurnaceBlockEntity.java -index c02d638b8f117156c815821207a91c55796f00b2..f76698862e0ec16c89f844d01e8116f12ec696b7 100644 ---- a/src/main/java/net/minecraft/world/level/block/entity/AbstractFurnaceBlockEntity.java -+++ b/src/main/java/net/minecraft/world/level/block/entity/AbstractFurnaceBlockEntity.java -@@ -68,7 +68,7 @@ public abstract class AbstractFurnaceBlockEntity extends BaseContainerBlockEntit - protected static final int SLOT_FUEL = 1; - protected static final int SLOT_RESULT = 2; - public static final int DATA_LIT_TIME = 0; -- private static final int[] SLOTS_FOR_UP = new int[]{0}; -+ private static final int[] SLOTS_FOR_UP = org.plazmamc.plazma.constants.Empty.ZINT; // Plazma - Reduce allocations - private static final int[] SLOTS_FOR_DOWN = new int[]{2, 1}; - private static final int[] SLOTS_FOR_SIDES = new int[]{1}; - public static final int DATA_LIT_DURATION = 1; +diff --git a/src/main/java/net/minecraft/world/level/chunk/ChunkStatus.java b/src/main/java/net/minecraft/world/level/chunk/ChunkStatus.java +index a907b79fd8291a0e92db138f37239d17424188a1..a402db6baa4b24b4c6750b01b8b2f56e213ecf78 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/ChunkStatus.java ++++ b/src/main/java/net/minecraft/world/level/chunk/ChunkStatus.java +@@ -148,7 +148,7 @@ public class ChunkStatus { + }, (chunkstatus, worldserver, structuretemplatemanager, lightenginethreaded, function, ichunkaccess) -> { + return (CompletableFuture) function.apply(ichunkaccess); + }); +- private static final List STATUS_BY_RANGE = ImmutableList.of(ChunkStatus.FULL, ChunkStatus.INITIALIZE_LIGHT, ChunkStatus.CARVERS, ChunkStatus.BIOMES, ChunkStatus.STRUCTURE_STARTS, ChunkStatus.STRUCTURE_STARTS, ChunkStatus.STRUCTURE_STARTS, ChunkStatus.STRUCTURE_STARTS, ChunkStatus.STRUCTURE_STARTS, ChunkStatus.STRUCTURE_STARTS, ChunkStatus.STRUCTURE_STARTS, ChunkStatus.STRUCTURE_STARTS, new ChunkStatus[0]); ++ private static final List STATUS_BY_RANGE = ImmutableList.of(ChunkStatus.FULL, ChunkStatus.INITIALIZE_LIGHT, ChunkStatus.CARVERS, ChunkStatus.BIOMES, ChunkStatus.STRUCTURE_STARTS, ChunkStatus.STRUCTURE_STARTS, ChunkStatus.STRUCTURE_STARTS, ChunkStatus.STRUCTURE_STARTS, ChunkStatus.STRUCTURE_STARTS, ChunkStatus.STRUCTURE_STARTS, ChunkStatus.STRUCTURE_STARTS, ChunkStatus.STRUCTURE_STARTS/*, new ChunkStatus[0]*/); // Plazma - Reduce allocations + private static final IntList RANGE_BY_STATUS = (IntList) Util.make(new IntArrayList(ChunkStatus.getStatusList().size()), (intarraylist) -> { + int i = 0; + diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFile.java b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFile.java -index 1362a47943cf1a51a185a15094b1f74c94bf40ef..2490b63cbba49fa0de076f74266866345c01fbca 100644 +index 6cf83502a954cce9c562ec036bfeddb477d38b73..d28bffce82d0b40c31f240c016621ac06bbad178 100644 --- a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFile.java +++ b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFile.java -@@ -445,7 +445,7 @@ public class RegionFile implements AutoCloseable { - this.path = path; +@@ -444,7 +444,7 @@ public class RegionFile implements AutoCloseable { initOversizedState(); // Paper - this.version = compressionFormat; + this.usedSectors = new RegionBitmap(); + this.version = outputChunkStreamVersion; - if (!Files.isDirectory(directory, new LinkOption[0])) { + if (!Files.isDirectory(directory/*, new LinkOption[0]*/)) { // Plazma - Reduce allocations - throw new IllegalArgumentException("Expected directory, got " + String.valueOf(directory.toAbsolutePath())); + throw new IllegalArgumentException("Expected directory, got " + directory.toAbsolutePath()); } else { this.externalFileDir = directory; -@@ -717,7 +717,7 @@ public class RegionFile implements AutoCloseable { +@@ -700,7 +700,7 @@ public class RegionFile implements AutoCloseable { private DataInputStream createExternalChunkInputStream(ChunkPos pos, byte flags) throws IOException { Path path = this.getExternalChunkPath(pos); @@ -939,7 +905,7 @@ index 1362a47943cf1a51a185a15094b1f74c94bf40ef..2490b63cbba49fa0de076f7426686634 RegionFile.LOGGER.error("External chunk path {} is not file", path); return null; } else { -@@ -769,7 +769,7 @@ public class RegionFile implements AutoCloseable { +@@ -752,7 +752,7 @@ public class RegionFile implements AutoCloseable { return false; } @@ -949,7 +915,7 @@ index 1362a47943cf1a51a185a15094b1f74c94bf40ef..2490b63cbba49fa0de076f7426686634 } } else { diff --git a/src/main/java/net/minecraft/world/level/levelgen/DebugLevelSource.java b/src/main/java/net/minecraft/world/level/levelgen/DebugLevelSource.java -index b8e333e79c2d9a1418d86df9f553fa8a76ce852d..b6012d36099132cb6ab381e2bfa96eccef49d142 100644 +index bf74c054e800104cd47208a29594d3c1340fd668..0320662df9bb039094ecd2bccc94dd59bcdd9d1d 100644 --- a/src/main/java/net/minecraft/world/level/levelgen/DebugLevelSource.java +++ b/src/main/java/net/minecraft/world/level/levelgen/DebugLevelSource.java @@ -89,7 +89,7 @@ public class DebugLevelSource extends ChunkGenerator { @@ -962,10 +928,10 @@ index b8e333e79c2d9a1418d86df9f553fa8a76ce852d..b6012d36099132cb6ab381e2bfa96ecc @Override diff --git a/src/main/java/net/minecraft/world/level/storage/LevelStorageSource.java b/src/main/java/net/minecraft/world/level/storage/LevelStorageSource.java -index 427ee4d6f12a7abd8da0c65e0b9081b25824df40..2655ad947d3e6bc1b06911bc7309e14991c04467 100644 +index 399da9d43aefbb95897df4697860d5bce5317152..df7816c1c5f069d89a22dbd876a2d663ba68949f 100644 --- a/src/main/java/net/minecraft/world/level/storage/LevelStorageSource.java +++ b/src/main/java/net/minecraft/world/level/storage/LevelStorageSource.java -@@ -107,7 +107,7 @@ public class LevelStorageSource { +@@ -110,7 +110,7 @@ public class LevelStorageSource { } public static DirectoryValidator parseValidator(Path allowedSymlinksFile) { @@ -974,7 +940,7 @@ index 427ee4d6f12a7abd8da0c65e0b9081b25824df40..2655ad947d3e6bc1b06911bc7309e149 try { BufferedReader bufferedreader = Files.newBufferedReader(allowedSymlinksFile); -@@ -176,7 +176,7 @@ public class LevelStorageSource { +@@ -189,7 +189,7 @@ public class LevelStorageSource { } public LevelStorageSource.LevelCandidates findLevelCandidates() throws LevelStorageException { @@ -983,7 +949,7 @@ index 427ee4d6f12a7abd8da0c65e0b9081b25824df40..2655ad947d3e6bc1b06911bc7309e149 throw new LevelStorageException(Component.translatable("selectWorld.load_folder_access")); } else { try { -@@ -185,11 +185,12 @@ public class LevelStorageSource { +@@ -198,11 +198,12 @@ public class LevelStorageSource { LevelStorageSource.LevelCandidates convertable_a; try { @@ -1001,7 +967,7 @@ index 427ee4d6f12a7abd8da0c65e0b9081b25824df40..2655ad947d3e6bc1b06911bc7309e149 convertable_a = new LevelStorageSource.LevelCandidates(list); } catch (Throwable throwable) { -@@ -301,7 +302,7 @@ public class LevelStorageSource { +@@ -312,7 +313,7 @@ public class LevelStorageSource { private LevelSummary readLevelSummary(LevelStorageSource.LevelDirectory save, boolean locked) { Path path = save.dataFile(); @@ -1010,7 +976,7 @@ index 427ee4d6f12a7abd8da0c65e0b9081b25824df40..2655ad947d3e6bc1b06911bc7309e149 try { if (Files.isSymbolicLink(path)) { List list = this.worldDirValidator.validateSymlink(path); -@@ -400,7 +401,7 @@ public class LevelStorageSource { +@@ -411,7 +412,7 @@ public class LevelStorageSource { public boolean levelExists(String name) { try { @@ -1019,7 +985,7 @@ index 427ee4d6f12a7abd8da0c65e0b9081b25824df40..2655ad947d3e6bc1b06911bc7309e149 } catch (InvalidPathException invalidpathexception) { return false; } -@@ -753,7 +754,7 @@ public class LevelStorageSource { +@@ -752,7 +753,7 @@ public class LevelStorageSource { } public boolean hasWorldData() { @@ -1029,20 +995,20 @@ index 427ee4d6f12a7abd8da0c65e0b9081b25824df40..2655ad947d3e6bc1b06911bc7309e149 public void close() throws IOException { diff --git a/src/main/java/net/minecraft/world/level/storage/PlayerDataStorage.java b/src/main/java/net/minecraft/world/level/storage/PlayerDataStorage.java -index 8ab7ca373a885fbe658013c9c6a2e38d32d77bb2..c6bcfd61b0a848129357d9515de96c8e899c25c7 100644 +index 63e187c65cb855031f286aad0d25ac4694f7a331..e0f085169fafa5e574caf368efa343514540b348 100644 --- a/src/main/java/net/minecraft/world/level/storage/PlayerDataStorage.java +++ b/src/main/java/net/minecraft/world/level/storage/PlayerDataStorage.java -@@ -60,7 +60,7 @@ public class PlayerDataStorage { - // s1 = entityhuman.getStringUUID(); // CraftBukkit - used above - Path path2 = path.resolve(s1 + "_corrupted_" + LocalDateTime.now().format(PlayerDataStorage.FORMATTER) + s); +@@ -124,7 +124,7 @@ public class PlayerDataStorage { + String[] astring = this.playerDir.list(); -- if (Files.isRegularFile(path1, new LinkOption[0])) { -+ if (Files.isRegularFile(path1/*, new LinkOption[0]*/)) { // Plazma - Reduce allocations - try { - Files.copy(path1, path2, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.COPY_ATTRIBUTES); - } catch (Exception exception) { + if (astring == null) { +- astring = new String[0]; ++ astring = org.plazmamc.plazma.constants.Empty.STRING; // Plazma - Reduce allocations + } + + for (int i = 0; i < astring.length; ++i) { diff --git a/src/main/java/net/minecraft/world/scores/Team.java b/src/main/java/net/minecraft/world/scores/Team.java -index b968d22e149bf9063f14167fe9856868e5933303..cec115cf5e4ad17240497c9cebb3981564a537bc 100644 +index f00791b89fdb1bb0fb358eff2af2e687bda15e85..aa16c93a4333b603729e2704b92f6d38aec0fd7b 100644 --- a/src/main/java/net/minecraft/world/scores/Team.java +++ b/src/main/java/net/minecraft/world/scores/Team.java @@ -70,7 +70,7 @@ public abstract class Team { @@ -1054,25 +1020,12 @@ index b968d22e149bf9063f14167fe9856868e5933303..cec115cf5e4ad17240497c9cebb39815 } @Nullable -diff --git a/src/main/java/org/bukkit/craftbukkit/bootstrap/Main.java b/src/main/java/org/bukkit/craftbukkit/bootstrap/Main.java -index 8a4f95049c63afb28bef6719c77b7a7092e75aae..db6b7cf7f5ad7e991d682eafd353480e9b14faa9 100644 ---- a/src/main/java/org/bukkit/craftbukkit/bootstrap/Main.java -+++ b/src/main/java/org/bukkit/craftbukkit/bootstrap/Main.java -@@ -50,7 +50,7 @@ public class Main { - System.exit(0); - } - -- URLClassLoader classLoader = new URLClassLoader(extractedUrls.toArray(new URL[0])); -+ URLClassLoader classLoader = new URLClassLoader(extractedUrls.toArray(new URL[0])); // Plazma - Reduce allocations (mark on diff) - - System.out.println("Starting server"); - Thread runThread = new Thread(() -> { diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java -index 90338017ebcb2a690dff7dad57aa6fbb95e0ff93..abfa1c2172220a16294cf52e78fb6638ec01ea9c 100644 +index fb2d05e43df3bfb72b1f6e325736dd3cbc6c3096..de4e1d713e5ec182ea8d4832b8359556095ac214 100644 --- a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java +++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java -@@ -502,7 +502,7 @@ public class CraftPlayer extends CraftHumanEntity implements Player { - public void sendTitle(com.destroystokyo.paper.Title title) { +@@ -426,7 +426,7 @@ public class CraftPlayer extends CraftHumanEntity implements Player { + public void sendTitle(Title title) { Preconditions.checkNotNull(title, "Title is null"); setTitleTimes(title.getFadeIn(), title.getStay(), title.getFadeOut()); - setSubtitle(title.getSubtitle() == null ? new BaseComponent[0] : title.getSubtitle()); @@ -1081,10 +1034,10 @@ index 90338017ebcb2a690dff7dad57aa6fbb95e0ff93..abfa1c2172220a16294cf52e78fb6638 } diff --git a/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaBook.java b/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaBook.java -index a395c7ce952f4a60a5edf80e8731afa6388d18ea..86d6c952682e10bf961c5752553a707610434679 100644 +index c5d1ba7a1be3f102edcdfdc05fc50b30ef1f775b..66006d9ff581ac96f13389261de0cd647279b98a 100644 --- a/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaBook.java +++ b/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaBook.java -@@ -448,7 +448,7 @@ public class CraftMetaBook extends CraftMetaItem implements BookMeta, WritableBo +@@ -610,7 +610,7 @@ public class CraftMetaBook extends CraftMetaItem implements BookMeta { throw new IllegalArgumentException("Invalid page number " + page + "/" + CraftMetaBook.this.getPageCount()); } @@ -1093,7 +1046,7 @@ index a395c7ce952f4a60a5edf80e8731afa6388d18ea..86d6c952682e10bf961c5752553a7076 CraftMetaBook.this.pages.set(page - 1, this.componentsToPage(newText)); } -@@ -461,7 +461,7 @@ public class CraftMetaBook extends CraftMetaItem implements BookMeta, WritableBo +@@ -623,7 +623,7 @@ public class CraftMetaBook extends CraftMetaItem implements BookMeta { public void addPage(final BaseComponent[]... pages) { for (BaseComponent[] page : pages) { if (page == null) { @@ -1102,28 +1055,6 @@ index a395c7ce952f4a60a5edf80e8731afa6388d18ea..86d6c952682e10bf961c5752553a7076 } CraftMetaBook.this.internalAddPage(this.componentsToPage(page)); -diff --git a/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaBookSigned.java b/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaBookSigned.java -index 6a6e9a1478a2ead20467bc711d0ad4a9ab3010cb..54d980117255f3b4c95adde4d253eb0775c6f1f5 100644 ---- a/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaBookSigned.java -+++ b/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaBookSigned.java -@@ -399,7 +399,7 @@ public class CraftMetaBookSigned extends CraftMetaItem implements BookMeta { - throw new IllegalArgumentException("Invalid page number " + page + "/" + CraftMetaBookSigned.this.getPageCount()); - } - -- BaseComponent[] newText = text == null ? new BaseComponent[0] : text; -+ BaseComponent[] newText = text == null ? org.plazmamc.plazma.constants.Empty.BASE_COMPONENT : text; // Plazma - Reduce allocations - CraftMetaBookSigned.this.pages.set(page - 1, this.componentsToPage(newText)); - } - -@@ -412,7 +412,7 @@ public class CraftMetaBookSigned extends CraftMetaItem implements BookMeta { - public void addPage(final BaseComponent[]... pages) { - for (BaseComponent[] page : pages) { - if (page == null) { -- page = new BaseComponent[0]; -+ page = org.plazmamc.plazma.constants.Empty.BASE_COMPONENT; // Plazma - Reduce allocations - } - - CraftMetaBookSigned.this.internalAddPage(this.componentsToPage(page)); diff --git a/src/main/java/org/bukkit/craftbukkit/util/WeakCollection.java b/src/main/java/org/bukkit/craftbukkit/util/WeakCollection.java index b25dc23b81687dd4d4e70b3615ffb91f8c03c68b..acb98770b9d01e930642a5794f9179660b411c02 100644 --- a/src/main/java/org/bukkit/craftbukkit/util/WeakCollection.java @@ -1139,16 +1070,15 @@ index b25dc23b81687dd4d4e70b3615ffb91f8c03c68b..acb98770b9d01e930642a5794f917966 @Override diff --git a/src/main/java/org/plazmamc/plazma/constants/Empty.java b/src/main/java/org/plazmamc/plazma/constants/Empty.java new file mode 100644 -index 0000000000000000000000000000000000000000..8e4c5d912113ebec0272b09f8e01117536eac1e3 +index 0000000000000000000000000000000000000000..2b6eb7997986ab73ccb3b1baca945a3234ae916e --- /dev/null +++ b/src/main/java/org/plazmamc/plazma/constants/Empty.java -@@ -0,0 +1,21 @@ +@@ -0,0 +1,20 @@ +package org.plazmamc.plazma.constants; + +public interface Empty { + + int[] INT = new int[0]; -+ int[] ZINT = new int[]{0}; + long[] LONG = new long[0]; + byte[] BYTE = new byte[0]; + diff --git a/patches/server/0006-Plazma-Configurations.patch b/patches/server/0007-Plazma-Configurations.patch similarity index 88% rename from patches/server/0006-Plazma-Configurations.patch rename to patches/server/0007-Plazma-Configurations.patch index 0c7d4a3..ee59dd7 100644 --- a/patches/server/0006-Plazma-Configurations.patch +++ b/patches/server/0007-Plazma-Configurations.patch @@ -5,10 +5,10 @@ Subject: [PATCH] Plazma Configurations diff --git a/src/main/java/io/papermc/paper/configuration/Configurations.java b/src/main/java/io/papermc/paper/configuration/Configurations.java -index 96142deb42700f888ea08689ab62c27ef2b881fd..3ac80b85ba7c4a4e0b0b4aa06fa92f6e4e376c28 100644 +index 218bf89fd7583d6db9f64754c4db8fcce5415bdb..bd2234f883faa50015289315fa745e6556d3f094 100644 --- a/src/main/java/io/papermc/paper/configuration/Configurations.java +++ b/src/main/java/io/papermc/paper/configuration/Configurations.java -@@ -39,28 +39,94 @@ public abstract class Configurations { +@@ -36,28 +36,93 @@ public abstract class Configurations { public static final String WORLD_DEFAULTS = "__world_defaults__"; public static final ResourceLocation WORLD_DEFAULTS_KEY = new ResourceLocation("configurations", WORLD_DEFAULTS); protected final Path globalFolder; @@ -26,7 +26,6 @@ index 96142deb42700f888ea08689ab62c27ef2b881fd..3ac80b85ba7c4a4e0b0b4aa06fa92f6e - final String defaultWorldConfigFileName, - final String worldConfigFileName - ) { -+ + // Plazma start - Configurable Plazma + @org.jetbrains.annotations.VisibleForTesting + public static final java.util.function.Supplier SPIGOT_WORLD_DEFAULTS = com.google.common.base.Suppliers.memoize(() -> new org.spigotmc.SpigotWorldConfig(org.apache.commons.lang.RandomStringUtils.randomAlphabetic(255)) { @@ -122,7 +121,7 @@ index 96142deb42700f888ea08689ab62c27ef2b881fd..3ac80b85ba7c4a4e0b0b4aa06fa92f6e protected ObjectMapper.Factory.Builder createObjectMapper() { return ObjectMapper.factoryBuilder() .addConstraint(Constraint.class, new Constraint.Factory()) -@@ -68,17 +134,21 @@ public abstract class Configurations { +@@ -65,17 +130,21 @@ public abstract class Configurations { } protected YamlConfigurationLoader.Builder createLoaderBuilder() { @@ -147,16 +146,16 @@ index 96142deb42700f888ea08689ab62c27ef2b881fd..3ac80b85ba7c4a4e0b0b4aa06fa92f6e } @MustBeInvokedByOverriders -@@ -96,7 +166,7 @@ public abstract class Configurations { +@@ -93,7 +162,7 @@ public abstract class Configurations { }; } - static CheckedFunction reloader(Class type, T instance) { -+ protected static CheckedFunction reloader(Class type, T instance) { // Plazma - AT (package -> protected) ++ protected static CheckedFunction reloader(Class type, T instance) { // Plazma - package -> protected return node -> { ObjectMapper.Factory factory = (ObjectMapper.Factory) Objects.requireNonNull(node.options().serializers().get(type)); ObjectMapper.Mutable mutable = (ObjectMapper.Mutable) factory.get(type); -@@ -106,7 +176,7 @@ public abstract class Configurations { +@@ -103,7 +172,7 @@ public abstract class Configurations { } public G initializeGlobalConfiguration(final RegistryAccess registryAccess) throws ConfigurateException { @@ -165,7 +164,7 @@ index 96142deb42700f888ea08689ab62c27ef2b881fd..3ac80b85ba7c4a4e0b0b4aa06fa92f6e } private void trySaveFileNode(YamlConfigurationLoader loader, ConfigurationNode node, String filename) throws ConfigurateException { -@@ -120,7 +190,7 @@ public abstract class Configurations { +@@ -117,7 +186,7 @@ public abstract class Configurations { } protected G initializeGlobalConfiguration(final CheckedFunction creator) throws ConfigurateException { @@ -174,7 +173,7 @@ index 96142deb42700f888ea08689ab62c27ef2b881fd..3ac80b85ba7c4a4e0b0b4aa06fa92f6e final YamlConfigurationLoader loader = this.createGlobalLoaderBuilder() .defaultOptions(this.applyObjectMapperFactory(this.createGlobalObjectMapperFactoryBuilder().build())) .path(configFile) -@@ -151,6 +221,13 @@ public abstract class Configurations { +@@ -148,6 +217,13 @@ public abstract class Configurations { } protected void applyGlobalConfigTransformations(final ConfigurationNode node) throws ConfigurateException { @@ -188,7 +187,7 @@ index 96142deb42700f888ea08689ab62c27ef2b881fd..3ac80b85ba7c4a4e0b0b4aa06fa92f6e } @MustBeInvokedByOverriders -@@ -158,6 +235,7 @@ public abstract class Configurations { +@@ -155,6 +231,7 @@ public abstract class Configurations { return ContextMap.builder() .put(WORLD_NAME, WORLD_DEFAULTS) .put(WORLD_KEY, WORLD_DEFAULTS_KEY) @@ -196,7 +195,7 @@ index 96142deb42700f888ea08689ab62c27ef2b881fd..3ac80b85ba7c4a4e0b0b4aa06fa92f6e .put(REGISTRY_ACCESS, registryAccess); } -@@ -165,7 +243,7 @@ public abstract class Configurations { +@@ -162,7 +239,7 @@ public abstract class Configurations { final ContextMap contextMap = this.createDefaultContextMap(registryAccess) .put(FIRST_DEFAULT) .build(); @@ -205,18 +204,18 @@ index 96142deb42700f888ea08689ab62c27ef2b881fd..3ac80b85ba7c4a4e0b0b4aa06fa92f6e final DefaultWorldLoader result = this.createDefaultWorldLoader(false, contextMap, configFile); final YamlConfigurationLoader loader = result.loader(); final ConfigurationNode node = loader.load(); -@@ -175,8 +253,8 @@ public abstract class Configurations { +@@ -172,8 +249,8 @@ public abstract class Configurations { this.verifyWorldConfigVersion(contextMap, node); } - this.applyWorldConfigTransformations(contextMap, node, null); + this.applyWorldConfigTransformations(contextMap, node); - final W instance = node.require(this.worldConfigClass); - node.set(this.worldConfigClass, instance); + final W instance = node.require(this.worldConfigClass()); // Plazma - Configurable Plazma + node.set(this.worldConfigClass(), instance); // Plazma - Configurable Plazma - this.trySaveFileNode(loader, node, configFile.toString()); + trySaveFileNode(loader, node, configFile.toString()); } -@@ -197,31 +275,42 @@ public abstract class Configurations { +@@ -194,30 +271,41 @@ public abstract class Configurations { private record DefaultWorldLoader(YamlConfigurationLoader loader, boolean isNewFile) { } @@ -237,8 +236,9 @@ index 96142deb42700f888ea08689ab62c27ef2b881fd..3ac80b85ba7c4a4e0b0b4aa06fa92f6e } // Make sure to run version transforms on the default world config first via #setupWorldDefaultsConfig - public W createWorldConfig(final ContextMap contextMap) throws IOException { +- public W createWorldConfig(final ContextMap contextMap) throws IOException { - return this.createWorldConfig(contextMap, creator(this.worldConfigClass, false)); ++ public W createWorldConfig(final ContextMap contextMap) { + final String levelName = contextMap.require(WORLD_NAME); + try { + return this.createWorldConfig(contextMap, creator(this.worldConfigClass(), false)); @@ -249,7 +249,6 @@ index 96142deb42700f888ea08689ab62c27ef2b881fd..3ac80b85ba7c4a4e0b0b4aa06fa92f6e + // Plazma end - Configurable Plazma protected W createWorldConfig(final ContextMap contextMap, final CheckedFunction creator) throws IOException { - Preconditions.checkArgument(!contextMap.isDefaultWorldContext(), "cannot create world map with default world context"); - final Path defaultsConfigFile = this.globalFolder.resolve(this.defaultWorldConfigFileName); + final Path defaultsConfigFile = this.globalFolder.resolve(this.defaultWorldConfigFileName()); // Plazma - Configurable Plazma final YamlConfigurationLoader defaultsLoader = this.createDefaultWorldLoader(true, this.createDefaultContextMap(contextMap.require(REGISTRY_ACCESS)).build(), defaultsConfigFile).loader(); @@ -265,19 +264,19 @@ index 96142deb42700f888ea08689ab62c27ef2b881fd..3ac80b85ba7c4a4e0b0b4aa06fa92f6e Files.createFile(worldConfigFile); // create empty file as template newFile = true; } -@@ -250,7 +339,7 @@ public abstract class Configurations { +@@ -246,7 +334,7 @@ public abstract class Configurations { if (worldName.equals(WORLD_DEFAULTS)) { LOGGER.warn("The world defaults config file didn't have a version set, assuming latest"); } else { - LOGGER.warn("The world config file for " + worldName + " didn't have a version set, assuming latest"); -+ LOGGER.warn("The world config file for {} didn't have a version set, assuming latest", worldName); // Plazma - nah ++ LOGGER.warn("The world config file for {} didn't have a version set, assuming latest", worldName); // Plazma } version.raw(this.worldConfigVersion()); } else if (version.getInt() > this.worldConfigVersion()) { -@@ -265,6 +354,13 @@ public abstract class Configurations { +@@ -261,6 +349,13 @@ public abstract class Configurations { } - protected void applyWorldConfigTransformations(final ContextMap contextMap, final ConfigurationNode node, final @Nullable ConfigurationNode defaultsNode) throws ConfigurateException { + protected void applyWorldConfigTransformations(final ContextMap contextMap, final ConfigurationNode node) throws ConfigurateException { + // Plazma start - Configurable Plazma + org.spongepowered.configurate.transformation.ConfigurationTransformation.Builder builder = org.spongepowered.configurate.transformation.ConfigurationTransformation.builder(); + for (org.spongepowered.configurate.NodePath path : removedWorldPaths()) { @@ -288,7 +287,7 @@ index 96142deb42700f888ea08689ab62c27ef2b881fd..3ac80b85ba7c4a4e0b0b4aa06fa92f6e } protected void applyDefaultsAwareWorldConfigTransformations(final ContextMap contextMap, final ConfigurationNode worldNode, final ConfigurationNode defaultsNode) throws ConfigurateException { -@@ -277,7 +373,7 @@ public abstract class Configurations { +@@ -273,7 +368,7 @@ public abstract class Configurations { } public Path getWorldConfigFile(ServerLevel level) { @@ -298,30 +297,30 @@ index 96142deb42700f888ea08689ab62c27ef2b881fd..3ac80b85ba7c4a4e0b0b4aa06fa92f6e public static class ContextMap { diff --git a/src/main/java/io/papermc/paper/configuration/PaperConfigurations.java b/src/main/java/io/papermc/paper/configuration/PaperConfigurations.java -index 83a726bcf8b7dce73a361b0d79dbd63a0afc7a12..27a2914fabdc2c5ac70e402a41f5c9bb7bfe3c51 100644 +index fa1c0aee8c3a4d0868482cf5c703bbfd08e09874..4a444ac90d357e1b5cc432bccad958054b8a8665 100644 --- a/src/main/java/io/papermc/paper/configuration/PaperConfigurations.java +++ b/src/main/java/io/papermc/paper/configuration/PaperConfigurations.java -@@ -134,6 +134,8 @@ public class PaperConfigurations extends Configurations SPIGOT_WORLD_DEFAULTS = Suppliers.memoize(() -> new SpigotWorldConfig(RandomStringUtils.randomAlphabetic(255)) { @Override // override to ensure "verbose" is false -@@ -142,12 +144,69 @@ public class PaperConfigurations extends Configurations> SPIGOT_WORLD_CONFIG_CONTEXT_KEY = new ContextKey<>(new TypeToken>() {}, "spigot world config"); +- ++ // Plazma start - Configurable Plazma + */ - public PaperConfigurations(final Path globalFolder) { - super(globalFolder, GlobalConfiguration.class, WorldConfiguration.class, GLOBAL_CONFIG_FILE_NAME, WORLD_DEFAULTS_CONFIG_FILE_NAME, WORLD_CONFIG_FILE_NAME); + super(globalFolder); - } - ++ } ++ + @Override + protected Class globalConfigClass() { + return GlobalConfiguration.class; @@ -335,8 +334,8 @@ index 83a726bcf8b7dce73a361b0d79dbd63a0afc7a12..27a2914fabdc2c5ac70e402a41f5c9bb + @Override + protected NodePath[] removedGlobalPaths() { + return RemovedConfigurations.REMOVED_GLOBAL_PATHS; -+ } -+ + } + + @Override + protected GlobalConfiguration getGlobalConfiguration() { + return GlobalConfiguration.get(); @@ -381,7 +380,7 @@ index 83a726bcf8b7dce73a361b0d79dbd63a0afc7a12..27a2914fabdc2c5ac70e402a41f5c9bb @Override protected int globalConfigVersion() { return GlobalConfiguration.CURRENT_VERSION; -@@ -158,11 +217,13 @@ public class PaperConfigurations extends Configurations builder -@@ -203,6 +264,7 @@ public class PaperConfigurations extends Configurations protected) ++ @Override ++ protected Object createWorldConfigInstance(ContextMap contextMap) { ++ // Plazma end - Configurable Plazma return new WorldConfiguration( contextMap.require(PaperConfigurations.SPIGOT_WORLD_CONFIG_CONTEXT_KEY).get(), contextMap.require(Configurations.WORLD_KEY) -@@ -229,7 +293,7 @@ public class PaperConfigurations extends Configurations options - .header(contextMap.require(WORLD_NAME).equals(WORLD_DEFAULTS) ? WORLD_DEFAULTS_HEADER : WORLD_HEADER.apply(contextMap)) -+ // .header(contextMap.require(WORLD_NAME).equals(WORLD_DEFAULTS) ? WORLD_DEFAULTS_HEADER : WORLD_HEADER.apply(contextMap)) // Plazma - Configurable Plazma .serializers(serializers -> serializers .register(new TypeToken>() {}, new FastutilMapSerializer.SomethingToPrimitive>(Reference2IntOpenHashMap::new, Integer.TYPE)) .register(new TypeToken>() {}, new FastutilMapSerializer.SomethingToPrimitive>(Reference2LongOpenHashMap::new, Long.TYPE)) -@@ -254,11 +318,7 @@ public class PaperConfigurations extends Configurations minecraftserver.paperConfigurations.createWorldConfig(io.papermc.paper.configuration.PaperConfigurations.createWorldContextMap(convertable_conversionsession.levelDirectory.path(), iworlddataserver.getLevelName(), resourcekey.location(), spigotConfig, minecraftserver.registryAccess(), iworlddataserver.getGameRules())), executor); // Paper - create paper world configs; Async-Anti-Xray: Pass executor -+ super(iworlddataserver, resourcekey, minecraftserver.registryAccess(), worlddimension.type(), minecraftserver::getProfiler, false, flag, i, minecraftserver.getMaxChainedNeighborUpdates(), gen, biomeProvider, env, spigotConfig -> minecraftserver.paperConfigurations.createWorldConfig(io.papermc.paper.configuration.PaperConfigurations.createWorldContextMap(convertable_conversionsession.levelDirectory.path(), iworlddataserver.getLevelName(), resourcekey.location(), spigotConfig, minecraftserver.registryAccess(), iworlddataserver.getGameRules())), spigotConfig -> minecraftserver.plazmaConfigurations.createWorldConfig(org.plazmamc.plazma.configurations.PlazmaConfigurations.createWorldContextMap(convertable_conversionsession.levelDirectory.path(), iworlddataserver.getLevelName(), resourcekey.location(), spigotConfig, minecraftserver.registryAccess(), iworlddataserver.getGameRules())), executor); // Paper - create paper world configs; Async-Anti-Xray: Pass executor // Plazma - Configurable Plazma +- super(iworlddataserver, resourcekey, minecraftserver.registryAccess(), worlddimension.type(), minecraftserver::getProfiler, false, flag, i, minecraftserver.getMaxChainedNeighborUpdates(), gen, biomeProvider, env, spigotConfig -> minecraftserver.paperConfigurations.createWorldConfig(io.papermc.paper.configuration.PaperConfigurations.createWorldContextMap(convertable_conversionsession.levelDirectory.path(), iworlddataserver.getLevelName(), resourcekey.location(), spigotConfig, minecraftserver.registryAccess())), executor); // Paper - create paper world configs; Async-Anti-Xray: Pass executor ++ super(iworlddataserver, resourcekey, minecraftserver.registryAccess(), worlddimension.type(), minecraftserver::getProfiler, false, flag, i, minecraftserver.getMaxChainedNeighborUpdates(), gen, biomeProvider, env, spigotConfig -> minecraftserver.paperConfigurations.createWorldConfig(io.papermc.paper.configuration.PaperConfigurations.createWorldContextMap(convertable_conversionsession.levelDirectory.path(), iworlddataserver.getLevelName(), resourcekey.location(), spigotConfig, minecraftserver.registryAccess())), spigotConfig -> minecraftserver.plazmaConfigurations.createWorldConfig(org.plazmamc.plazma.configurations.PlazmaConfigurations.createWorldContextMap(convertable_conversionsession.levelDirectory.path(), iworlddataserver.getLevelName(), resourcekey.location(), spigotConfig, minecraftserver.registryAccess())), executor); // Paper - create paper world configs; Async-Anti-Xray: Pass executor // Plazma - Configurable Plazma this.pvpMode = minecraftserver.isPvpAllowed(); this.convertable = convertable_conversionsession; this.uuid = WorldUUID.getUUID(convertable_conversionsession.levelDirectory.path().toFile()); +diff --git a/src/main/java/net/minecraft/world/item/ItemStack.java b/src/main/java/net/minecraft/world/item/ItemStack.java +index f6664447c45b1d6f3371af7bed8b1175b17f25e2..9161f020d2ecc0cb9191666cfbc6a877d067063e 100644 +--- a/src/main/java/net/minecraft/world/item/ItemStack.java ++++ b/src/main/java/net/minecraft/world/item/ItemStack.java +@@ -271,7 +271,7 @@ public final class ItemStack { + if (0 < version && version < CraftMagicNumbers.INSTANCE.getDataVersion() && MinecraftServer.getServer() != null) { // Paper - skip conversion if the server doesn't exist (for tests) + CompoundTag savedStack = new CompoundTag(); + this.save(savedStack); +- savedStack = (CompoundTag) MinecraftServer.getServer().fixerUpper.update(References.ITEM_STACK, new Dynamic(NbtOps.INSTANCE, savedStack), version, CraftMagicNumbers.INSTANCE.getDataVersion()).getValue(); ++ savedStack = (CompoundTag) MinecraftServer.getServer().getFixerUpper().update(References.ITEM_STACK, new Dynamic(NbtOps.INSTANCE, savedStack), version, CraftMagicNumbers.INSTANCE.getDataVersion()).getValue(); // Plazma - Configurable Plazma + this.load(savedStack); + } + } diff --git a/src/main/java/net/minecraft/world/level/Level.java b/src/main/java/net/minecraft/world/level/Level.java -index eda2f8cc034cf46293be1be117a60cf8b663c303..79ebec0543f730af403240e9c1c011777464a698 100644 +index 311c853f2150247350ab6ccb2dd92d58dbfc645c..7578b6d4ee52ebafea16b7eaf88dcedbd1f093d8 100644 --- a/src/main/java/net/minecraft/world/level/Level.java +++ b/src/main/java/net/minecraft/world/level/Level.java -@@ -168,6 +168,12 @@ public abstract class Level implements LevelAccessor, AutoCloseable { +@@ -171,7 +171,12 @@ public abstract class Level implements LevelAccessor, AutoCloseable { return this.paperConfig; } // Paper end - add paper world config +- + // Plazma start - Configurable Plazma + private final org.plazmamc.plazma.configurations.WorldConfigurations plazmaConfig; + public org.plazmamc.plazma.configurations.WorldConfigurations plazmaConfig() { + return this.plazmaConfig; + } + // Plazma end - Configurable Plazma - public final com.destroystokyo.paper.antixray.ChunkPacketBlockController chunkPacketBlockController; // Paper - Anti-Xray public final org.purpurmc.purpur.PurpurWorldConfig purpurConfig; // Purpur -@@ -257,9 +263,10 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + public final co.aikar.timings.WorldTimingsHandler timings; // Paper +@@ -262,9 +267,10 @@ public abstract class Level implements LevelAccessor, AutoCloseable { - public abstract ResourceKey getTypeKey(); + //protected final io.papermc.paper.util.math.ThreadUnsafeRandom randomTickRandom = new io.papermc.paper.util.math.ThreadUnsafeRandom(java.util.concurrent.ThreadLocalRandom.current().nextLong()); public net.minecraft.util.RandomSource getThreadUnsafeRandom() { return this.randomTickRandom; } // Pufferfish - move thread unsafe random initialization // Pufferfish - getter // Purpur - dont break ABI - protected Level(WritableLevelData worlddatamutable, ResourceKey resourcekey, RegistryAccess iregistrycustom, Holder holder, Supplier supplier, boolean flag, boolean flag1, long i, int j, org.bukkit.generator.ChunkGenerator gen, org.bukkit.generator.BiomeProvider biomeProvider, org.bukkit.World.Environment env, java.util.function.Function paperWorldConfigCreator, java.util.concurrent.Executor executor) { // Paper - create paper world config; Async-Anti-Xray: Pass executor + protected Level(WritableLevelData worlddatamutable, ResourceKey resourcekey, RegistryAccess iregistrycustom, Holder holder, Supplier supplier, boolean flag, boolean flag1, long i, int j, org.bukkit.generator.ChunkGenerator gen, org.bukkit.generator.BiomeProvider biomeProvider, org.bukkit.World.Environment env, java.util.function.Function paperWorldConfigCreator, java.util.function.Function plazmaWorldConfigurationCreator, java.util.concurrent.Executor executor) { // Paper - create paper world config; Async-Anti-Xray: Pass executor // Plazma - Configurable Plazma @@ -651,7 +682,7 @@ index eda2f8cc034cf46293be1be117a60cf8b663c303..79ebec0543f730af403240e9c1c01177 this.playerBreedingCooldowns = this.getNewBreedingCooldownCache(); // Purpur this.generator = gen; diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java -index dca6cce8768c8c08e4abba249c30731dbdec7763..dffbcd950e081325539289e2b867570be9dd427d 100644 +index b2470f03eecb1b81d3f0acbd9bc3c38f321ef3d9..ceea71c3e6886b03d017c44c287dc20928f63f09 100644 --- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java +++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java @@ -1068,6 +1068,7 @@ public final class CraftServer implements Server { @@ -662,7 +693,7 @@ index dca6cce8768c8c08e4abba249c30731dbdec7763..dffbcd950e081325539289e2b867570b org.purpurmc.purpur.PurpurConfig.init((File) console.options.valueOf("purpur-settings")); // Purpur for (ServerLevel world : this.console.getAllLevels()) { // world.serverLevelData.setDifficulty(config.difficulty); // Paper - per level difficulty -@@ -3126,6 +3127,13 @@ public final class CraftServer implements Server { +@@ -3111,6 +3112,13 @@ public final class CraftServer implements Server { } // Purpur end @@ -677,11 +708,11 @@ index dca6cce8768c8c08e4abba249c30731dbdec7763..dffbcd950e081325539289e2b867570b public void restart() { org.spigotmc.RestartCommand.restart(); diff --git a/src/main/java/org/bukkit/craftbukkit/Main.java b/src/main/java/org/bukkit/craftbukkit/Main.java -index 333c8a313208652700c21fc2ac629296b5b02078..9b3371fee86b5e41d5564424c0b3a3805b2059f9 100644 +index d36c880012153058803b595429084adb36458741..e8df8e8520cea5d21197c61a172f7211a3a2c34d 100644 --- a/src/main/java/org/bukkit/craftbukkit/Main.java +++ b/src/main/java/org/bukkit/craftbukkit/Main.java -@@ -190,6 +190,14 @@ public class Main { - .defaultsTo("A Plazma Server") // Plazma - Rebrand +@@ -195,6 +195,14 @@ public class Main { + .defaultsTo("Plazma Server") // Plazma - Branding .describedAs("Name"); // Paper end + @@ -1246,7 +1277,7 @@ index 0000000000000000000000000000000000000000..f2d3d51cb4b8fc7a5fd6db1a63289fff + +} diff --git a/src/test/java/org/bukkit/support/AbstractTestingBase.java b/src/test/java/org/bukkit/support/AbstractTestingBase.java -index 1b1d51a68c0abe7d8f0aa1172064192c71ae645e..74af3501397bfd89d637ad5ce72bb8f2c2bbdc8a 100644 +index b786ed8e620feb51baceae41dd2d1538dc298705..6c2db1219714a524b456428523ed4674f79ffe03 100644 --- a/src/test/java/org/bukkit/support/AbstractTestingBase.java +++ b/src/test/java/org/bukkit/support/AbstractTestingBase.java @@ -64,6 +64,7 @@ public abstract class AbstractTestingBase { @@ -1258,10 +1289,10 @@ index 1b1d51a68c0abe7d8f0aa1172064192c71ae645e..74af3501397bfd89d637ad5ce72bb8f2 CraftRegistry.setMinecraftRegistry(REGISTRY_CUSTOM); diff --git a/src/test/java/org/bukkit/support/DummyServer.java b/src/test/java/org/bukkit/support/DummyServer.java -index 7a4681155f740a98ecafa0b992eae1fb5524551f..37b3c3fe0be6366f0cdb868467d6dcb6f2904c41 100644 +index 3b3e44c5ed24f653f7dc1e5d3d4f0ff76084f277..9391d5447e26a42142c6b44c8e470b35c0f9b0cf 100644 --- a/src/test/java/org/bukkit/support/DummyServer.java +++ b/src/test/java/org/bukkit/support/DummyServer.java -@@ -59,6 +59,13 @@ public final class DummyServer { +@@ -57,6 +57,13 @@ public final class DummyServer { when(instance.getTag(anyString(), any(org.bukkit.NamespacedKey.class), any())).thenAnswer(ignored -> new io.papermc.paper.util.EmptyTag()); // paper end - testing additions diff --git a/patches/server/0007-Setup-basic-configuration-sections.patch b/patches/server/0008-Setup-basic-configuration-sections.patch similarity index 100% rename from patches/server/0007-Setup-basic-configuration-sections.patch rename to patches/server/0008-Setup-basic-configuration-sections.patch diff --git a/patches/server/0009-Always-agree-EULA-on-development-mode.patch b/patches/server/0009-Always-agree-EULA-on-development-mode.patch deleted file mode 100644 index 5689168..0000000 --- a/patches/server/0009-Always-agree-EULA-on-development-mode.patch +++ /dev/null @@ -1,154 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: AlphaKR93 -Date: Sun, 5 Nov 2023 10:13:14 +0900 -Subject: [PATCH] Always agree EULA on development mode - - -diff --git a/build.gradle.kts b/build.gradle.kts -index e2c178e4136fa99427f4e394da363caf7872edcd..4ce3329a2d8a5eb04b3b65a7bbdf0b88e4a4bc2d 100644 ---- a/build.gradle.kts -+++ b/build.gradle.kts -@@ -20,6 +20,7 @@ dependencies { - implementation(common.asm.commons) - implementation(common.log4j.iostreams) - implementation(common.commons.lang2) -+ implementation(common.adventure.serializer.ansi) - implementation(server.velocity) { isTransitive = false } - - runtimeOnly(common.maven.provider) -diff --git a/src/main/java/io/papermc/paper/configuration/Configurations.java b/src/main/java/io/papermc/paper/configuration/Configurations.java -index 3ac80b85ba7c4a4e0b0b4aa06fa92f6e4e376c28..d32a963b0210438ae9f313af47125423a2263bf8 100644 ---- a/src/main/java/io/papermc/paper/configuration/Configurations.java -+++ b/src/main/java/io/papermc/paper/configuration/Configurations.java -@@ -85,16 +85,17 @@ public abstract class Configurations { - } - - protected static ContextMap createWorldContextMap(ServerLevel level) { -- return createWorldContextMap(level.convertable.levelDirectory.path(), level.serverLevelData.getLevelName(), level.dimension().location(), level.spigotConfig, level.registryAccess()); -+ return createWorldContextMap(level.convertable.levelDirectory.path(), level.serverLevelData.getLevelName(), level.dimension().location(), level.spigotConfig, level.registryAccess(), level.getGameRules()); - } - -- public static ContextMap createWorldContextMap(Path dir, String levelName, ResourceLocation worldKey, org.spigotmc.SpigotWorldConfig spigotConfig, RegistryAccess registryAccess) { -+ public static ContextMap createWorldContextMap(Path dir, String levelName, ResourceLocation worldKey, org.spigotmc.SpigotWorldConfig spigotConfig, RegistryAccess registryAccess, final GameRules gameRules) { - return ContextMap.builder() - .put(WORLD_DIRECTORY, dir) - .put(WORLD_NAME, levelName) - .put(WORLD_KEY, worldKey) - .put(SPIGOT_WORLD_CONFIG_CONTEXT_KEY, com.google.common.base.Suppliers.ofInstance(spigotConfig)) - .put(REGISTRY_ACCESS, registryAccess) -+ .put(GAME_RULES, gameRules) - .build(); - } - -@@ -290,7 +291,7 @@ public abstract class Configurations { - } - - // Make sure to run version transforms on the default world config first via #setupWorldDefaultsConfig -- public W createWorldConfig(final ContextMap contextMap) throws IOException { -+ public W createWorldConfig(final ContextMap contextMap) { - final String levelName = contextMap.require(WORLD_NAME); - try { - return this.createWorldConfig(contextMap, creator(this.worldConfigClass(), false)); -diff --git a/src/main/java/io/papermc/paper/configuration/PaperConfigurations.java b/src/main/java/io/papermc/paper/configuration/PaperConfigurations.java -index 27a2914fabdc2c5ac70e402a41f5c9bb7bfe3c51..72c2a84fbf4368d9f80888579878f7247b77f363 100644 ---- a/src/main/java/io/papermc/paper/configuration/PaperConfigurations.java -+++ b/src/main/java/io/papermc/paper/configuration/PaperConfigurations.java -@@ -225,7 +225,7 @@ public class PaperConfigurations extends Configurations builder - .register(MapSerializer.TYPE, new MapSerializer(false)) - .register(new EnumValueSerializer()) -@@ -238,7 +238,7 @@ public class PaperConfigurations extends Configurations { - } - - static MutableComponent translatable(String key) { -- return MutableComponent.create(new TranslatableContents(key, (String) null, TranslatableContents.NO_ARGS)); -+ return MutableComponent.create(new TranslatableContents(key, (String) null, org.plazmamc.plazma.constants.Empty.OBJECT)); // Plazma - Reduce allocations - } - - static MutableComponent translatable(String key, Object... args) { -@@ -209,7 +209,7 @@ public interface Component extends Message, FormattedText, Iterable { - } - - static MutableComponent translatableWithFallback(String key, @Nullable String fallback) { -- return MutableComponent.create(new TranslatableContents(key, fallback, TranslatableContents.NO_ARGS)); -+ return MutableComponent.create(new TranslatableContents(key, fallback, org.plazmamc.plazma.constants.Empty.OBJECT)); // Plazma - Reduce allocations - } - - static MutableComponent translatableWithFallback(String key, @Nullable String fallback, Object... args) { -diff --git a/src/main/java/net/minecraft/server/Main.java b/src/main/java/net/minecraft/server/Main.java -index d06b81b7c72f4cd7f6212c470fa0e7d620054122..69ebe97bcff3e8ea4cd2a3575bedaaa829e826b2 100644 ---- a/src/main/java/net/minecraft/server/Main.java -+++ b/src/main/java/net/minecraft/server/Main.java -@@ -171,6 +171,7 @@ public class Main { - - // Spigot Start - boolean eulaAgreed = Boolean.getBoolean( "com.mojang.eula.agree" ); -+ eulaAgreed = eulaAgreed || Boolean.getBoolean("Paper.isRunDev"); // Plazma - Always agree EULA on development mode - if ( eulaAgreed ) - { - System.err.println( "You have used the Spigot command line EULA agreement flag." ); -diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java -index 5afac92929226388bff73aba6cef253b7624b9ce..7cd380fe9e837bbe8ed50c5444fd714f08d5dadf 100644 ---- a/src/main/java/net/minecraft/server/MinecraftServer.java -+++ b/src/main/java/net/minecraft/server/MinecraftServer.java -@@ -313,6 +313,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop mapRenderer.getClass() == org.bukkit.craftbukkit.map.CraftMapRenderer.class)) return; // Plazma - SparklyPaper port; Skip map item ticking if the craft map renderer is not present -+ if (skipTickWhenCraftNotPresent && mapItemSavedData.mapView.getRenderers().stream().noneMatch(mapRenderer -> mapRenderer.getClass() == org.bukkit.craftbukkit.map.CraftMapRenderer.class)) return; // Plazma - SparklyPaper port; Skip map item ticking if the craft map renderer is not present - if (entity instanceof Player player) { - mapItemSavedData.tickCarriedBy(player, stack); - } -diff --git a/src/test/java/org/bukkit/support/DummyServer.java b/src/test/java/org/bukkit/support/DummyServer.java -index 37b3c3fe0be6366f0cdb868467d6dcb6f2904c41..aae4590428d570244e52cb927a043120aec4d160 100644 ---- a/src/test/java/org/bukkit/support/DummyServer.java -+++ b/src/test/java/org/bukkit/support/DummyServer.java -@@ -61,8 +61,6 @@ public final class DummyServer { - - // Plazma start - Configurable Plazma - net.minecraft.server.MinecraftServer handle = mock(withSettings().stubOnly()); -- when(handle.random()).thenReturn(net.minecraft.util.RandomSource.create()); -- when(handle.getFixerUpper()).thenReturn(net.minecraft.util.datafix.DataFixers.getDataFixer()); - net.minecraft.server.MinecraftServer.setServer(handle); - // Plazma end - Configurable Plazma - diff --git a/patches/server/0008-Port-SparklyPaper-patches.patch b/patches/server/0009-Port-SparklyPaper-patches.patch similarity index 90% rename from patches/server/0008-Port-SparklyPaper-patches.patch rename to patches/server/0009-Port-SparklyPaper-patches.patch index 7e6fec7..8360707 100644 --- a/patches/server/0008-Port-SparklyPaper-patches.patch +++ b/patches/server/0009-Port-SparklyPaper-patches.patch @@ -9,10 +9,10 @@ Copyright (C) 2024 SparklyPower Based on commit: 29212936a832106c4d68e2a2017acbea2fdd3cc4 diff --git a/src/main/java/net/minecraft/server/level/ServerEntity.java b/src/main/java/net/minecraft/server/level/ServerEntity.java -index 39e7dcf3c92c9203c190782be401c00c010b8aeb..bb03ef9c06bf55be279d5f5ecc95099ad7b1b10e 100644 +index 04b98e23eed926d8473cc2464e04a5b9f18f1140..cf098d4a3111771c13766285c5ec5f1fc1f539a4 100644 --- a/src/main/java/net/minecraft/server/level/ServerEntity.java +++ b/src/main/java/net/minecraft/server/level/ServerEntity.java -@@ -78,6 +78,7 @@ public class ServerEntity { +@@ -74,6 +74,7 @@ public class ServerEntity { private List> trackedDataValues; // CraftBukkit start public final Set trackedPlayers; // Purpur - private -> public @@ -20,7 +20,7 @@ index 39e7dcf3c92c9203c190782be401c00c010b8aeb..bb03ef9c06bf55be279d5f5ecc95099a public ServerEntity(ServerLevel worldserver, Entity entity, int i, boolean flag, Consumer> consumer, Set trackedPlayers) { this.trackedPlayers = trackedPlayers; -@@ -208,12 +209,14 @@ public class ServerEntity { +@@ -210,12 +211,14 @@ public class ServerEntity { if ((this.trackDelta || this.entity.hasImpulse || this.entity instanceof LivingEntity && ((LivingEntity) this.entity).isFallFlying()) && this.tickCount > 0) { Vec3 vec3d1 = this.entity.getDeltaMovement(); @@ -57,10 +57,10 @@ index f890738d3bb9fb5e70a9d323c6cec97f9948f9cf..52e72277c661b67a54bc5ce584efb041 } } diff --git a/src/main/java/net/minecraft/world/item/MapItem.java b/src/main/java/net/minecraft/world/item/MapItem.java -index 608390ed36710a419de1542b80340dd3fcc7299c..b5c584e5cf767ab7ecfd6b2104d1d31ba0f61d60 100644 +index 8d3c1897044f9a2bbe1911e1a72dc9a00fb246df..a68112a1d1904edfc84acb6c209e13f0836d97e0 100644 --- a/src/main/java/net/minecraft/world/item/MapItem.java +++ b/src/main/java/net/minecraft/world/item/MapItem.java -@@ -268,11 +268,13 @@ public class MapItem extends ComplexItem { +@@ -313,12 +313,14 @@ public class MapItem extends ComplexItem { } } @@ -68,12 +68,13 @@ index 608390ed36710a419de1542b80340dd3fcc7299c..b5c584e5cf767ab7ecfd6b2104d1d31b @Override public void inventoryTick(ItemStack stack, Level world, Entity entity, int slot, boolean selected) { if (!world.isClientSide) { - MapItemSavedData mapItemSavedData = getSavedData(stack, world); - if (mapItemSavedData != null) { + MapItemSavedData worldmap = MapItem.getSavedData(stack, world); + + if (worldmap != null) { + if (skipTickWhenCraftNotPresent && worldmap.mapView.getRenderers().stream().noneMatch(mapRenderer -> mapRenderer.getClass() == org.bukkit.craftbukkit.map.CraftMapRenderer.class)) return; // Plazma - SparklyPaper port; Skip map item ticking if the craft map renderer is not present - if (entity instanceof Player player) { - mapItemSavedData.tickCarriedBy(player, stack); - } + if (entity instanceof Player) { + Player entityhuman = (Player) entity; + diff --git a/src/main/java/org/bukkit/craftbukkit/map/CraftMapColorCache.java b/src/main/java/org/bukkit/craftbukkit/map/CraftMapColorCache.java index 8149b9c51b78eb5c689b7218a2ca3aab60e73bcf..b9a303f6280a2f6ad3616da152922a4f4a504281 100644 --- a/src/main/java/org/bukkit/craftbukkit/map/CraftMapColorCache.java diff --git a/patches/server/0010-Always-agree-EULA-on-development-mode.patch b/patches/server/0010-Always-agree-EULA-on-development-mode.patch new file mode 100644 index 0000000..d838b92 --- /dev/null +++ b/patches/server/0010-Always-agree-EULA-on-development-mode.patch @@ -0,0 +1,18 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: AlphaKR93 +Date: Sun, 5 Nov 2023 10:13:14 +0900 +Subject: [PATCH] Always agree EULA on development mode + + +diff --git a/src/main/java/net/minecraft/server/Main.java b/src/main/java/net/minecraft/server/Main.java +index b91c3b8ca8f840335ba6470658d82c5d71bb75e1..23f0302f2d90b7229828890eb364bc2c9abc11d2 100644 +--- a/src/main/java/net/minecraft/server/Main.java ++++ b/src/main/java/net/minecraft/server/Main.java +@@ -168,6 +168,7 @@ public class Main { + + // Spigot Start + boolean eulaAgreed = Boolean.getBoolean( "com.mojang.eula.agree" ); ++ eulaAgreed = eulaAgreed || Boolean.getBoolean("Paper.isRunDev"); // Plazma - Always agree EULA on development mode + if ( eulaAgreed ) + { + System.err.println( "You have used the Spigot command line EULA agreement flag." );