From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 From: Kevin Raneri Date: Sun, 2 Jun 2024 20:43:58 +0900 Subject: [PATCH] Pufferfish Server Changes Pufferfish Copyright (C) 2024 Pufferfish Studios LLC This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . diff --git a/build.gradle.kts b/build.gradle.kts index 4998aff0b7cb084dcda15c6a18bbe45e99b6000a..74425cec45d784566cb53eeb5391a5cc00d777e5 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -13,7 +13,7 @@ configurations.named(log4jPlugins.compileClasspathConfigurationName) { val alsoShade: Configuration by configurations.creating dependencies { - implementation(project(":paper-api")) + implementation(project(":pufferfish-api")) // Pufferfish // Paper start implementation("org.jline:jline-terminal-jansi:3.21.0") implementation("net.minecrell:terminalconsoleappender:1.3.0") @@ -47,6 +47,14 @@ 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") + // Pufferfish start + implementation("org.yaml:snakeyaml:1.32") + implementation ("me.carleslc.Simple-YAML:Simple-Yaml:1.8.4") { + exclude(group="org.yaml", module="snakeyaml") + } + // Pufferfish end + implementation("com.github.technove:Flare:34637f3f87") // Pufferfish - flare + 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") @@ -67,6 +75,13 @@ paperweight { craftBukkitPackageVersion.set("v1_20_R4") // also needs to be updated in MappingEnvironment } +// Pufferfish Start +tasks.withType { + val compilerArgs = options.compilerArgs + compilerArgs.add("--add-modules=jdk.incubator.vector") +} +// Pufferfish End + tasks.jar { archiveClassifier.set("dev") @@ -80,14 +95,14 @@ tasks.jar { val gitBranch = git("rev-parse", "--abbrev-ref", "HEAD").getText().trim() // Paper attributes( "Main-Class" to "org.bukkit.craftbukkit.Main", - "Implementation-Title" to "Paper", + "Implementation-Title" to "Pufferfish", // Pufferfish "Implementation-Version" to implementationVersion, "Implementation-Vendor" to date, // Paper - "Specification-Title" to "Paper", + "Specification-Title" to "Pufferfish", // Pufferfish "Specification-Version" to project.version, - "Specification-Vendor" to "Paper Team", - "Brand-Id" to "papermc:paper", - "Brand-Name" to "Paper", + "Specification-Vendor" to "Pufferfish Team", // Pufferfish + "Brand-Id" to "pufferfishgg:pufferfish", // Pufferfish + "Brand-Name" to "Pufferfish", // Pufferfish "Build-Number" to (build ?: ""), "Build-Time" to Instant.now().toString(), "Git-Branch" to gitBranch, // Paper diff --git a/src/main/java/co/aikar/timings/TimingsExport.java b/src/main/java/co/aikar/timings/TimingsExport.java index 7620c72a4c243cbeea245203ce03a97cbfa7d922..b35a9f4c5f8960864c402ede8a51fb5ab9c4fcc0 100644 --- a/src/main/java/co/aikar/timings/TimingsExport.java +++ b/src/main/java/co/aikar/timings/TimingsExport.java @@ -240,7 +240,8 @@ public class TimingsExport extends Thread { parent.put("config", createObject( pair("spigot", mapAsJSON(Bukkit.spigot().getSpigotConfig(), null)), pair("bukkit", mapAsJSON(Bukkit.spigot().getBukkitConfig(), null)), - pair("paper", mapAsJSON(Bukkit.spigot().getPaperConfig(), null)) + pair("paper", mapAsJSON(Bukkit.spigot().getPaperConfig(), null)), // Pufferfish + pair("pufferfish", mapAsJSON(gg.pufferfish.pufferfish.PufferfishConfig.getConfigCopy(), null)) // Pufferfish )); new TimingsExport(listeners, parent, history).start(); diff --git a/src/main/java/com/destroystokyo/paper/Metrics.java b/src/main/java/com/destroystokyo/paper/Metrics.java index 4b002e8b75d117b726b0de274a76d3596fce015b..692c962193cf9fcc6801fc93f3220bdc673d527b 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("Pufferfish", serverUUID, logFailedRequests, Bukkit.getLogger()); // Pufferfish metrics.addCustomChart(new Metrics.SimplePie("minecraft_version", () -> { String minecraftVersion = Bukkit.getVersion(); @@ -607,11 +607,11 @@ public class Metrics { 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); + paperVersion = "git-Pufferfish-%s-%s".formatted(Bukkit.getServer().getMinecraftVersion(), buildOrHash); // Pufferfish } else { paperVersion = "unknown"; } - metrics.addCustomChart(new Metrics.SimplePie("paper_version", () -> paperVersion)); + metrics.addCustomChart(new Metrics.SimplePie("pufferfish_version", () -> paperVersion)); // Pufferfish metrics.addCustomChart(new Metrics.DrilldownPie("java_version", () -> { Map> map = new HashMap<>(); diff --git a/src/main/java/com/destroystokyo/paper/util/misc/AreaMap.java b/src/main/java/com/destroystokyo/paper/util/misc/AreaMap.java index 41b9405d6759d865e0d14dd4f95163e9690e967d..091b1ae822e1c0517e59572e7a9bda11e998c0ee 100644 --- a/src/main/java/com/destroystokyo/paper/util/misc/AreaMap.java +++ b/src/main/java/com/destroystokyo/paper/util/misc/AreaMap.java @@ -26,7 +26,7 @@ public abstract class AreaMap { // we use linked for better iteration. // map of: coordinate to set of objects in coordinate - protected final Long2ObjectOpenHashMap> areaMap = new Long2ObjectOpenHashMap<>(1024, 0.7f); + protected Long2ObjectOpenHashMap> areaMap = new Long2ObjectOpenHashMap<>(1024, 0.7f); // Pufferfish - not actually final protected final PooledLinkedHashSets pooledHashSets; protected final ChangeCallback addCallback; @@ -160,7 +160,8 @@ public abstract class AreaMap { protected abstract PooledLinkedHashSets.PooledObjectLinkedOpenHashSet getEmptySetFor(final E object); // expensive op, only for debug - protected void validate(final E object, final int viewDistance) { + protected void validate0(final E object, final int viewDistance) { // Pufferfish - rename this thing just in case it gets used I'd rather a compile time error. + if (true) throw new UnsupportedOperationException(); // Pufferfish - not going to put in the effort to fix this if it doesn't ever get used. int entiesGot = 0; int expectedEntries = (2 * viewDistance + 1); expectedEntries *= expectedEntries; diff --git a/src/main/java/com/destroystokyo/paper/util/misc/PlayerAreaMap.java b/src/main/java/com/destroystokyo/paper/util/misc/PlayerAreaMap.java index 46954db7ecd35ac4018fdf476df7c8020d7ce6c8..1ad890a244bdf6df48a8db68cb43450e08c788a6 100644 --- a/src/main/java/com/destroystokyo/paper/util/misc/PlayerAreaMap.java +++ b/src/main/java/com/destroystokyo/paper/util/misc/PlayerAreaMap.java @@ -5,7 +5,7 @@ import net.minecraft.server.level.ServerPlayer; /** * @author Spottedleaf */ -public final class PlayerAreaMap extends AreaMap { +public class PlayerAreaMap extends AreaMap { // Pufferfish - not actually final public PlayerAreaMap() { super(); diff --git a/src/main/java/gg/airplane/structs/FluidDirectionCache.java b/src/main/java/gg/airplane/structs/FluidDirectionCache.java new file mode 100644 index 0000000000000000000000000000000000000000..aa8467b9dda1f7707e41f50ac7b3e9d7343723ec --- /dev/null +++ b/src/main/java/gg/airplane/structs/FluidDirectionCache.java @@ -0,0 +1,136 @@ +package gg.airplane.structs; + +import it.unimi.dsi.fastutil.HashCommon; + +/** + * This is a replacement for the cache used in FluidTypeFlowing. + * The requirements for the previous cache were: + * - Store 200 entries + * - Look for the flag in the cache + * - If it exists, move to front of cache + * - If it doesn't exist, remove last entry in cache and insert in front + * + * This class accomplishes something similar, however has a few different + * requirements put into place to make this more optimize: + * + * - maxDistance is the most amount of entries to be checked, instead + * of having to check the entire list. + * - In combination with that, entries are all tracked by age and how + * frequently they're used. This enables us to remove old entries, + * without constantly shifting any around. + * + * Usage of the previous map would have to reset the head every single usage, + * shifting the entire map. Here, nothing happens except an increment when + * the cache is hit, and when it needs to replace an old element only a single + * element is modified. + */ +public class FluidDirectionCache { + + private static class FluidDirectionEntry { + private final T data; + private final boolean flag; + private int uses = 0; + private int age = 0; + + private FluidDirectionEntry(T data, boolean flag) { + this.data = data; + this.flag = flag; + } + + public int getValue() { + return this.uses - (this.age >> 1); // age isn't as important as uses + } + + public void incrementUses() { + this.uses = this.uses + 1 & Integer.MAX_VALUE; + } + + public void incrementAge() { + this.age = this.age + 1 & Integer.MAX_VALUE; + } + } + + private final FluidDirectionEntry[] entries; + private final int mask; + private final int maxDistance; // the most amount of entries to check for a value + + public FluidDirectionCache(int size) { + int arraySize = HashCommon.nextPowerOfTwo(size); + this.entries = new FluidDirectionEntry[arraySize]; + this.mask = arraySize - 1; + this.maxDistance = Math.min(arraySize, 4); + } + + public Boolean getValue(T data) { + FluidDirectionEntry curr; + int pos; + + if ((curr = this.entries[pos = HashCommon.mix(data.hashCode()) & this.mask]) == null) { + return null; + } else if (data.equals(curr.data)) { + curr.incrementUses(); + return curr.flag; + } + + int checked = 1; // start at 1 because we already checked the first spot above + + while ((curr = this.entries[pos = (pos + 1) & this.mask]) != null) { + if (data.equals(curr.data)) { + curr.incrementUses(); + return curr.flag; + } else if (++checked >= this.maxDistance) { + break; + } + } + + return null; + } + + public void putValue(T data, boolean flag) { + FluidDirectionEntry curr; + int pos; + + if ((curr = this.entries[pos = HashCommon.mix(data.hashCode()) & this.mask]) == null) { + this.entries[pos] = new FluidDirectionEntry<>(data, flag); // add + return; + } else if (data.equals(curr.data)) { + curr.incrementUses(); + return; + } + + int checked = 1; // start at 1 because we already checked the first spot above + + while ((curr = this.entries[pos = (pos + 1) & this.mask]) != null) { + if (data.equals(curr.data)) { + curr.incrementUses(); + return; + } else if (++checked >= this.maxDistance) { + this.forceAdd(data, flag); + return; + } + } + + this.entries[pos] = new FluidDirectionEntry<>(data, flag); // add + } + + private void forceAdd(T data, boolean flag) { + int expectedPos = HashCommon.mix(data.hashCode()) & this.mask; + + int toRemovePos = expectedPos; + FluidDirectionEntry entryToRemove = this.entries[toRemovePos]; + + for (int i = expectedPos + 1; i < expectedPos + this.maxDistance; i++) { + int pos = i & this.mask; + FluidDirectionEntry entry = this.entries[pos]; + if (entry.getValue() < entryToRemove.getValue()) { + toRemovePos = pos; + entryToRemove = entry; + } + + entry.incrementAge(); // use this as a mechanism to age the other entries + } + + // remove the least used/oldest entry + this.entries[toRemovePos] = new FluidDirectionEntry(data, flag); + } +} diff --git a/src/main/java/gg/airplane/structs/ItemListWithBitset.java b/src/main/java/gg/airplane/structs/ItemListWithBitset.java new file mode 100644 index 0000000000000000000000000000000000000000..1b7a4ee47f4445d7f2ac91d3a73ae113edbdddb2 --- /dev/null +++ b/src/main/java/gg/airplane/structs/ItemListWithBitset.java @@ -0,0 +1,114 @@ +package gg.airplane.structs; + +import net.minecraft.core.NonNullList; +import net.minecraft.world.item.ItemStack; +import org.apache.commons.lang.Validate; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.AbstractList; +import java.util.Arrays; +import java.util.List; + +public class ItemListWithBitset extends AbstractList { + public static ItemListWithBitset fromList(List list) { + if (list instanceof ItemListWithBitset ours) { + return ours; + } + return new ItemListWithBitset(list); + } + + private static ItemStack[] createArray(int size) { + ItemStack[] array = new ItemStack[size]; + Arrays.fill(array, ItemStack.EMPTY); + return array; + } + + private final ItemStack[] items; + + private long bitSet = 0; + private final long allBits; + + private static class OurNonNullList extends NonNullList { + protected OurNonNullList(List delegate) { + super(delegate, ItemStack.EMPTY); + } + } + + public final NonNullList nonNullList = new OurNonNullList(this); + + private ItemListWithBitset(List list) { + this(list.size()); + + for (int i = 0; i < list.size(); i++) { + this.set(i, list.get(i)); + } + } + + public ItemListWithBitset(int size) { + Validate.isTrue(size < Long.BYTES * 8, "size is too large"); + + this.items = createArray(size); + this.allBits = ((1L << size) - 1); + } + + public boolean isCompletelyEmpty() { + return this.bitSet == 0; + } + + public boolean hasFullStacks() { + return (this.bitSet & this.allBits) == allBits; + } + + @Override + public ItemStack set(int index, @NotNull ItemStack itemStack) { + ItemStack existing = this.items[index]; + + this.items[index] = itemStack; + + if (itemStack == ItemStack.EMPTY) { + this.bitSet &= ~(1L << index); + } else { + this.bitSet |= 1L << index; + } + + return existing; + } + + @NotNull + @Override + public ItemStack get(int var0) { + return this.items[var0]; + } + + @Override + public int size() { + return this.items.length; + } + + @Override + public void clear() { + Arrays.fill(this.items, ItemStack.EMPTY); + } + + // these are unsupported for block inventories which have a static size + @Override + public void add(int var0, ItemStack var1) { + throw new UnsupportedOperationException(); + } + + @Override + public ItemStack remove(int var0) { + throw new UnsupportedOperationException(); + } + + @Override + public String toString() { + return "ItemListWithBitset{" + + "items=" + Arrays.toString(items) + + ", bitSet=" + Long.toString(bitSet, 2) + + ", allBits=" + Long.toString(allBits, 2) + + ", size=" + this.items.length + + '}'; + } +} diff --git a/src/main/java/gg/airplane/structs/Long2FloatAgingCache.java b/src/main/java/gg/airplane/structs/Long2FloatAgingCache.java new file mode 100644 index 0000000000000000000000000000000000000000..a7f297ebb569f7c1f205e967ca485be70013a714 --- /dev/null +++ b/src/main/java/gg/airplane/structs/Long2FloatAgingCache.java @@ -0,0 +1,119 @@ +package gg.airplane.structs; + +import it.unimi.dsi.fastutil.HashCommon; + +/** + * A replacement for the cache used in Biome. + */ +public class Long2FloatAgingCache { + + private static class AgingEntry { + private long data; + private float value; + private int uses = 0; + private int age = 0; + + private AgingEntry(long data, float value) { + this.data = data; + this.value = value; + } + + public void replace(long data, float flag) { + this.data = data; + this.value = flag; + } + + public int getValue() { + return this.uses - (this.age >> 1); // age isn't as important as uses + } + + public void incrementUses() { + this.uses = this.uses + 1 & Integer.MAX_VALUE; + } + + public void incrementAge() { + this.age = this.age + 1 & Integer.MAX_VALUE; + } + } + + private final AgingEntry[] entries; + private final int mask; + private final int maxDistance; // the most amount of entries to check for a value + + public Long2FloatAgingCache(int size) { + int arraySize = HashCommon.nextPowerOfTwo(size); + this.entries = new AgingEntry[arraySize]; + this.mask = arraySize - 1; + this.maxDistance = Math.min(arraySize, 4); + } + + public float getValue(long data) { + AgingEntry curr; + int pos; + + if ((curr = this.entries[pos = HashCommon.mix(HashCommon.long2int(data)) & this.mask]) == null) { + return Float.NaN; + } else if (data == curr.data) { + curr.incrementUses(); + return curr.value; + } + + int checked = 1; // start at 1 because we already checked the first spot above + + while ((curr = this.entries[pos = (pos + 1) & this.mask]) != null) { + if (data == curr.data) { + curr.incrementUses(); + return curr.value; + } else if (++checked >= this.maxDistance) { + break; + } + } + + return Float.NaN; + } + + public void putValue(long data, float value) { + AgingEntry curr; + int pos; + + if ((curr = this.entries[pos = HashCommon.mix(HashCommon.long2int(data)) & this.mask]) == null) { + this.entries[pos] = new AgingEntry(data, value); // add + return; + } else if (data == curr.data) { + curr.incrementUses(); + return; + } + + int checked = 1; // start at 1 because we already checked the first spot above + + while ((curr = this.entries[pos = (pos + 1) & this.mask]) != null) { + if (data == curr.data) { + curr.incrementUses(); + return; + } else if (++checked >= this.maxDistance) { + this.forceAdd(data, value); + return; + } + } + + this.entries[pos] = new AgingEntry(data, value); // add + } + + private void forceAdd(long data, float value) { + int expectedPos = HashCommon.mix(HashCommon.long2int(data)) & this.mask; + AgingEntry entryToRemove = this.entries[expectedPos]; + + for (int i = expectedPos + 1; i < expectedPos + this.maxDistance; i++) { + int pos = i & this.mask; + AgingEntry entry = this.entries[pos]; + if (entry.getValue() < entryToRemove.getValue()) { + entryToRemove = entry; + } + + entry.incrementAge(); // use this as a mechanism to age the other entries + } + + // remove the least used/oldest entry + entryToRemove.replace(data, value); + } +} diff --git a/src/main/java/gg/pufferfish/pufferfish/PufferfishCommand.java b/src/main/java/gg/pufferfish/pufferfish/PufferfishCommand.java new file mode 100644 index 0000000000000000000000000000000000000000..020368da69b9a492155f6de6297f74732f4ab6ea --- /dev/null +++ b/src/main/java/gg/pufferfish/pufferfish/PufferfishCommand.java @@ -0,0 +1,68 @@ +package gg.pufferfish.pufferfish; + +import java.io.IOException; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import net.md_5.bungee.api.ChatColor; +import net.minecraft.server.MinecraftServer; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.command.Command; +import org.bukkit.command.CommandSender; + +public class PufferfishCommand extends Command { + + public PufferfishCommand() { + super("pufferfish"); + this.description = "Pufferfish related commands"; + this.usageMessage = "/pufferfish [reload | version]"; + this.setPermission("bukkit.command.pufferfish"); + } + + public static void init() { + MinecraftServer.getServer().server.getCommandMap().register("pufferfish", "Pufferfish", new PufferfishCommand()); + } + + @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; + String prefix = ChatColor.of("#12fff6") + "" + ChatColor.BOLD + "Pufferfish » " + ChatColor.of("#e8f9f9"); + + if (args.length != 1) { + sender.sendMessage(prefix + "Usage: " + usageMessage); + args = new String[]{"version"}; + } + + if (args[0].equalsIgnoreCase("reload")) { + MinecraftServer console = MinecraftServer.getServer(); + try { + PufferfishConfig.load(); + } catch (IOException e) { + sender.sendMessage(Component.text("Failed to reload.", NamedTextColor.RED)); + e.printStackTrace(); + return true; + } + console.server.reloadCount++; + + Command.broadcastCommandMessage(sender, prefix + "Pufferfish configuration has been reloaded."); + } else if (args[0].equalsIgnoreCase("version")) { + Command.broadcastCommandMessage(sender, prefix + "This server is running " + Bukkit.getName() + " version " + Bukkit.getVersion() + " (Implementing API version " + Bukkit.getBukkitVersion() + ")"); + } + + return true; + } +} diff --git a/src/main/java/gg/pufferfish/pufferfish/PufferfishConfig.java b/src/main/java/gg/pufferfish/pufferfish/PufferfishConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..1ca25529245ed369b8705075a8c2e32fa4d9b1d1 --- /dev/null +++ b/src/main/java/gg/pufferfish/pufferfish/PufferfishConfig.java @@ -0,0 +1,334 @@ +package gg.pufferfish.pufferfish; + +import gg.pufferfish.pufferfish.simd.SIMDDetection; +import java.io.File; +import java.io.IOException; +import java.util.Collections; +import net.minecraft.core.registries.BuiltInRegistries; +import java.util.Locale; +import java.util.Map; +import net.minecraft.server.MinecraftServer; +import net.minecraft.tags.TagKey; +import org.apache.logging.log4j.Level; +import org.bukkit.configuration.ConfigurationSection; +import net.minecraft.world.entity.EntityType; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.List; +import gg.pufferfish.pufferfish.flare.FlareCommand; +import net.minecraft.server.MinecraftServer; +import org.apache.logging.log4j.Level; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.configuration.MemoryConfiguration; +import org.jetbrains.annotations.Nullable; +import org.simpleyaml.configuration.comments.CommentType; +import org.simpleyaml.configuration.file.YamlFile; +import org.simpleyaml.exceptions.InvalidConfigurationException; +import org.bukkit.command.SimpleCommandMap; + +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.List; +import java.net.URI; +import java.util.Collections; + +public class PufferfishConfig { + + private static final YamlFile config = new YamlFile(); + private static int updates = 0; + + private static ConfigurationSection convertToBukkit(org.simpleyaml.configuration.ConfigurationSection section) { + ConfigurationSection newSection = new MemoryConfiguration(); + for (String key : section.getKeys(false)) { + if (section.isConfigurationSection(key)) { + newSection.set(key, convertToBukkit(section.getConfigurationSection(key))); + } else { + newSection.set(key, section.get(key)); + } + } + return newSection; + } + + public static ConfigurationSection getConfigCopy() { + return convertToBukkit(config); + } + + public static int getUpdates() { + return updates; + } + + public static void load() throws IOException { + File configFile = new File("pufferfish.yml"); + + if (configFile.exists()) { + try { + config.load(configFile); + } catch (InvalidConfigurationException e) { + throw new IOException(e); + } + } + + getString("info.version", "1.0"); + setComment("info", + "Pufferfish Configuration", + "Check out Pufferfish Host for maximum performance server hosting: https://pufferfish.host", + "Join our Discord for support: https://discord.gg/reZw4vQV9H", + "Download new builds at https://ci.pufferfish.host/job/Pufferfish"); + + for (Method method : PufferfishConfig.class.getDeclaredMethods()) { + if (Modifier.isStatic(method.getModifiers()) && Modifier.isPrivate(method.getModifiers()) && method.getParameterCount() == 0 && + method.getReturnType() == Void.TYPE && !method.getName().startsWith("lambda")) { + method.setAccessible(true); + try { + method.invoke(null); + } catch (Throwable t) { + MinecraftServer.LOGGER.warn("Failed to load configuration option from " + method.getName(), t); + } + } + } + + updates++; + + config.save(configFile); + + // Attempt to detect vectorization + try { + SIMDDetection.isEnabled = SIMDDetection.canEnable(PufferfishLogger.LOGGER); + SIMDDetection.versionLimited = SIMDDetection.getJavaVersion() != 17 && SIMDDetection.getJavaVersion() != 18 && SIMDDetection.getJavaVersion() != 19; + } catch (NoClassDefFoundError | Exception ignored) { + ignored.printStackTrace(); + } + + if (SIMDDetection.isEnabled) { + PufferfishLogger.LOGGER.info("SIMD operations detected as functional. Will replace some operations with faster versions."); + } else if (SIMDDetection.versionLimited) { + PufferfishLogger.LOGGER.warning("Will not enable SIMD! These optimizations are only safely supported on Java 17, Java 18, and Java 19."); + } else { + PufferfishLogger.LOGGER.warning("SIMD operations are available for your server, but are not configured!"); + PufferfishLogger.LOGGER.warning("To enable additional optimizations, add \"--add-modules=jdk.incubator.vector\" to your startup flags, BEFORE the \"-jar\"."); + PufferfishLogger.LOGGER.warning("If you have already added this flag, then SIMD operations are not supported on your JVM or CPU."); + PufferfishLogger.LOGGER.warning("Debug: Java: " + System.getProperty("java.version") + ", test run: " + SIMDDetection.testRun); + } + } + + private static void setComment(String key, String... comment) { + if (config.contains(key)) { + config.setComment(key, String.join("\n", comment), CommentType.BLOCK); + } + } + + private static void ensureDefault(String key, Object defaultValue, String... comment) { + if (!config.contains(key)) { + config.set(key, defaultValue); + config.setComment(key, String.join("\n", comment), CommentType.BLOCK); + } + } + + private static boolean getBoolean(String key, boolean defaultValue, String... comment) { + return getBoolean(key, null, defaultValue, comment); + } + + private static boolean getBoolean(String key, @Nullable String oldKey, boolean defaultValue, String... comment) { + ensureDefault(key, defaultValue, comment); + return config.getBoolean(key, defaultValue); + } + + private static int getInt(String key, int defaultValue, String... comment) { + return getInt(key, null, defaultValue, comment); + } + + private static int getInt(String key, @Nullable String oldKey, int defaultValue, String... comment) { + ensureDefault(key, defaultValue, comment); + return config.getInt(key, defaultValue); + } + + private static double getDouble(String key, double defaultValue, String... comment) { + return getDouble(key, null, defaultValue, comment); + } + + private static double getDouble(String key, @Nullable String oldKey, double defaultValue, String... comment) { + ensureDefault(key, defaultValue, comment); + return config.getDouble(key, defaultValue); + } + + private static String getString(String key, String defaultValue, String... comment) { + return getOldString(key, null, defaultValue, comment); + } + + private static String getOldString(String key, @Nullable String oldKey, String defaultValue, String... comment) { + ensureDefault(key, defaultValue, comment); + return config.getString(key, defaultValue); + } + + private static List getStringList(String key, List defaultValue, String... comment) { + return getStringList(key, null, defaultValue, comment); + } + + private static List getStringList(String key, @Nullable String oldKey, List defaultValue, String... comment) { + ensureDefault(key, defaultValue, comment); + return config.getStringList(key); + } + + public static String sentryDsn; + private static void sentry() { + String sentryEnvironment = System.getenv("SENTRY_DSN"); + String sentryConfig = getString("sentry-dsn", "", "Sentry DSN for improved error logging, leave blank to disable", "Obtain from https://sentry.io/"); + + sentryDsn = sentryEnvironment == null ? sentryConfig : sentryEnvironment; + if (sentryDsn != null && !sentryDsn.isBlank()) { + gg.pufferfish.pufferfish.sentry.SentryManager.init(); + } + } + + public static boolean enableBooks; + private static void books() { + enableBooks = getBoolean("enable-books", true, + "Whether or not books should be writeable.", + "Servers that anticipate being a target for duping may want to consider", + "disabling this option.", + "This can be overridden per-player with the permission pufferfish.usebooks"); + } + + public static boolean tpsCatchup; + private static void tpsCatchup() { + tpsCatchup = getBoolean("tps-catchup", true, + "If this setting is true, the server will run faster after a lag spike in", + "an attempt to maintain 20 TPS. This option (defaults to true per", + "spigot/paper) can cause mobs to move fast after a lag spike."); + } + + public static boolean enableSuffocationOptimization; + private static void suffocationOptimization() { + enableSuffocationOptimization = getBoolean("enable-suffocation-optimization", true, + "Optimizes the suffocation check by selectively skipping", + "the check in a way that still appears vanilla. This should", + "be left enabled on most servers, but is provided as a", + "configuration option if the vanilla deviation is undesirable."); + } + + public static boolean enableAsyncMobSpawning; + public static boolean asyncMobSpawningInitialized; + private static void asyncMobSpawning() { + boolean temp = getBoolean("enable-async-mob-spawning", true, + "Whether or not asynchronous mob spawning should be enabled.", + "On servers with many entities, this can improve performance by up to 15%. You must have", + "paper's per-player-mob-spawns setting set to true for this to work.", + "One quick note - this does not actually spawn mobs async (that would be very unsafe).", + "This just offloads some expensive calculations that are required for mob spawning."); + + // This prevents us from changing the value during a reload. + if (!asyncMobSpawningInitialized) { + asyncMobSpawningInitialized = true; + enableAsyncMobSpawning = temp; + } + } + + public static int maxProjectileLoadsPerTick; + public static int maxProjectileLoadsPerProjectile; + private static void projectileLoading() { + maxProjectileLoadsPerTick = getInt("projectile.max-loads-per-tick", 10, "Controls how many chunks are allowed", "to be sync loaded by projectiles in a tick."); + maxProjectileLoadsPerProjectile = getInt("projectile.max-loads-per-projectile", 10, "Controls how many chunks a projectile", "can load in its lifetime before it gets", "automatically removed."); + + setComment("projectile", "Optimizes projectile settings"); + } + + + public static boolean dearEnabled; + public static int startDistance; + public static int startDistanceSquared; + public static int maximumActivationPrio; + public static int activationDistanceMod; + + private static void dynamicActivationOfBrains() throws IOException { + dearEnabled = getBoolean("dab.enabled", "activation-range.enabled", true); + startDistance = getInt("dab.start-distance", "activation-range.start-distance", 12, + "This value determines how far away an entity has to be", + "from the player to start being effected by DEAR."); + startDistanceSquared = startDistance * startDistance; + maximumActivationPrio = getInt("dab.max-tick-freq", "activation-range.max-tick-freq", 20, + "This value defines how often in ticks, the furthest entity", + "will get their pathfinders and behaviors ticked. 20 = 1s"); + activationDistanceMod = getInt("dab.activation-dist-mod", "activation-range.activation-dist-mod", 8, + "This value defines how much distance modifies an entity's", + "tick frequency. freq = (distanceToPlayer^2) / (2^value)", + "If you want further away entities to tick less often, use 7.", + "If you want further away entities to tick more often, try 9."); + + for (EntityType entityType : BuiltInRegistries.ENTITY_TYPE) { + entityType.dabEnabled = true; // reset all, before setting the ones to true + } + getStringList("dab.blacklisted-entities", "activation-range.blacklisted-entities", Collections.emptyList(), "A list of entities to ignore for activation") + .forEach(name -> EntityType.byString(name).ifPresentOrElse(entityType -> { + entityType.dabEnabled = false; + }, () -> MinecraftServer.LOGGER.warn("Unknown entity \"" + name + "\""))); + + setComment("dab", "Optimizes entity brains when", "they're far away from the player"); + } + + public static Map projectileTimeouts; + private static void projectileTimeouts() { + // Set some defaults + getInt("entity_timeouts.SNOWBALL", -1); + getInt("entity_timeouts.LLAMA_SPIT", -1); + setComment("entity_timeouts", + "These values define a entity's maximum lifespan. If an", + "entity is in this list and it has survived for longer than", + "that number of ticks, then it will be removed. Setting a value to", + "-1 disables this feature."); + + for (EntityType entityType : BuiltInRegistries.ENTITY_TYPE) { + String type = EntityType.getKey(entityType).getPath().toUpperCase(Locale.ROOT); + entityType.ttl = config.getInt("entity_timeouts." + type, -1); + } + } + + public static boolean throttleInactiveGoalSelectorTick; + private static void inactiveGoalSelectorThrottle() { + throttleInactiveGoalSelectorTick = getBoolean("inactive-goal-selector-throttle", "inactive-goal-selector-disable", true, + "Throttles the AI goal selector in entity inactive ticks.", + "This can improve performance by a few percent, but has minor gameplay implications."); + } + + public static boolean allowEndCrystalRespawn; + private static void allowEndCrystalRespawn() { + allowEndCrystalRespawn = getBoolean("allow-end-crystal-respawn", true, + "Allows end crystals to respawn the ender dragon.", + "On servers that expect end crystal fights in the end dimension, disabling this", + "will prevent the server from performing an expensive search to attempt respawning", + "the ender dragon whenever a player places an end crystal."); + } + + public static URI profileWebUrl; + private static void profilerOptions() { + profileWebUrl = URI.create(getString("flare.url", "https://flare.airplane.gg", "Sets the server to use for profiles.")); + + setComment("flare", "Configures Flare, the built-in profiler"); + } + + + public static String accessToken; + private static void airplaneWebServices() { + accessToken = getString("web-services.token", ""); + // todo lookup token (off-thread) and let users know if their token is valid + if (accessToken.length() > 0) { + gg.pufferfish.pufferfish.flare.FlareSetup.init(); // Pufferfish + SimpleCommandMap commandMap = MinecraftServer.getServer().server.getCommandMap(); + if (commandMap.getCommand("flare") == null) { + commandMap.register("flare", "Pufferfish", new FlareCommand()); + } + } + + setComment("web-services", "Options for connecting to Pufferfish/Airplane's online utilities"); + + } + + + public static boolean disableMethodProfiler; + public static boolean disableOutOfOrderChat; + private static void miscSettings() { + disableMethodProfiler = getBoolean("misc.disable-method-profiler", true); + disableOutOfOrderChat = getBoolean("misc.disable-out-of-order-chat", false); + setComment("misc", "Settings for things that don't belong elsewhere"); + } + +} diff --git a/src/main/java/gg/pufferfish/pufferfish/PufferfishLogger.java b/src/main/java/gg/pufferfish/pufferfish/PufferfishLogger.java new file mode 100644 index 0000000000000000000000000000000000000000..53f2df00c6809618a9ee3d2ea72e85e8052fbcf1 --- /dev/null +++ b/src/main/java/gg/pufferfish/pufferfish/PufferfishLogger.java @@ -0,0 +1,16 @@ +package gg.pufferfish.pufferfish; + +import java.util.logging.Level; +import java.util.logging.Logger; +import org.bukkit.Bukkit; + +public class PufferfishLogger extends Logger { + public static final PufferfishLogger LOGGER = new PufferfishLogger(); + + private PufferfishLogger() { + super("Pufferfish", null); + + setParent(Bukkit.getLogger()); + setLevel(Level.ALL); + } +} diff --git a/src/main/java/gg/pufferfish/pufferfish/PufferfishVersionFetcher.java b/src/main/java/gg/pufferfish/pufferfish/PufferfishVersionFetcher.java new file mode 100644 index 0000000000000000000000000000000000000000..893d8c0946ef71a0561221dd76bffff0dc940d56 --- /dev/null +++ b/src/main/java/gg/pufferfish/pufferfish/PufferfishVersionFetcher.java @@ -0,0 +1,136 @@ +package gg.pufferfish.pufferfish; + +import static net.kyori.adventure.text.Component.text; +import static net.kyori.adventure.text.format.NamedTextColor.GREEN; +import static net.kyori.adventure.text.format.NamedTextColor.RED; + +import com.destroystokyo.paper.VersionHistoryManager; +import com.destroystokyo.paper.util.VersionFetcher; +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import java.util.logging.Logger; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.JoinConfiguration; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.format.TextDecoration; +import org.bukkit.craftbukkit.CraftServer; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public class PufferfishVersionFetcher implements VersionFetcher { + + private static final Logger LOGGER = Logger.getLogger("PufferfishVersionFetcher"); + private static final HttpClient client = HttpClient.newHttpClient(); + + private static final URI JENKINS_URI = URI.create("https://ci.pufferfish.host/job/Pufferfish-1.20/lastSuccessfulBuild/buildNumber"); + private static final String GITHUB_FORMAT = "https://api.github.com/repos/pufferfish-gg/Pufferfish/compare/ver/1.20...%s"; + + private static final HttpResponse.BodyHandler JSON_OBJECT_BODY_HANDLER = responseInfo -> HttpResponse.BodySubscribers + .mapping( + HttpResponse.BodySubscribers.ofString(StandardCharsets.UTF_8), + string -> new Gson().fromJson(string, JsonObject.class) + ); + + @Override + public long getCacheTime() { + return TimeUnit.MINUTES.toMillis(30); + } + + @Override + public @NotNull Component getVersionMessage(final @NotNull String serverVersion) { + final String[] parts = CraftServer.class.getPackage().getImplementationVersion().split("-"); + @NotNull Component component; + + if (parts.length != 3) { + component = text("Unknown server version.", RED); + } else { + final String versionString = parts[2]; + + try { + component = this.fetchJenkinsVersion(Integer.parseInt(versionString)); + } catch (NumberFormatException e) { + component = this.fetchGithubVersion(versionString.substring(1, versionString.length() - 1)); + } + } + + final @Nullable Component history = this.getHistory(); + return history != null ? Component + .join(JoinConfiguration.noSeparators(), component, Component.newline(), this.getHistory()) : component; + } + + private @NotNull Component fetchJenkinsVersion(final int versionNumber) { + final HttpRequest request = HttpRequest.newBuilder(JENKINS_URI).build(); + try { + final HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + if (response.statusCode() != 200) { + return text("Received invalid status code (" + response.statusCode() + ") from server.", RED); + } + + int latestVersionNumber; + try { + latestVersionNumber = Integer.parseInt(response.body()); + } catch (NumberFormatException e) { + LOGGER.log(Level.WARNING, "Received invalid response from Jenkins \"" + response.body() + "\"."); + return text("Received invalid response from server.", RED); + } + + final int versionDiff = latestVersionNumber - versionNumber; + return this.getResponseMessage(versionDiff); + } catch (IOException | InterruptedException e) { + LOGGER.log(Level.WARNING, "Failed to look up version from Jenkins", e); + return text("Failed to retrieve version from server.", RED); + } + } + + // Based off code contributed by Techcable in Paper/GH-65 + private @NotNull Component fetchGithubVersion(final @NotNull String hash) { + final URI uri = URI.create(String.format(GITHUB_FORMAT, hash)); + final HttpRequest request = HttpRequest.newBuilder(uri).build(); + try { + final HttpResponse response = client.send(request, JSON_OBJECT_BODY_HANDLER); + if (response.statusCode() != 200) { + return text("Received invalid status code (" + response.statusCode() + ") from server.", RED); + } + + final JsonObject obj = response.body(); + final int versionDiff = obj.get("behind_by").getAsInt(); + + return this.getResponseMessage(versionDiff); + } catch (IOException | InterruptedException e) { + LOGGER.log(Level.WARNING, "Failed to look up version from GitHub", e); + return text("Failed to retrieve version from server.", RED); + } + } + + private @NotNull Component getResponseMessage(final int versionDiff) { + return switch (Math.max(-1, Math.min(1, versionDiff))) { + case -1 -> text("You are running an unsupported version of Pufferfish.", RED); + case 0 -> text("You are on the latest version!", GREEN); + default -> text("You are running " + versionDiff + " version" + (versionDiff == 1 ? "" : "s") + " beyond. " + + "Please update your server when possible to maintain stability, security, and receive the latest optimizations.", + RED); + }; + } + + private @Nullable Component getHistory() { + final VersionHistoryManager.VersionData data = VersionHistoryManager.INSTANCE.getVersionData(); + if (data == null) { + return null; + } + + final String oldVersion = data.getOldVersion(); + if (oldVersion == null) { + return null; + } + + return Component.text("Previous version: " + oldVersion, NamedTextColor.GRAY, TextDecoration.ITALIC); + } +} \ No newline at end of file diff --git a/src/main/java/gg/pufferfish/pufferfish/compat/ServerConfigurations.java b/src/main/java/gg/pufferfish/pufferfish/compat/ServerConfigurations.java new file mode 100644 index 0000000000000000000000000000000000000000..4ad189d52b27560424ddb311d0817a334637dc95 --- /dev/null +++ b/src/main/java/gg/pufferfish/pufferfish/compat/ServerConfigurations.java @@ -0,0 +1,78 @@ +package gg.pufferfish.pufferfish.compat; + +import co.aikar.timings.TimingsManager; +import com.google.common.io.Files; +import org.bukkit.configuration.InvalidConfigurationException; +import org.bukkit.configuration.file.YamlConfiguration; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.stream.Collectors; + +public class ServerConfigurations { + + public static final String[] configurationFiles = new String[]{ + "server.properties", + "bukkit.yml", + "spigot.yml", + // "paper.yml", // TODO: Figure out what to do with this. + "pufferfish.yml" + }; + + public static Map getCleanCopies() throws IOException { + Map files = new HashMap<>(configurationFiles.length); + for (String file : configurationFiles) { + files.put(file, getCleanCopy(file)); + } + return files; + } + + public static String getCleanCopy(String configName) throws IOException { + File file = new File(configName); + List hiddenConfigs = TimingsManager.hiddenConfigs; + + switch (Files.getFileExtension(configName)) { + case "properties": { + Properties properties = new Properties(); + try (FileInputStream inputStream = new FileInputStream(file)) { + properties.load(inputStream); + } + for (String hiddenConfig : hiddenConfigs) { + properties.remove(hiddenConfig); + } + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + properties.store(outputStream, ""); + return Arrays.stream(outputStream.toString() + .split("\n")) + .filter(line -> !line.startsWith("#")) + .collect(Collectors.joining("\n")); + } + case "yml": { + YamlConfiguration configuration = new YamlConfiguration(); + try { + configuration.load(file); + } catch (InvalidConfigurationException e) { + throw new IOException(e); + } + configuration.options().header(null); + for (String key : configuration.getKeys(true)) { + if (hiddenConfigs.contains(key)) { + configuration.set(key, null); + } + } + return configuration.saveToString(); + } + default: + throw new IllegalArgumentException("Bad file type " + configName); + } + } + +} diff --git a/src/main/java/gg/pufferfish/pufferfish/flare/CustomCategories.java b/src/main/java/gg/pufferfish/pufferfish/flare/CustomCategories.java new file mode 100644 index 0000000000000000000000000000000000000000..401b42e29bccb5251684062f10b2e0f8b091bc95 --- /dev/null +++ b/src/main/java/gg/pufferfish/pufferfish/flare/CustomCategories.java @@ -0,0 +1,8 @@ +package gg.pufferfish.pufferfish.flare; + +import co.technove.flare.live.category.GraphCategory; + +public class CustomCategories { + public static final GraphCategory MC_PERF = new GraphCategory("MC Performance"); + public static final GraphCategory ENTITIES_AND_CHUNKS = new GraphCategory("Entities & Chunks"); +} diff --git a/src/main/java/gg/pufferfish/pufferfish/flare/FlareCommand.java b/src/main/java/gg/pufferfish/pufferfish/flare/FlareCommand.java new file mode 100644 index 0000000000000000000000000000000000000000..3785d1512eb650f91d58903672c059e7449598fc --- /dev/null +++ b/src/main/java/gg/pufferfish/pufferfish/flare/FlareCommand.java @@ -0,0 +1,136 @@ +package gg.pufferfish.pufferfish.flare; + +import co.technove.flare.exceptions.UserReportableException; +import co.technove.flare.internal.profiling.ProfileType; +import gg.pufferfish.pufferfish.PufferfishConfig; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.event.ClickEvent; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.format.TextColor; +import net.kyori.adventure.text.format.TextDecoration; +import net.minecraft.server.MinecraftServer; +import org.apache.logging.log4j.Level; +import org.bukkit.Bukkit; +import org.bukkit.command.Command; +import org.bukkit.command.CommandSender; +import org.bukkit.command.ConsoleCommandSender; +import org.bukkit.craftbukkit.scheduler.MinecraftInternalPlugin; +import org.bukkit.util.StringUtil; +import org.jetbrains.annotations.NotNull; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class FlareCommand extends Command { + + private static final String BASE_URL = "https://blog.airplane.gg/flare-tutorial/#setting-the-access-token"; + private static final TextColor HEX = TextColor.fromHexString("#e3eaea"); + private static final Component PREFIX = Component.text() + .append(Component.text("Flare ✈") + .color(TextColor.fromHexString("#6a7eda")) + .decoration(TextDecoration.BOLD, true) + .append(Component.text(" ", HEX) + .decoration(TextDecoration.BOLD, false))) + .asComponent(); + + public FlareCommand() { + super("flare", "Profile your server with Flare", "/flare", Collections.singletonList("profile")); + this.setPermission("airplane.flare"); + } + + @Override + public boolean execute(@NotNull CommandSender sender, @NotNull String commandLabel, String @NotNull [] args) { + if (!testPermission(sender)) return true; + if (PufferfishConfig.accessToken.length() == 0) { + Component clickable = Component.text(BASE_URL, HEX, TextDecoration.UNDERLINED).clickEvent(ClickEvent.clickEvent(ClickEvent.Action.OPEN_URL, BASE_URL)); + + sender.sendMessage(PREFIX.append(Component.text("Flare currently requires an access token to use. To learn more, visit ").color(HEX).append(clickable))); + return true; + } + + if (!FlareSetup.isSupported()) { + sender.sendMessage(PREFIX.append( + Component.text("Profiling is not supported in this environment, check your startup logs for the error.", NamedTextColor.RED))); + return true; + } + if (ProfilingManager.isProfiling()) { + if (args.length == 1 && args[0].equalsIgnoreCase("status")) { + sender.sendMessage(PREFIX.append(Component.text("Current profile has been ran for " + ProfilingManager.getTimeRan().toString(), HEX))); + return true; + } + if (ProfilingManager.stop()) { + if (!(sender instanceof ConsoleCommandSender)) { + sender.sendMessage(PREFIX.append(Component.text("Profiling has been stopped.", HEX))); + } + } else { + sender.sendMessage(PREFIX.append(Component.text("Profiling has already been stopped.", HEX))); + } + } else { + ProfileType profileType = ProfileType.ITIMER; + if (args.length > 0) { + try { + profileType = ProfileType.valueOf(args[0].toUpperCase()); + } catch (Exception e) { + sender.sendMessage(PREFIX.append(Component + .text("Invalid profile type ", HEX) + .append(Component.text(args[0], HEX, TextDecoration.BOLD) + .append(Component.text("!", HEX))) + )); + } + } + ProfileType finalProfileType = profileType; + Bukkit.getScheduler().runTaskAsynchronously(new MinecraftInternalPlugin(), () -> { + try { + if (ProfilingManager.start(finalProfileType)) { + if (!(sender instanceof ConsoleCommandSender)) { + sender.sendMessage(PREFIX.append(Component + .text("Flare has been started: " + ProfilingManager.getProfilingUri(), HEX) + .clickEvent(ClickEvent.openUrl(ProfilingManager.getProfilingUri())) + )); + sender.sendMessage(PREFIX.append(Component.text(" Run /" + commandLabel + " to stop the Flare.", HEX))); + } + } else { + sender.sendMessage(PREFIX.append(Component + .text("Flare has already been started: " + ProfilingManager.getProfilingUri(), HEX) + .clickEvent(ClickEvent.openUrl(ProfilingManager.getProfilingUri())) + )); + } + } catch (UserReportableException e) { + sender.sendMessage(Component.text("Flare failed to start: " + e.getUserError(), NamedTextColor.RED)); + if (e.getCause() != null) { + MinecraftServer.LOGGER.warn("Flare failed to start", e); + } + } + }); + } + return true; + } + + @Override + public @NotNull List tabComplete(@NotNull CommandSender sender, @NotNull String alias, String @NotNull [] args) throws IllegalArgumentException { + List list = new ArrayList<>(); + if (ProfilingManager.isProfiling()) { + if (args.length == 1) { + String lastWord = args[0]; + if (StringUtil.startsWithIgnoreCase("status", lastWord)) { + list.add("status"); + } + if (StringUtil.startsWithIgnoreCase("stop", lastWord)) { + list.add("stop"); + } + } + } else { + if (args.length <= 1) { + String lastWord = args.length == 0 ? "" : args[0]; + for (ProfileType value : ProfileType.values()) { + if (StringUtil.startsWithIgnoreCase(value.getInternalName(), lastWord)) { + list.add(value.name().toLowerCase()); + } + } + } + } + return list; + } +} diff --git a/src/main/java/gg/pufferfish/pufferfish/flare/FlareSetup.java b/src/main/java/gg/pufferfish/pufferfish/flare/FlareSetup.java new file mode 100644 index 0000000000000000000000000000000000000000..cd22e4dcc8b7b57b10a95ef084637249a98e524f --- /dev/null +++ b/src/main/java/gg/pufferfish/pufferfish/flare/FlareSetup.java @@ -0,0 +1,33 @@ +package gg.pufferfish.pufferfish.flare; + +import co.technove.flare.FlareInitializer; +import co.technove.flare.internal.profiling.InitializationException; +import net.minecraft.server.MinecraftServer; +import org.apache.logging.log4j.Level; + +public class FlareSetup { + + private static boolean initialized = false; + private static boolean supported = false; + + public static void init() { + if (initialized) { + return; + } + + initialized = true; + try { + for (String warning : FlareInitializer.initialize()) { + MinecraftServer.LOGGER.warn("Flare warning: " + warning); + } + supported = true; + } catch (InitializationException e) { + MinecraftServer.LOGGER.warn("Failed to enable Flare:", e); + } + } + + public static boolean isSupported() { + return supported; + } + +} diff --git a/src/main/java/gg/pufferfish/pufferfish/flare/PluginLookup.java b/src/main/java/gg/pufferfish/pufferfish/flare/PluginLookup.java new file mode 100644 index 0000000000000000000000000000000000000000..74aab5eb4b54ffbaf19b8976ffb8ca4a64584006 --- /dev/null +++ b/src/main/java/gg/pufferfish/pufferfish/flare/PluginLookup.java @@ -0,0 +1,44 @@ +package gg.pufferfish.pufferfish.flare; + +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import org.bukkit.Bukkit; +import org.bukkit.plugin.Plugin; +import org.bukkit.plugin.java.PluginClassLoader; + +import java.util.Optional; +import java.util.concurrent.TimeUnit; + +public class PluginLookup { + private static final Cache pluginNameCache = CacheBuilder.newBuilder() + .expireAfterAccess(1, TimeUnit.MINUTES) + .maximumSize(1024) + .build(); + + public static Optional getPluginForClass(String name) { + if (name.startsWith("net.minecraft") || name.startsWith("java.") || name.startsWith("com.mojang") || + name.startsWith("com.google") || name.startsWith("it.unimi") || name.startsWith("sun")) { + return Optional.empty(); + } + + String existing = pluginNameCache.getIfPresent(name); + if (existing != null) { + return Optional.ofNullable(existing.isEmpty() ? null : existing); + } + + String newValue = ""; + + for (Plugin plugin : Bukkit.getPluginManager().getPlugins()) { + ClassLoader classLoader = plugin.getClass().getClassLoader(); + if (classLoader instanceof PluginClassLoader) { + if (((PluginClassLoader) classLoader)._airplane_hasClass(name)) { + newValue = plugin.getName(); + break; + } + } + } + + pluginNameCache.put(name, newValue); + return Optional.ofNullable(newValue.isEmpty() ? null : newValue); + } +} diff --git a/src/main/java/gg/pufferfish/pufferfish/flare/ProfilingManager.java b/src/main/java/gg/pufferfish/pufferfish/flare/ProfilingManager.java new file mode 100644 index 0000000000000000000000000000000000000000..e3f76eb11a261c3347f0cd89b5da309bc2dc82f9 --- /dev/null +++ b/src/main/java/gg/pufferfish/pufferfish/flare/ProfilingManager.java @@ -0,0 +1,151 @@ +package gg.pufferfish.pufferfish.flare; + +import co.technove.flare.Flare; +import co.technove.flare.FlareAuth; +import co.technove.flare.FlareBuilder; +import co.technove.flare.exceptions.UserReportableException; +import co.technove.flare.internal.profiling.ProfileType; +import gg.pufferfish.pufferfish.PufferfishConfig; +import gg.pufferfish.pufferfish.PufferfishLogger; +import gg.pufferfish.pufferfish.compat.ServerConfigurations; +import gg.pufferfish.pufferfish.flare.collectors.GCEventCollector; +import gg.pufferfish.pufferfish.flare.collectors.StatCollector; +import gg.pufferfish.pufferfish.flare.collectors.TPSCollector; +import gg.pufferfish.pufferfish.flare.collectors.WorldCountCollector; +import org.bukkit.Bukkit; +import org.bukkit.craftbukkit.scheduler.MinecraftInternalPlugin; +import org.bukkit.scheduler.BukkitTask; +import oshi.SystemInfo; +import oshi.hardware.CentralProcessor; +import oshi.hardware.GlobalMemory; +import oshi.hardware.HardwareAbstractionLayer; +import oshi.hardware.VirtualMemory; +import oshi.software.os.OperatingSystem; + +import java.io.IOException; +import java.net.URI; +import java.time.Duration; +import java.util.Objects; +import java.util.logging.Level; + +public class ProfilingManager { + + private static Flare currentFlare; + private static BukkitTask currentTask = null; + + public static synchronized boolean isProfiling() { + return currentFlare != null && currentFlare.isRunning(); + } + + public static synchronized String getProfilingUri() { + return Objects.requireNonNull(currentFlare).getURI().map(URI::toString).orElse("Flare is not running"); + } + + public static Duration getTimeRan() { + Flare flare = currentFlare; // copy reference so no need to sync + if (flare == null) { + return Duration.ofMillis(0); + } + return flare.getCurrentDuration(); + } + + public static synchronized boolean start(ProfileType profileType) throws UserReportableException { + if (currentFlare != null && !currentFlare.isRunning()) { + currentFlare = null; // errored out + } + if (isProfiling()) { + return false; + } + if (Bukkit.isPrimaryThread()) { + throw new UserReportableException("Profiles should be started off-thread"); + } + + try { + OperatingSystem os = new SystemInfo().getOperatingSystem(); + + SystemInfo systemInfo = new SystemInfo(); + HardwareAbstractionLayer hardware = systemInfo.getHardware(); + + CentralProcessor processor = hardware.getProcessor(); + CentralProcessor.ProcessorIdentifier processorIdentifier = processor.getProcessorIdentifier(); + + GlobalMemory memory = hardware.getMemory(); + VirtualMemory virtualMemory = memory.getVirtualMemory(); + + FlareBuilder builder = new FlareBuilder() + .withProfileType(profileType) + .withMemoryProfiling(true) + .withAuth(FlareAuth.fromTokenAndUrl(PufferfishConfig.accessToken, PufferfishConfig.profileWebUrl)) + + .withFiles(ServerConfigurations.getCleanCopies()) + .withVersion("Primary Version", Bukkit.getVersion()) + .withVersion("Bukkit Version", Bukkit.getBukkitVersion()) + .withVersion("Minecraft Version", Bukkit.getMinecraftVersion()) + + .withGraphCategories(CustomCategories.ENTITIES_AND_CHUNKS, CustomCategories.MC_PERF) + .withCollectors(new TPSCollector(), new WorldCountCollector(), new GCEventCollector(), new StatCollector()) + .withClassIdentifier(PluginLookup::getPluginForClass) + + .withHardware(new FlareBuilder.HardwareBuilder() + .setCoreCount(processor.getPhysicalProcessorCount()) + .setThreadCount(processor.getLogicalProcessorCount()) + .setCpuModel(processorIdentifier.getName()) + .setCpuFrequency(processor.getMaxFreq()) + + .setTotalMemory(memory.getTotal()) + .setTotalSwap(virtualMemory.getSwapTotal()) + .setTotalVirtual(virtualMemory.getVirtualMax()) + ) + + .withOperatingSystem(new FlareBuilder.OperatingSystemBuilder() + .setManufacturer(os.getManufacturer()) + .setFamily(os.getFamily()) + .setVersion(os.getVersionInfo().toString()) + .setBitness(os.getBitness()) + ); + + currentFlare = builder.build(); + } catch (IOException e) { + PufferfishLogger.LOGGER.log(Level.WARNING, "Failed to read configuration files:", e); + throw new UserReportableException("Failed to load configuration files, check logs for further details."); + } + + try { + currentFlare.start(); + } catch (IllegalStateException e) { + PufferfishLogger.LOGGER.log(Level.WARNING, "Error starting Flare:", e); + throw new UserReportableException("Failed to start Flare, check logs for further details."); + } + + currentTask = Bukkit.getScheduler().runTaskLater(new MinecraftInternalPlugin(), ProfilingManager::stop, 20 * 60 * 15); + PufferfishLogger.LOGGER.log(Level.INFO, "Flare has been started: " + getProfilingUri()); + return true; + } + + public static synchronized boolean stop() { + if (!isProfiling()) { + return false; + } + if (!currentFlare.isRunning()) { + currentFlare = null; + return true; + } + PufferfishLogger.LOGGER.log(Level.INFO, "Flare has been stopped: " + getProfilingUri()); + try { + currentFlare.stop(); + } catch (IllegalStateException e) { + PufferfishLogger.LOGGER.log(Level.WARNING, "Error occurred stopping Flare", e); + } + currentFlare = null; + + try { + currentTask.cancel(); + } catch (Throwable t) { + PufferfishLogger.LOGGER.log(Level.WARNING, "Error occurred stopping Flare", t); + } + + currentTask = null; + return true; + } + +} diff --git a/src/main/java/gg/pufferfish/pufferfish/flare/collectors/GCEventCollector.java b/src/main/java/gg/pufferfish/pufferfish/flare/collectors/GCEventCollector.java new file mode 100644 index 0000000000000000000000000000000000000000..d426575c669020f369960107da1e2de2f11f082f --- /dev/null +++ b/src/main/java/gg/pufferfish/pufferfish/flare/collectors/GCEventCollector.java @@ -0,0 +1,66 @@ +package gg.pufferfish.pufferfish.flare.collectors; + +import co.technove.flare.Flare; +import co.technove.flare.internal.FlareInternal; +import co.technove.flare.live.CollectorData; +import co.technove.flare.live.EventCollector; +import co.technove.flare.live.LiveEvent; +import co.technove.flare.live.category.GraphCategory; +import co.technove.flare.live.formatter.DataFormatter; +import com.google.common.collect.ImmutableMap; +import com.sun.management.GarbageCollectionNotificationInfo; + +import javax.management.ListenerNotFoundException; +import javax.management.Notification; +import javax.management.NotificationEmitter; +import javax.management.NotificationListener; +import javax.management.openmbean.CompositeData; +import java.lang.management.GarbageCollectorMXBean; +import java.lang.management.ManagementFactory; + +public class GCEventCollector extends EventCollector implements NotificationListener { + + private static final CollectorData MINOR_GC = new CollectorData("builtin:gc:minor", "Minor GC", "A small pause in the program to allow Garbage Collection to run.", DataFormatter.MILLISECONDS, GraphCategory.SYSTEM); + private static final CollectorData MAJOR_GC = new CollectorData("builtin:gc:major", "Major GC", "A large pause in the program to allow Garbage Collection to run.", DataFormatter.MILLISECONDS, GraphCategory.SYSTEM); + private static final CollectorData UNKNOWN_GC = new CollectorData("builtin:gc:generic", "Major GC", "A run of the Garbage Collection.", DataFormatter.MILLISECONDS, GraphCategory.SYSTEM); + + public GCEventCollector() { + super(MINOR_GC, MAJOR_GC, UNKNOWN_GC); + } + + private static CollectorData fromString(String string) { + if (string.endsWith("minor GC")) { + return MINOR_GC; + } else if (string.endsWith("major GC")) { + return MAJOR_GC; + } + return UNKNOWN_GC; + } + + @Override + public void start(Flare flare) { + for (GarbageCollectorMXBean garbageCollectorBean : ManagementFactory.getGarbageCollectorMXBeans()) { + NotificationEmitter notificationEmitter = (NotificationEmitter) garbageCollectorBean; + notificationEmitter.addNotificationListener(this, null, null); + } + } + + @Override + public void stop(Flare flare) { + for (GarbageCollectorMXBean garbageCollectorBean : ManagementFactory.getGarbageCollectorMXBeans()) { + NotificationEmitter notificationEmitter = (NotificationEmitter) garbageCollectorBean; + try { + notificationEmitter.removeNotificationListener(this); + } catch (ListenerNotFoundException e) { + } + } + } + + @Override + public void handleNotification(Notification notification, Object o) { + if (notification.getType().equals(GarbageCollectionNotificationInfo.GARBAGE_COLLECTION_NOTIFICATION)) { + GarbageCollectionNotificationInfo gcInfo = GarbageCollectionNotificationInfo.from((CompositeData) notification.getUserData()); + reportEvent(new LiveEvent(fromString(gcInfo.getGcAction()), System.currentTimeMillis(), (int) gcInfo.getGcInfo().getDuration(), ImmutableMap.of())); + } + } +} diff --git a/src/main/java/gg/pufferfish/pufferfish/flare/collectors/StatCollector.java b/src/main/java/gg/pufferfish/pufferfish/flare/collectors/StatCollector.java new file mode 100644 index 0000000000000000000000000000000000000000..a22c6dbae53667e4c72464fa27153aee30c7946e --- /dev/null +++ b/src/main/java/gg/pufferfish/pufferfish/flare/collectors/StatCollector.java @@ -0,0 +1,41 @@ +package gg.pufferfish.pufferfish.flare.collectors; + +import co.technove.flare.live.CollectorData; +import co.technove.flare.live.LiveCollector; +import co.technove.flare.live.category.GraphCategory; +import co.technove.flare.live.formatter.DataFormatter; +import com.sun.management.OperatingSystemMXBean; +import oshi.SystemInfo; +import oshi.hardware.CentralProcessor; + +import java.lang.management.ManagementFactory; +import java.time.Duration; + +public class StatCollector extends LiveCollector { + + private static final CollectorData CPU = new CollectorData("builtin:stat:cpu", "CPU Load", "The total amount of CPU usage across all cores.", DataFormatter.PERCENT, GraphCategory.SYSTEM); + private static final CollectorData CPU_PROCESS = new CollectorData("builtin:stat:cpu_process", "Process CPU", "The amount of CPU being used by this process.", DataFormatter.PERCENT, GraphCategory.SYSTEM); + private static final CollectorData MEMORY = new CollectorData("builtin:stat:memory_used", "Memory", "The amount of memory being used currently.", DataFormatter.BYTES, GraphCategory.SYSTEM); + private static final CollectorData MEMORY_TOTAL = new CollectorData("builtin:stat:memory_total", "Memory Total", "The total amount of memory allocated.", DataFormatter.BYTES, GraphCategory.SYSTEM); + + private final OperatingSystemMXBean bean; + private final CentralProcessor processor; + + public StatCollector() { + super(CPU, CPU_PROCESS, MEMORY, MEMORY_TOTAL); + this.interval = Duration.ofSeconds(5); + + this.bean = (OperatingSystemMXBean) ManagementFactory.getOperatingSystemMXBean(); + this.processor = new SystemInfo().getHardware().getProcessor(); + } + + @Override + public void run() { + Runtime runtime = Runtime.getRuntime(); + + this.report(CPU, this.processor.getSystemLoadAverage(1)[0] / 100); // percentage + this.report(CPU_PROCESS, this.bean.getProcessCpuLoad()); + this.report(MEMORY, runtime.totalMemory() - runtime.freeMemory()); + this.report(MEMORY_TOTAL, runtime.totalMemory()); + } +} diff --git a/src/main/java/gg/pufferfish/pufferfish/flare/collectors/TPSCollector.java b/src/main/java/gg/pufferfish/pufferfish/flare/collectors/TPSCollector.java new file mode 100644 index 0000000000000000000000000000000000000000..40447d00aefb5ffedb8a2ee87155a04088f0649f --- /dev/null +++ b/src/main/java/gg/pufferfish/pufferfish/flare/collectors/TPSCollector.java @@ -0,0 +1,31 @@ +package gg.pufferfish.pufferfish.flare.collectors; + +import co.technove.flare.live.CollectorData; +import co.technove.flare.live.LiveCollector; +import co.technove.flare.live.formatter.SuffixFormatter; +import gg.pufferfish.pufferfish.flare.CustomCategories; +import net.minecraft.server.MinecraftServer; +import org.bukkit.Bukkit; + +import java.time.Duration; +import java.util.Arrays; + +public class TPSCollector extends LiveCollector { + private static final CollectorData TPS = new CollectorData("airplane:tps", "TPS", "Ticks per second, or how fast the server updates. For a smooth server this should be a constant 20TPS.", SuffixFormatter.of("TPS"), CustomCategories.MC_PERF); + private static final CollectorData MSPT = new CollectorData("airplane:mspt", "MSPT", "Milliseconds per tick, which can show how well your server is performing. This value should always be under 50mspt.", SuffixFormatter.of("mspt"), CustomCategories.MC_PERF); + + public TPSCollector() { + super(TPS, MSPT); + + this.interval = Duration.ofSeconds(5); + } + + @Override + public void run() { + long[] times = MinecraftServer.getServer().tickTimes5s.getTimes(); + double mspt = ((double) Arrays.stream(times).sum() / (double) times.length) * 1.0E-6D; + + this.report(TPS, Math.min(20D, Math.round(Bukkit.getServer().getTPS()[0] * 100d) / 100d)); + this.report(MSPT, (double) Math.round(mspt * 100d) / 100d); + } +} diff --git a/src/main/java/gg/pufferfish/pufferfish/flare/collectors/WorldCountCollector.java b/src/main/java/gg/pufferfish/pufferfish/flare/collectors/WorldCountCollector.java new file mode 100644 index 0000000000000000000000000000000000000000..029d840e28d67d26d3c0dd6785e25dbf15f9226c --- /dev/null +++ b/src/main/java/gg/pufferfish/pufferfish/flare/collectors/WorldCountCollector.java @@ -0,0 +1,45 @@ +package gg.pufferfish.pufferfish.flare.collectors; + +import co.technove.flare.live.CollectorData; +import co.technove.flare.live.LiveCollector; +import co.technove.flare.live.formatter.SuffixFormatter; +import gg.pufferfish.pufferfish.flare.CustomCategories; +import org.bukkit.Bukkit; +import org.bukkit.World; + +import java.time.Duration; + +public class WorldCountCollector extends LiveCollector { + + private static final CollectorData PLAYER_COUNT = new CollectorData("airplane:world:playercount", "Player Count", "The number of players currently on the server.", new SuffixFormatter(" Player", " Players"), CustomCategories.ENTITIES_AND_CHUNKS); + private static final CollectorData ENTITY_COUNT = new CollectorData("airplane:world:entitycount", "Entity Count", "The number of entities in all worlds", new SuffixFormatter(" Entity", " Entities"), CustomCategories.ENTITIES_AND_CHUNKS); + private static final CollectorData CHUNK_COUNT = new CollectorData("airplane:world:chunkcount", "Chunk Count", "The number of chunks currently loaded.", new SuffixFormatter(" Chunk", " Chunks"), CustomCategories.ENTITIES_AND_CHUNKS); + private static final CollectorData TILE_ENTITY_COUNT = new CollectorData("airplane:world:blockentitycount", "Block Entity Count", "The number of block entities currently loaded.", new SuffixFormatter(" Block Entity", " Block Entities"), CustomCategories.ENTITIES_AND_CHUNKS); + + public WorldCountCollector() { + super(PLAYER_COUNT, ENTITY_COUNT, CHUNK_COUNT, TILE_ENTITY_COUNT); + + this.interval = Duration.ofSeconds(5); + } + + @Override + public void run() { + if (true) return; // This doesn't work, and it's not worth fixing at the moment. + int entities = 0; + int chunkCount = 0; + int tileEntityCount = 0; + + if (!Bukkit.isStopping()) { + for (World world : Bukkit.getWorlds()) { + world.getEntityCount(); + chunkCount += world.getChunkCount(); + tileEntityCount += world.getTileEntityCount(); + } + } + + this.report(PLAYER_COUNT, Bukkit.getOnlinePlayers().size()); + this.report(ENTITY_COUNT, entities); + this.report(CHUNK_COUNT, chunkCount); + this.report(TILE_ENTITY_COUNT, tileEntityCount); + } +} diff --git a/src/main/java/gg/pufferfish/pufferfish/sentry/PufferfishSentryAppender.java b/src/main/java/gg/pufferfish/pufferfish/sentry/PufferfishSentryAppender.java new file mode 100644 index 0000000000000000000000000000000000000000..731ef11c7a025ae95ed8a757b530d834733d0621 --- /dev/null +++ b/src/main/java/gg/pufferfish/pufferfish/sentry/PufferfishSentryAppender.java @@ -0,0 +1,135 @@ +package gg.pufferfish.pufferfish.sentry; + +import com.google.common.reflect.TypeToken; +import com.google.gson.Gson; +import io.sentry.Breadcrumb; +import io.sentry.Sentry; +import io.sentry.SentryEvent; +import io.sentry.SentryLevel; +import io.sentry.protocol.Message; +import io.sentry.protocol.User; +import java.util.Map; +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Marker; +import org.apache.logging.log4j.core.LogEvent; +import org.apache.logging.log4j.core.Logger; +import org.apache.logging.log4j.core.appender.AbstractAppender; +import org.apache.logging.log4j.core.filter.AbstractFilter; + +public class PufferfishSentryAppender extends AbstractAppender { + + private static final org.apache.logging.log4j.Logger logger = LogManager.getLogger(PufferfishSentryAppender.class); + private static final Gson GSON = new Gson(); + + public PufferfishSentryAppender() { + super("PufferfishSentryAdapter", new SentryFilter(), null); + } + + @Override + public void append(LogEvent logEvent) { + if (logEvent.getThrown() != null && logEvent.getLevel().isMoreSpecificThan(Level.WARN)) { + try { + logException(logEvent); + } catch (Exception e) { + logger.warn("Failed to log event with sentry", e); + } + } else { + try { + logBreadcrumb(logEvent); + } catch (Exception e) { + logger.warn("Failed to log event with sentry", e); + } + } + } + + private void logException(LogEvent e) { + SentryEvent event = new SentryEvent(e.getThrown()); + + Message sentryMessage = new Message(); + sentryMessage.setMessage(e.getMessage().getFormattedMessage()); + + event.setThrowable(e.getThrown()); + event.setLevel(getLevel(e.getLevel())); + event.setLogger(e.getLoggerName()); + event.setTransaction(e.getLoggerName()); + event.setExtra("thread_name", e.getThreadName()); + + boolean hasContext = e.getContextData() != null; + + if (hasContext && e.getContextData().containsKey("pufferfishsentry_playerid")) { + User user = new User(); + user.setId(e.getContextData().getValue("pufferfishsentry_playerid")); + user.setUsername(e.getContextData().getValue("pufferfishsentry_playername")); + event.setUser(user); + } + + if (hasContext && e.getContextData().containsKey("pufferfishsentry_pluginname")) { + event.setExtra("plugin.name", e.getContextData().getValue("pufferfishsentry_pluginname")); + event.setExtra("plugin.version", e.getContextData().getValue("pufferfishsentry_pluginversion")); + event.setTransaction(e.getContextData().getValue("pufferfishsentry_pluginname")); + } + + if (hasContext && e.getContextData().containsKey("pufferfishsentry_eventdata")) { + Map eventFields = GSON.fromJson((String) e.getContextData().getValue("pufferfishsentry_eventdata"), new TypeToken>() {}.getType()); + if (eventFields != null) { + event.setExtra("event", eventFields); + } + } + + Sentry.captureEvent(event); + } + + private void logBreadcrumb(LogEvent e) { + Breadcrumb breadcrumb = new Breadcrumb(); + + breadcrumb.setLevel(getLevel(e.getLevel())); + breadcrumb.setCategory(e.getLoggerName()); + breadcrumb.setType(e.getLoggerName()); + breadcrumb.setMessage(e.getMessage().getFormattedMessage()); + + Sentry.addBreadcrumb(breadcrumb); + } + + private SentryLevel getLevel(Level level) { + switch (level.getStandardLevel()) { + case TRACE: + case DEBUG: + return SentryLevel.DEBUG; + case WARN: + return SentryLevel.WARNING; + case ERROR: + return SentryLevel.ERROR; + case FATAL: + return SentryLevel.FATAL; + case INFO: + default: + return SentryLevel.INFO; + } + } + + private static class SentryFilter extends AbstractFilter { + + @Override + public Result filter(Logger logger, org.apache.logging.log4j.Level level, Marker marker, String msg, + Object... params) { + return this.filter(logger.getName()); + } + + @Override + public Result filter(Logger logger, org.apache.logging.log4j.Level level, Marker marker, Object msg, Throwable t) { + return this.filter(logger.getName()); + } + + @Override + public Result filter(LogEvent event) { + return this.filter(event == null ? null : event.getLoggerName()); + } + + private Result filter(String loggerName) { + return loggerName != null && loggerName.startsWith("gg.castaway.pufferfish.sentry") ? Result.DENY + : Result.NEUTRAL; + } + + } +} diff --git a/src/main/java/gg/pufferfish/pufferfish/sentry/SentryManager.java b/src/main/java/gg/pufferfish/pufferfish/sentry/SentryManager.java new file mode 100644 index 0000000000000000000000000000000000000000..1b29210ad0bbb4ada150f23357f0c80d331c996d --- /dev/null +++ b/src/main/java/gg/pufferfish/pufferfish/sentry/SentryManager.java @@ -0,0 +1,40 @@ +package gg.pufferfish.pufferfish.sentry; + +import gg.pufferfish.pufferfish.PufferfishConfig; +import io.sentry.Sentry; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public class SentryManager { + + private static final Logger logger = LogManager.getLogger(SentryManager.class); + + private SentryManager() { + + } + + private static boolean initialized = false; + + public static synchronized void init() { + if (initialized) { + return; + } + try { + initialized = true; + + Sentry.init(options -> { + options.setDsn(PufferfishConfig.sentryDsn); + options.setMaxBreadcrumbs(100); + }); + + PufferfishSentryAppender appender = new PufferfishSentryAppender(); + appender.start(); + ((org.apache.logging.log4j.core.Logger) LogManager.getRootLogger()).addAppender(appender); + logger.info("Sentry logging started!"); + } catch (Exception e) { + logger.warn("Failed to initialize sentry!", e); + initialized = false; + } + } + +} diff --git a/src/main/java/gg/pufferfish/pufferfish/util/AsyncExecutor.java b/src/main/java/gg/pufferfish/pufferfish/util/AsyncExecutor.java new file mode 100644 index 0000000000000000000000000000000000000000..8e5323d5d9af25c8a85c4b34a6be76cfc54384cf --- /dev/null +++ b/src/main/java/gg/pufferfish/pufferfish/util/AsyncExecutor.java @@ -0,0 +1,73 @@ +package gg.pufferfish.pufferfish.util; + +import com.google.common.collect.Queues; +import gg.pufferfish.pufferfish.PufferfishLogger; +import java.util.Queue; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; +import java.util.logging.Level; + +public class AsyncExecutor implements Runnable { + + private final Queue jobs = Queues.newArrayDeque(); + private final Lock mutex = new ReentrantLock(); + private final Condition cond = mutex.newCondition(); + private final Thread thread; + private volatile boolean killswitch = false; + + public AsyncExecutor(String threadName) { + this.thread = new Thread(this, threadName); + } + + public void start() { + thread.start(); + } + + public void kill() { + killswitch = true; + cond.signalAll(); + } + + public void submit(Runnable runnable) { + mutex.lock(); + try { + jobs.offer(runnable); + cond.signalAll(); + } finally { + mutex.unlock(); + } + } + + @Override + public void run() { + while (!killswitch) { + try { + Runnable runnable = takeRunnable(); + if (runnable != null) { + runnable.run(); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } catch (Exception e) { + PufferfishLogger.LOGGER.log(Level.SEVERE, e, () -> "Failed to execute async job for thread " + thread.getName()); + } + } + } + + private Runnable takeRunnable() throws InterruptedException { + mutex.lock(); + try { + while (jobs.isEmpty() && !killswitch) { + cond.await(); + } + + if (jobs.isEmpty()) return null; // We've set killswitch + + return jobs.remove(); + } finally { + mutex.unlock(); + } + } + +} diff --git a/src/main/java/gg/pufferfish/pufferfish/util/AsyncPlayerAreaMap.java b/src/main/java/gg/pufferfish/pufferfish/util/AsyncPlayerAreaMap.java new file mode 100644 index 0000000000000000000000000000000000000000..fdcb62d12164024a5f354d60cc863821a18d1b2a --- /dev/null +++ b/src/main/java/gg/pufferfish/pufferfish/util/AsyncPlayerAreaMap.java @@ -0,0 +1,31 @@ +package gg.pufferfish.pufferfish.util; + +import com.destroystokyo.paper.util.misc.PlayerAreaMap; +import com.destroystokyo.paper.util.misc.PooledLinkedHashSets; +import java.util.concurrent.ConcurrentHashMap; +import net.minecraft.server.level.ServerPlayer; + +public final class AsyncPlayerAreaMap extends PlayerAreaMap { + + public AsyncPlayerAreaMap() { + super(); + this.areaMap = new Long2ObjectOpenHashMapWrapper<>(new ConcurrentHashMap<>(1024, 0.7f)); + } + + public AsyncPlayerAreaMap(final PooledLinkedHashSets pooledHashSets) { + super(pooledHashSets); + this.areaMap = new Long2ObjectOpenHashMapWrapper<>(new ConcurrentHashMap<>(1024, 0.7f)); + } + + public AsyncPlayerAreaMap(final PooledLinkedHashSets pooledHashSets, final ChangeCallback addCallback, + final ChangeCallback removeCallback) { + this(pooledHashSets, addCallback, removeCallback, null); + } + + public AsyncPlayerAreaMap(final PooledLinkedHashSets pooledHashSets, final ChangeCallback addCallback, + final ChangeCallback removeCallback, final ChangeSourceCallback changeSourceCallback) { + super(pooledHashSets, addCallback, removeCallback, changeSourceCallback); + this.areaMap = new Long2ObjectOpenHashMapWrapper<>(new ConcurrentHashMap<>(1024, 0.7f)); + } + +} diff --git a/src/main/java/gg/pufferfish/pufferfish/util/IterableWrapper.java b/src/main/java/gg/pufferfish/pufferfish/util/IterableWrapper.java new file mode 100644 index 0000000000000000000000000000000000000000..c1929840254a3e6d721816f4a20415bea1742580 --- /dev/null +++ b/src/main/java/gg/pufferfish/pufferfish/util/IterableWrapper.java @@ -0,0 +1,20 @@ +package gg.pufferfish.pufferfish.util; + +import java.util.Iterator; +import org.jetbrains.annotations.NotNull; + +public class IterableWrapper implements Iterable { + + private final Iterator iterator; + + public IterableWrapper(Iterator iterator) { + this.iterator = iterator; + } + + @NotNull + @Override + public Iterator iterator() { + return iterator; + } + +} diff --git a/src/main/java/gg/pufferfish/pufferfish/util/Long2ObjectOpenHashMapWrapper.java b/src/main/java/gg/pufferfish/pufferfish/util/Long2ObjectOpenHashMapWrapper.java new file mode 100644 index 0000000000000000000000000000000000000000..facd55463d44cb7e3d2ca6892982f5497b8dded1 --- /dev/null +++ b/src/main/java/gg/pufferfish/pufferfish/util/Long2ObjectOpenHashMapWrapper.java @@ -0,0 +1,40 @@ +package gg.pufferfish.pufferfish.util; + +import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; +import java.util.Map; +import org.jetbrains.annotations.Nullable; + +public class Long2ObjectOpenHashMapWrapper extends Long2ObjectOpenHashMap { + + private final Map backingMap; + + public Long2ObjectOpenHashMapWrapper(Map map) { + backingMap = map; + } + + @Override + public V put(Long key, V value) { + return backingMap.put(key, value); + } + + @Override + public V get(Object key) { + return backingMap.get(key); + } + + @Override + public V remove(Object key) { + return backingMap.remove(key); + } + + @Nullable + @Override + public V putIfAbsent(Long key, V value) { + return backingMap.putIfAbsent(key, value); + } + + @Override + public int size() { + return backingMap.size(); + } +} diff --git a/src/main/java/io/papermc/paper/ServerBuildInfoImpl.java b/src/main/java/io/papermc/paper/ServerBuildInfoImpl.java index 790bad0494454ca12ee152e3de6da3da634d9b20..12d52f6677a1c19084a8d5c7d56850efbe203877 100644 --- a/src/main/java/io/papermc/paper/ServerBuildInfoImpl.java +++ b/src/main/java/io/papermc/paper/ServerBuildInfoImpl.java @@ -31,6 +31,7 @@ public record ServerBuildInfoImpl( private static final String ATTRIBUTE_GIT_COMMIT = "Git-Commit"; private static final String BRAND_PAPER_NAME = "Paper"; + private static final String BRAND_PUFFERFISH_NAME = "Pufferfish"; // Pufferfish private static final String BUILD_DEV = "DEV"; @@ -42,9 +43,9 @@ public record ServerBuildInfoImpl( this( getManifestAttribute(manifest, ATTRIBUTE_BRAND_ID) .map(Key::key) - .orElse(BRAND_PAPER_ID), + .orElse(BRAND_PUFFERFISH_ID), // Pufferfish getManifestAttribute(manifest, ATTRIBUTE_BRAND_NAME) - .orElse(BRAND_PAPER_NAME), + .orElse(BRAND_PUFFERFISH_NAME), // Pufferfish SharedConstants.getCurrentVersion().getId(), SharedConstants.getCurrentVersion().getName(), getManifestAttribute(manifest, ATTRIBUTE_BUILD_NUMBER) diff --git a/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java b/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java index 2874bc3001c4e7d9191e47ba512c5a68369c21f1..32035e37b39ba42232fea948166e7c1d4d06190c 100644 --- a/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java +++ b/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java @@ -8,6 +8,7 @@ import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; import net.minecraft.network.protocol.Packet; import net.minecraft.network.protocol.game.ServerboundPlaceRecipePacket; +import org.bukkit.Bukkit; // Pufferfish import org.checkerframework.checker.nullness.qual.Nullable; import org.slf4j.Logger; import org.spongepowered.configurate.objectmapping.ConfigSerializable; @@ -19,6 +20,7 @@ import org.spongepowered.configurate.objectmapping.meta.Setting; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.logging.Level; // Pufferfish @SuppressWarnings({"CanBeFinal", "FieldCanBeLocal", "FieldMayBeFinal", "NotNullFieldNotInitialized", "InnerClassMayBeStatic"}) public class GlobalConfiguration extends ConfigurationPart { @@ -97,6 +99,7 @@ public class GlobalConfiguration extends ConfigurationPart { @Deprecated(forRemoval = true) public class Timings extends ConfigurationPart { public boolean enabled = true; + public boolean reallyEnabled = false; public boolean verbose = true; public String url = "https://timings.aikar.co/"; public boolean serverNamePrivacy = false; @@ -110,6 +113,14 @@ public class GlobalConfiguration extends ConfigurationPart { @PostProcess private void postProcess() { + // Pufferfish start + if (enabled && !reallyEnabled) { + Bukkit.getLogger().log(Level.WARNING, "[Pufferfish] To improve performance, timings have been disabled by default"); + Bukkit.getLogger().log(Level.WARNING, "[Pufferfish] You can still use timings by using /timings on, but they will not start on server startup unless you set timings.really-enabled to true in paper.yml"); + Bukkit.getLogger().log(Level.WARNING, "[Pufferfish] If you would like to disable this message, either set timings.really-enabled to true or timings.enabled to false."); + } + enabled = reallyEnabled; + // Pufferfish end MinecraftTimings.processConfig(this); } } diff --git a/src/main/java/io/papermc/paper/util/MCUtil.java b/src/main/java/io/papermc/paper/util/MCUtil.java index 1d6b3fe2ce240af4ede61588795456b046eee6c9..cdcb1bff7913bfe86fed008271016a3175b6df90 100644 --- a/src/main/java/io/papermc/paper/util/MCUtil.java +++ b/src/main/java/io/papermc/paper/util/MCUtil.java @@ -215,7 +215,7 @@ public final class MCUtil { } public static long getCoordinateKey(final Entity entity) { - return ((long)(MCUtil.fastFloor(entity.getZ()) >> 4) << 32) | ((MCUtil.fastFloor(entity.getX()) >> 4) & 0xFFFFFFFFL); + return ((long)(entity.blockPosition.getZ() >> 4) << 32) | ((entity.blockPosition.getX() >> 4) & 0xFFFFFFFFL); // Pufferfish - eliminate double->long cast in hotpath } public static long getCoordinateKey(final ChunkPos pair) { diff --git a/src/main/java/net/minecraft/network/chat/SignedMessageChain.java b/src/main/java/net/minecraft/network/chat/SignedMessageChain.java index 300929a406905f5ff1ede664d5b99fb0938d4d2e..01a6b1135420ea659d092ddca499f281d7240054 100644 --- a/src/main/java/net/minecraft/network/chat/SignedMessageChain.java +++ b/src/main/java/net/minecraft/network/chat/SignedMessageChain.java @@ -45,7 +45,7 @@ public class SignedMessageChain { SignedMessageLink signedMessageLink = SignedMessageChain.this.nextLink; if (signedMessageLink == null) { throw new SignedMessageChain.DecodeException(SignedMessageChain.DecodeException.CHAIN_BROKEN); - } else if (body.timeStamp().isBefore(SignedMessageChain.this.lastTimeStamp)) { + } else if (!gg.pufferfish.pufferfish.PufferfishConfig.disableOutOfOrderChat && body.timeStamp().isBefore(SignedMessageChain.this.lastTimeStamp)) { // Pufferfish this.setChainBroken(); throw new SignedMessageChain.DecodeException(SignedMessageChain.DecodeException.OUT_OF_ORDER_CHAT, org.bukkit.event.player.PlayerKickEvent.Cause.OUT_OF_ORDER_CHAT); // Paper - kick event causes } else { diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java index 0ed42fa899721f83b598db05be1b5f321af3614a..c110559de69f61c56445f12e733021b6acbcfb4a 100644 --- a/src/main/java/net/minecraft/server/MinecraftServer.java +++ b/src/main/java/net/minecraft/server/MinecraftServer.java @@ -316,6 +316,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop S spin(Function serverFactory) { AtomicReference atomicreference = new AtomicReference(); @@ -1234,6 +1235,12 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop needsChangeBroadcasting = new it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet<>(); - public final com.destroystokyo.paper.util.misc.PlayerAreaMap playerMobSpawnMap = new com.destroystokyo.paper.util.misc.PlayerAreaMap(this.pooledLinkedPlayerHashSets); + public final com.destroystokyo.paper.util.misc.PlayerAreaMap playerMobSpawnMap = new gg.pufferfish.pufferfish.util.AsyncPlayerAreaMap(this.pooledLinkedPlayerHashSets); // Pufferfish // Paper end - optimise chunk tick iteration public ChunkMap(ServerLevel world, LevelStorageSource.LevelStorageAccess session, DataFixer dataFixer, StructureTemplateManager structureTemplateManager, Executor executor, BlockableEventLoop mainThreadExecutor, LightChunkGetter chunkProvider, ChunkGenerator chunkGenerator, ChunkProgressListener worldGenerationProgressListener, ChunkStatusUpdateListener chunkStatusChangeListener, Supplier persistentStateManagerFactory, int viewDistance, boolean dsync) { @@ -1463,8 +1463,28 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider return ChunkMap.this.level.getServer().getScaledTrackingDistance(initialDistance); } + private static int getHighestRange(Entity parent, int highest) { + List passengers = parent.getPassengers(); + + for (int i = 0, size = passengers.size(); i < size; i++) { + Entity entity = passengers.get(i); + int range = entity.getType().clientTrackingRange() * 16; + range = org.spigotmc.TrackingRange.getEntityTrackingRange(entity, range); // Paper + + if (range > highest) { // Paper - we need the lowest range thanks to the fact that our tracker doesn't account for passenger logic // Tuinity - not anymore! + highest = range; + } + + highest = getHighestRange(entity, highest); + } + + return highest; + } + private int getEffectiveRange() { int i = this.range; + // Pufferfish start - remove iterators and streams + /* Iterator iterator = this.entity.getIndirectPassengers().iterator(); while (iterator.hasNext()) { @@ -1476,6 +1496,9 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider i = j; } } + */ + i = getHighestRange(this.entity, i); + // Pufferfish end return this.scaledRange(i); } diff --git a/src/main/java/net/minecraft/server/level/ServerChunkCache.java b/src/main/java/net/minecraft/server/level/ServerChunkCache.java index b99f50604bafecbc68835974c9ed0caa91911a40..cadbf2501c87ea22b3ff5db7d8e4aeebfa3d3abc 100644 --- a/src/main/java/net/minecraft/server/level/ServerChunkCache.java +++ b/src/main/java/net/minecraft/server/level/ServerChunkCache.java @@ -75,6 +75,9 @@ public class ServerChunkCache extends ChunkSource { private final LevelChunk[] lastLoadedChunks = new LevelChunk[4 * 4]; // Paper end + public boolean firstRunSpawnCounts = true; // Pufferfish + public final java.util.concurrent.atomic.AtomicBoolean _pufferfish_spawnCountsReady = new java.util.concurrent.atomic.AtomicBoolean(false); // Pufferfish - optimize countmobs + public ServerChunkCache(ServerLevel world, LevelStorageSource.LevelStorageAccess session, DataFixer dataFixer, StructureTemplateManager structureTemplateManager, Executor workerExecutor, ChunkGenerator chunkGenerator, int viewDistance, int simulationDistance, boolean dsync, ChunkProgressListener worldGenerationProgressListener, ChunkStatusUpdateListener chunkStatusChangeListener, Supplier persistentStateManagerFactory) { this.level = world; this.mainThreadProcessor = new ServerChunkCache.MainThreadExecutor(world); @@ -511,6 +514,7 @@ public class ServerChunkCache extends ChunkSource { // Paper - optimise chunk tick iteration + this.level.resetIceAndSnowTick(); // Pufferfish - reset ice & snow tick random if (this.level.tickRateManager().runsNormally()) { gameprofilerfiller.popPush("naturalSpawnCount"); this.level.timings.countNaturalMobs.startTiming(); // Paper - timings @@ -519,6 +523,7 @@ public class ServerChunkCache extends ChunkSource { int naturalSpawnChunkCount = k; NaturalSpawner.SpawnState spawnercreature_d; // moved down if ((this.spawnFriendlies || this.spawnEnemies) && this.level.paperConfig().entities.spawning.perPlayerMobSpawns) { // don't count mobs when animals and monsters are disabled + if (!gg.pufferfish.pufferfish.PufferfishConfig.enableAsyncMobSpawning) { // Pufferfish - moved down when async processing // re-set mob counts for (ServerPlayer player : this.level.players) { // Paper start - per player mob spawning backoff @@ -533,14 +538,18 @@ public class ServerChunkCache extends ChunkSource { } // Paper end - per player mob spawning backoff } - spawnercreature_d = NaturalSpawner.createState(naturalSpawnChunkCount, this.level.getAllEntities(), this::getFullChunk, null, true); + lastSpawnState = NaturalSpawner.createState(naturalSpawnChunkCount, this.level.getAllEntities(), this::getFullChunk, null, true); // Pufferfish - async mob spawning + } // Pufferfish - (endif) moved down when async processing } else { - spawnercreature_d = NaturalSpawner.createState(naturalSpawnChunkCount, this.level.getAllEntities(), this::getFullChunk, !this.level.paperConfig().entities.spawning.perPlayerMobSpawns ? new LocalMobCapCalculator(this.chunkMap) : null, false); + // Pufferfish start - async mob spawning + lastSpawnState = NaturalSpawner.createState(naturalSpawnChunkCount, this.level.getAllEntities(), this::getFullChunk, !this.level.paperConfig().entities.spawning.perPlayerMobSpawns ? new LocalMobCapCalculator(this.chunkMap) : null, false); + _pufferfish_spawnCountsReady.set(true); + // Pufferfish end } // Paper end - Optional per player mob spawns this.level.timings.countNaturalMobs.stopTiming(); // Paper - timings - this.lastSpawnState = spawnercreature_d; + // this.lastSpawnState = spawnercreature_d; // Pufferfish - this is managed asynchronously gameprofilerfiller.popPush("spawnAndTick"); boolean flag = this.level.getGameRules().getBoolean(GameRules.RULE_DOMOBSPAWNING) && !this.level.players().isEmpty(); // CraftBukkit @@ -630,8 +639,8 @@ public class ServerChunkCache extends ChunkSource { if (tick && chunk1.chunkStatus.isOrAfter(net.minecraft.server.level.FullChunkStatus.ENTITY_TICKING)) { // Paper end - optimise chunk tick iteration chunk1.incrementInhabitedTime(j); - if (spawn && flag && (this.spawnEnemies || this.spawnFriendlies) && this.level.getWorldBorder().isWithinBounds(chunkcoordintpair)) { // Spigot // Paper - optimise chunk tick iteration - NaturalSpawner.spawnForChunk(this.level, chunk1, spawnercreature_d, this.spawnFriendlies, this.spawnEnemies, flag1); + if (spawn && flag && (!gg.pufferfish.pufferfish.PufferfishConfig.enableAsyncMobSpawning || _pufferfish_spawnCountsReady.get()) && (this.spawnEnemies || this.spawnFriendlies) && this.level.getWorldBorder().isWithinBounds(chunkcoordintpair)) { // Spigot // Paper - optimise chunk tick iteration // Pufferfish + NaturalSpawner.spawnForChunk(this.level, chunk1, lastSpawnState, this.spawnFriendlies, this.spawnEnemies, flag1); // Pufferfish } if (true || this.level.shouldTickBlocksAt(chunkcoordintpair.toLong())) { // Paper - optimise chunk tick iteration @@ -678,6 +687,40 @@ public class ServerChunkCache extends ChunkSource { gameprofilerfiller.pop(); gameprofilerfiller.pop(); } + + // Pufferfish start - optimize mob spawning + if (gg.pufferfish.pufferfish.PufferfishConfig.enableAsyncMobSpawning) { + for (ServerPlayer player : this.level.players) { + // Paper start - per player mob spawning backoff + for (int ii = 0; ii < ServerPlayer.MOBCATEGORY_TOTAL_ENUMS; ii++) { + player.mobCounts[ii] = 0; + + int newBackoff = player.mobBackoffCounts[ii] - 1; // TODO make configurable bleed // TODO use nonlinear algorithm? + if (newBackoff < 0) { + newBackoff = 0; + } + player.mobBackoffCounts[ii] = newBackoff; + } + // Paper end - per player mob spawning backoff + } + if (firstRunSpawnCounts) { + firstRunSpawnCounts = false; + _pufferfish_spawnCountsReady.set(true); + } + if (_pufferfish_spawnCountsReady.getAndSet(false)) { + net.minecraft.server.MinecraftServer.getServer().mobSpawnExecutor.submit(() -> { + int mapped = distanceManager.getNaturalSpawnChunkCount(); + io.papermc.paper.util.maplist.IteratorSafeOrderedReferenceSet.Iterator objectiterator = + level.entityTickList.entities.iterator(io.papermc.paper.util.maplist.IteratorSafeOrderedReferenceSet.ITERATOR_FLAG_SEE_ADDITIONS); + gg.pufferfish.pufferfish.util.IterableWrapper wrappedIterator = + new gg.pufferfish.pufferfish.util.IterableWrapper<>(objectiterator); + lastSpawnState = NaturalSpawner.createState(mapped, wrappedIterator, this::getFullChunk, null, true); + objectiterator.finishedIterating(); + _pufferfish_spawnCountsReady.set(true); + }); + } + } + // Pufferfish end } private void getFullChunk(long pos, Consumer chunkConsumer) { diff --git a/src/main/java/net/minecraft/server/level/ServerEntity.java b/src/main/java/net/minecraft/server/level/ServerEntity.java index a2279262c93408c11f5d2290b48fd794975e8cfe..ba1e0a61d3ecf73bfe962b5dfe892b4509278ae8 100644 --- a/src/main/java/net/minecraft/server/level/ServerEntity.java +++ b/src/main/java/net/minecraft/server/level/ServerEntity.java @@ -191,6 +191,7 @@ public class ServerEntity { boolean flag6 = k < -32768L || k > 32767L || l < -32768L || l > 32767L || i1 < -32768L || i1 > 32767L; if (!this.forceStateResync && !flag6 && this.teleportDelay <= 400 && !this.wasRiding && this.wasOnGround == this.entity.onGround()&& !(io.papermc.paper.configuration.GlobalConfiguration.get().collisions.sendFullPosForHardCollidingEntities && this.entity.hardCollides())) { // Paper - send full pos for hard colliding entities to prevent collision problems due to desync + if (flag2 || flag3 || this.entity instanceof AbstractArrow) { // Pufferfish if ((!flag2 || !flag3) && !(this.entity instanceof AbstractArrow)) { if (flag2) { packet1 = new ClientboundMoveEntityPacket.Pos(this.entity.getId(), (short) ((int) k), (short) ((int) l), (short) ((int) i1), this.entity.onGround()); @@ -204,6 +205,7 @@ public class ServerEntity { flag4 = true; flag5 = true; } + } // Pufferfish } else { this.wasOnGround = this.entity.onGround(); this.teleportDelay = 0; diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java index ca56a0b596976448da6bb2a0e82b3d5cd4133e12..377ca8c01864d6817eff30d33403bc15bf85e6d4 100644 --- a/src/main/java/net/minecraft/server/level/ServerLevel.java +++ b/src/main/java/net/minecraft/server/level/ServerLevel.java @@ -900,6 +900,7 @@ public class ServerLevel extends Level implements WorldGenLevel { org.spigotmc.ActivationRange.activateEntities(this); // Spigot this.timings.entityTick.startTiming(); // Spigot this.entityTickList.forEach((entity) -> { + entity.activatedPriorityReset = false; // Pufferfish - DAB if (!entity.isRemoved()) { if (false && this.shouldDiscardEntity(entity)) { // CraftBukkit - We prevent spawning in general, so this butchering is not needed entity.discard(); @@ -919,7 +920,20 @@ public class ServerLevel extends Level implements WorldGenLevel { } gameprofilerfiller.push("tick"); - this.guardEntityTick(this::tickNonPassenger, entity); + // Pufferfish start - copied from this.guardEntityTick + try { + this.tickNonPassenger(entity); // Pufferfish - changed + MinecraftServer.getServer().executeMidTickTasks(); // Tuinity - execute chunk tasks mid tick + } catch (Throwable throwable) { + if (throwable instanceof ThreadDeath) throw throwable; // Paper + // Paper start - Prevent tile entity and entity crashes + final String msg = String.format("Entity threw exception at %s:%s,%s,%s", entity.level().getWorld().getName(), entity.getX(), entity.getY(), entity.getZ()); + MinecraftServer.LOGGER.error(msg, throwable); + getCraftServer().getPluginManager().callEvent(new com.destroystokyo.paper.event.server.ServerExceptionEvent(new com.destroystokyo.paper.exception.ServerInternalException(msg, throwable))); + entity.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.DISCARD); + // Paper end + } + // Pufferfish end gameprofilerfiller.pop(); } } @@ -984,9 +998,11 @@ public class ServerLevel extends Level implements WorldGenLevel { } // Paper start - optimise random block ticking private final BlockPos.MutableBlockPos chunkTickMutablePosition = new BlockPos.MutableBlockPos(); - private final io.papermc.paper.util.math.ThreadUnsafeRandom randomTickRandom = new io.papermc.paper.util.math.ThreadUnsafeRandom(this.random.nextLong()); + // private final io.papermc.paper.util.math.ThreadUnsafeRandom randomTickRandom = new io.papermc.paper.util.math.ThreadUnsafeRandom(); // Pufferfish - moved to super // Paper end + private int currentIceAndSnowTick = 0; protected void resetIceAndSnowTick() { this.currentIceAndSnowTick = this.randomTickRandom.nextInt(16); } // Pufferfish + public void tickChunk(LevelChunk chunk, int randomTickSpeed) { ChunkPos chunkcoordintpair = chunk.getPos(); boolean flag = this.isRaining(); @@ -997,7 +1013,7 @@ public class ServerLevel extends Level implements WorldGenLevel { gameprofilerfiller.push("thunder"); 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 + if (!this.paperConfig().environment.disableThunder && flag && this.isThundering() && this.spigotConfig.thunderChance > 0 && /*this.random.nextInt(this.spigotConfig.thunderChance) == 0 &&*/ chunk.shouldDoLightning(this.random)) { // Spigot // Paper - Option to disable thunder // Pufferfish - replace random with shouldDoLightning blockposition.set(this.findLightningTargetAround(this.getBlockRandomPos(j, 0, k, 15))); // Paper if (this.isRainingAt(blockposition)) { diff --git a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java index 4ae88bfcead40cd05f9514a48a922a37767cb3cf..24257e653672a7632bca4f9eb55f127588604498 100644 --- a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java +++ b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java @@ -1154,6 +1154,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl @Override public void handleEditBook(ServerboundEditBookPacket packet) { + if (!gg.pufferfish.pufferfish.PufferfishConfig.enableBooks && !this.player.getBukkitEntity().hasPermission("pufferfish.usebooks")) return; // Pufferfish // Paper start - Book size limits if (!this.cserver.isPrimaryThread()) { List pageList = packet.pages(); diff --git a/src/main/java/net/minecraft/world/CompoundContainer.java b/src/main/java/net/minecraft/world/CompoundContainer.java index 241fec02e6869c638d3a160819b32173a081467b..e1caa652db6035c12918dad2fcbc7f9df658e92d 100644 --- a/src/main/java/net/minecraft/world/CompoundContainer.java +++ b/src/main/java/net/minecraft/world/CompoundContainer.java @@ -1,5 +1,6 @@ package net.minecraft.world; +import net.minecraft.core.Direction; // Pufferfish import net.minecraft.world.entity.player.Player; import net.minecraft.world.item.ItemStack; @@ -64,6 +65,23 @@ public class CompoundContainer implements Container { this.container2 = second; } + // Pufferfish start + @Override + public boolean hasEmptySlot(Direction enumdirection) { + return this.container1.hasEmptySlot(null) || this.container2.hasEmptySlot(null); + } + + @Override + public boolean isCompletelyFull(Direction enumdirection) { + return this.container1.isCompletelyFull(null) && this.container2.isCompletelyFull(null); + } + + @Override + public boolean isCompletelyEmpty(Direction enumdirection) { + return this.container1.isCompletelyEmpty(null) && this.container2.isCompletelyEmpty(null); + } + // Pufferfish end + @Override public int getContainerSize() { return this.container1.getContainerSize() + this.container2.getContainerSize(); diff --git a/src/main/java/net/minecraft/world/Container.java b/src/main/java/net/minecraft/world/Container.java index f402dbbfe3a443e6bc51f88b85abe937852b52f0..c960ddfe628051257cca0d25da385c1da09d505e 100644 --- a/src/main/java/net/minecraft/world/Container.java +++ b/src/main/java/net/minecraft/world/Container.java @@ -3,6 +3,7 @@ package net.minecraft.world; import java.util.Set; import java.util.function.Predicate; import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; // Pufferfish import net.minecraft.world.entity.player.Player; import net.minecraft.world.item.Item; import net.minecraft.world.item.ItemStack; @@ -14,6 +15,63 @@ import org.bukkit.craftbukkit.entity.CraftHumanEntity; // CraftBukkit end public interface Container extends Clearable { + // Pufferfish start - allow the inventory to override and optimize these frequent calls + default boolean hasEmptySlot(@org.jetbrains.annotations.Nullable Direction enumdirection) { // there is a slot with 0 items in it + if (this instanceof WorldlyContainer worldlyContainer) { + for (int i : worldlyContainer.getSlotsForFace(enumdirection)) { + if (this.getItem(i).isEmpty()) { + return true; + } + } + } else { + int size = this.getContainerSize(); + for (int i = 0; i < size; i++) { + if (this.getItem(i).isEmpty()) { + return true; + } + } + } + return false; + } + + default boolean isCompletelyFull(@org.jetbrains.annotations.Nullable Direction enumdirection) { // every stack is maxed + if (this instanceof WorldlyContainer worldlyContainer) { + for (int i : worldlyContainer.getSlotsForFace(enumdirection)) { + ItemStack itemStack = this.getItem(i); + if (itemStack.getCount() < itemStack.getMaxStackSize()) { + return false; + } + } + } else { + int size = this.getContainerSize(); + for (int i = 0; i < size; i++) { + ItemStack itemStack = this.getItem(i); + if (itemStack.getCount() < itemStack.getMaxStackSize()) { + return false; + } + } + } + return true; + } + + default boolean isCompletelyEmpty(@org.jetbrains.annotations.Nullable Direction enumdirection) { + if (this instanceof WorldlyContainer worldlyContainer) { + for (int i : worldlyContainer.getSlotsForFace(enumdirection)) { + if (!this.getItem(i).isEmpty()) { + return false; + } + } + } else { + int size = this.getContainerSize(); + for (int i = 0; i < size; i++) { + if (!this.getItem(i).isEmpty()) { + return false; + } + } + } + return true; + } + // Pufferfish end float DEFAULT_DISTANCE_BUFFER = 4.0F; diff --git a/src/main/java/net/minecraft/world/entity/Entity.java b/src/main/java/net/minecraft/world/entity/Entity.java index 7ef9f67d27cc240191dd5d07e8dcf5fbdebe1049..8ae15962f4f52e436041731094f62550fdae2b73 100644 --- a/src/main/java/net/minecraft/world/entity/Entity.java +++ b/src/main/java/net/minecraft/world/entity/Entity.java @@ -311,7 +311,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess public double yo; public double zo; private Vec3 position; - private BlockPos blockPosition; + public BlockPos blockPosition; // Pufferfish - private->public private ChunkPos chunkPosition; private Vec3 deltaMovement; private float yRot; @@ -425,6 +425,9 @@ 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 boolean activatedPriorityReset = false; // Pufferfish - DAB + public int activatedPriority = gg.pufferfish.pufferfish.PufferfishConfig.maximumActivationPrio; // Pufferfish - DAB (golf score) + public final BlockPos.MutableBlockPos cachedBlockPos = new BlockPos.MutableBlockPos(); // Pufferfish - reduce entity allocations public void setOrigin(@javax.annotation.Nonnull Location location) { this.origin = location.toVector(); @@ -863,6 +866,12 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess } public void tick() { + // Pufferfish start - entity TTL + if (type != EntityType.PLAYER && type.ttl >= 0 && this.tickCount >= type.ttl) { + discard(); + return; + } + // Pufferfish end - entity TTL this.baseTick(); } @@ -4483,16 +4492,18 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess } public boolean updateFluidHeightAndDoFluidPushing(TagKey tag, double speed) { - if (this.touchingUnloadedChunk()) { + if (false && this.touchingUnloadedChunk()) { // Pufferfish - cost of a lookup here is the same cost as below, so skip return false; } else { AABB axisalignedbb = this.getBoundingBox().deflate(0.001D); - int i = Mth.floor(axisalignedbb.minX); - int j = Mth.ceil(axisalignedbb.maxX); - int k = Mth.floor(axisalignedbb.minY); - int l = Mth.ceil(axisalignedbb.maxY); - int i1 = Mth.floor(axisalignedbb.minZ); - int j1 = Mth.ceil(axisalignedbb.maxZ); + // Pufferfish start - rename + int minBlockX = Mth.floor(axisalignedbb.minX); + int maxBlockX = Mth.ceil(axisalignedbb.maxX); + int minBlockY = Mth.floor(axisalignedbb.minY); + int maxBlockY = Mth.ceil(axisalignedbb.maxY); + int minBlockZ = Mth.floor(axisalignedbb.minZ); + int maxBlockZ = Mth.ceil(axisalignedbb.maxZ); + // Pufferfish end double d1 = 0.0D; boolean flag = this.isPushedByFluid(); boolean flag1 = false; @@ -4500,14 +4511,61 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess int k1 = 0; BlockPos.MutableBlockPos blockposition_mutableblockposition = new BlockPos.MutableBlockPos(); - for (int l1 = i; l1 < j; ++l1) { - for (int i2 = k; i2 < l; ++i2) { - for (int j2 = i1; j2 < j1; ++j2) { - blockposition_mutableblockposition.set(l1, i2, j2); - FluidState fluid = this.level().getFluidState(blockposition_mutableblockposition); + // Pufferfish start - based off CollisionUtil.getCollisionsForBlocksOrWorldBorder + final int minSection = io.papermc.paper.util.WorldUtil.getMinSection(this.level()); + final int maxSection = io.papermc.paper.util.WorldUtil.getMaxSection(this.level()); + final int minBlock = minSection << 4; + final int maxBlock = (maxSection << 4) | 15; + + // special cases: + if (minBlockY > maxBlock || maxBlockY < minBlock) { + // no point in checking + return false; + } + + int minYIterate = Math.max(minBlock, minBlockY); + int maxYIterate = Math.min(maxBlock, maxBlockY); + + int minChunkX = minBlockX >> 4; + int maxChunkX = maxBlockX >> 4; + + int minChunkZ = minBlockZ >> 4; + int maxChunkZ = maxBlockZ >> 4; + + for (int currChunkZ = minChunkZ; currChunkZ <= maxChunkZ; ++currChunkZ) { + int minZ = currChunkZ == minChunkZ ? minBlockZ & 15 : 0; // coordinate in chunk + int maxZ = currChunkZ == maxChunkZ ? maxBlockZ & 15 : 16; // coordinate in chunk + + for (int currChunkX = minChunkX; currChunkX <= maxChunkX; ++currChunkX) { + int minX = currChunkX == minChunkX ? minBlockX & 15 : 0; // coordinate in chunk + int maxX = currChunkX == maxChunkX ? maxBlockX & 15 : 16; // coordinate in chunk + + net.minecraft.world.level.chunk.ChunkAccess chunk = this.level().getChunkIfLoadedImmediately(currChunkX, currChunkZ); + if (chunk == null) { + return false; // if we're touching an unloaded chunk then it's false + } + + net.minecraft.world.level.chunk.LevelChunkSection[] sections = chunk.getSections(); + + for (int currY = minYIterate; currY < maxYIterate; ++currY) { + net.minecraft.world.level.chunk.LevelChunkSection section = sections[(currY >> 4) - minSection]; + + if (section == null || section.hasOnlyAir() || section.fluidStateCount == 0) { // if no fluids, nothing in this section + // empty + // skip to next section + currY = (currY & ~(15)) + 15; // increment by 15: iterator loop increments by the extra one + continue; + } + + net.minecraft.world.level.chunk.PalettedContainer blocks = section.states; + + for (int currZ = minZ; currZ < maxZ; ++currZ) { + for (int currX = minX; currX < maxX; ++currX) { + FluidState fluid = blocks.get(currX & 15, currY & 15, currZ & 15).getFluidState(); if (fluid.is(tag)) { - double d2 = (double) ((float) i2 + fluid.getHeight(this.level(), blockposition_mutableblockposition)); + blockposition_mutableblockposition.set((currChunkX << 4) + currX, currY, (currChunkZ << 4) + currZ); + double d2 = (double) ((float) currY + fluid.getHeight(this.level(), blockposition_mutableblockposition)); if (d2 >= axisalignedbb.minY) { flag1 = true; @@ -4529,9 +4587,12 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess // CraftBukkit end } } + } + } } } } + // Pufferfish end if (vec3d.length() > 0.0D) { if (k1 > 0) { diff --git a/src/main/java/net/minecraft/world/entity/EntityType.java b/src/main/java/net/minecraft/world/entity/EntityType.java index a46bf73c608641bf1f00fd55242de71a0f2ee06e..e6edbe6177b168d85759bd9c414dc87ea8a394fe 100644 --- a/src/main/java/net/minecraft/world/entity/EntityType.java +++ b/src/main/java/net/minecraft/world/entity/EntityType.java @@ -316,6 +316,8 @@ public class EntityType implements FeatureElement, EntityTypeT private final boolean canSpawnFarFromPlayer; private final int clientTrackingRange; private final int updateInterval; + public boolean dabEnabled = false; // Pufferfish + public int ttl = -1; // Pufferfish @Nullable private String descriptionId; @Nullable diff --git a/src/main/java/net/minecraft/world/entity/LivingEntity.java b/src/main/java/net/minecraft/world/entity/LivingEntity.java index ce01fe82dc1eaaf06ca317ddbc62b7d1b87a48b2..60d8fddfecdcd0fe3609a3851230aa06ce5297a0 100644 --- a/src/main/java/net/minecraft/world/entity/LivingEntity.java +++ b/src/main/java/net/minecraft/world/entity/LivingEntity.java @@ -150,7 +150,6 @@ import org.bukkit.event.entity.EntityTeleportEvent; import org.bukkit.event.player.PlayerItemConsumeEvent; // CraftBukkit end -import co.aikar.timings.MinecraftTimings; // Paper public abstract class LivingEntity extends Entity implements Attackable { @@ -427,7 +426,7 @@ public abstract class LivingEntity extends Entity implements Attackable { boolean flag = this instanceof net.minecraft.world.entity.player.Player; if (!this.level().isClientSide) { - if (this.isInWall()) { + if (shouldCheckForSuffocation() && this.isInWall()) { // Pufferfish - optimize suffocation this.hurt(this.damageSources().inWall(), 1.0F); } else if (flag && !this.level().getWorldBorder().isWithinBounds(this.getBoundingBox())) { double d0 = this.level().getWorldBorder().getDistanceToBorder(this) + this.level().getWorldBorder().getDamageSafeZone(); @@ -1431,6 +1430,19 @@ public abstract class LivingEntity extends Entity implements Attackable { return this.getHealth() <= 0.0F; } + // Pufferfish start - optimize suffocation + public boolean couldPossiblyBeHurt(float amount) { + if ((float) this.invulnerableTime > (float) this.invulnerableDuration / 2.0F && amount <= this.lastHurt) { + return false; + } + return true; + } + + public boolean shouldCheckForSuffocation() { + return !gg.pufferfish.pufferfish.PufferfishConfig.enableSuffocationOptimization || (tickCount % 10 == 0 && couldPossiblyBeHurt(1.0F)); + } + // Pufferfish end + @Override public boolean hurt(DamageSource source, float amount) { if (this.isInvulnerableTo(source)) { @@ -2040,6 +2052,20 @@ public abstract class LivingEntity extends Entity implements Attackable { return this.lastClimbablePos; } + + // Pufferfish start + private boolean cachedOnClimable = false; + private BlockPos lastClimbingPosition = null; + + public boolean onClimableCached() { + if (!this.blockPosition().equals(this.lastClimbingPosition)) { + this.cachedOnClimable = this.onClimbable(); + this.lastClimbingPosition = this.blockPosition(); + } + return this.cachedOnClimable; + } + // Pufferfish end + public boolean onClimbable() { if (this.isSpectator()) { return false; diff --git a/src/main/java/net/minecraft/world/entity/Mob.java b/src/main/java/net/minecraft/world/entity/Mob.java index 8b612b772ca87c852d0b108c2afd6785c261c9b9..5e42bfa9e98215ecb8aca4da2486a4b3d94b38aa 100644 --- a/src/main/java/net/minecraft/world/entity/Mob.java +++ b/src/main/java/net/minecraft/world/entity/Mob.java @@ -237,14 +237,16 @@ public abstract class Mob extends LivingEntity implements EquipmentUser, Targeti return this.lookControl; } + int _pufferfish_inactiveTickDisableCounter = 0; // Pufferfish - throttle inactive goal selector ticking // Paper start @Override public void inactiveTick() { super.inactiveTick(); - if (this.goalSelector.inactiveTick()) { + boolean isThrottled = gg.pufferfish.pufferfish.PufferfishConfig.throttleInactiveGoalSelectorTick && _pufferfish_inactiveTickDisableCounter++ % 20 != 0; // Pufferfish - throttle inactive goal selector ticking + if (this.goalSelector.inactiveTick(this.activatedPriority, true) && !isThrottled) { // Pufferfish - pass activated priroity // Pufferfish - throttle inactive goal selector ticking this.goalSelector.tick(); } - if (this.targetSelector.inactiveTick()) { + if (this.targetSelector.inactiveTick(this.activatedPriority, true)) { // Pufferfish - pass activated priority this.targetSelector.tick(); } } @@ -972,16 +974,20 @@ public abstract class Mob extends LivingEntity implements EquipmentUser, Targeti if (i % 2 != 0 && this.tickCount > 1) { gameprofilerfiller.push("targetSelector"); + if (this.targetSelector.inactiveTick(this.activatedPriority, false)) // Pufferfish - use this to alternate ticking this.targetSelector.tickRunningGoals(false); gameprofilerfiller.pop(); gameprofilerfiller.push("goalSelector"); + if (this.goalSelector.inactiveTick(this.activatedPriority, false)) // Pufferfish - use this to alternate ticking this.goalSelector.tickRunningGoals(false); gameprofilerfiller.pop(); } else { gameprofilerfiller.push("targetSelector"); + if (this.targetSelector.inactiveTick(this.activatedPriority, false)) // Pufferfish - use this to alternate ticking this.targetSelector.tick(); gameprofilerfiller.pop(); gameprofilerfiller.push("goalSelector"); + if (this.goalSelector.inactiveTick(this.activatedPriority, false)) // Pufferfish - use this to alternate ticking this.goalSelector.tick(); gameprofilerfiller.pop(); } 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..bc178967affd21ad04b83ea26639a2dd9b497454 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,9 +22,11 @@ public class AttributeMap { private final Map, AttributeInstance> attributes = new Object2ObjectOpenHashMap<>(); private final Set dirtyAttributes = new ObjectOpenHashSet<>(); private final AttributeSupplier supplier; + private final java.util.function.Function, AttributeInstance> createInstance; // Pufferfish public AttributeMap(AttributeSupplier defaultAttributes) { this.supplier = defaultAttributes; + this.createInstance = attributex -> this.supplier.createInstance(this::onAttributeModified, attributex); // Pufferfish } private void onAttributeModified(AttributeInstance instance) { @@ -43,7 +45,7 @@ public class AttributeMap { @Nullable public AttributeInstance getInstance(Holder attribute) { - return this.attributes.computeIfAbsent(attribute, attributex -> this.supplier.createInstance(this::onAttributeModified, attributex)); + return this.attributes.computeIfAbsent(attribute, this.createInstance); // Pufferfish - cache lambda, as for some reason java allocates it anyways } public boolean hasAttribute(Holder attribute) { diff --git a/src/main/java/net/minecraft/world/entity/ai/behavior/VillagerPanicTrigger.java b/src/main/java/net/minecraft/world/entity/ai/behavior/VillagerPanicTrigger.java index 758f62416ca9c02351348ac0d41deeb4624abc0e..69130969c9a434ec2361e573c9a1ec9f462dfda2 100644 --- a/src/main/java/net/minecraft/world/entity/ai/behavior/VillagerPanicTrigger.java +++ b/src/main/java/net/minecraft/world/entity/ai/behavior/VillagerPanicTrigger.java @@ -36,7 +36,11 @@ public class VillagerPanicTrigger extends Behavior { @Override protected void tick(ServerLevel world, Villager entity, long time) { - if (time % 100L == 0L) { + // Pufferfish start + if (entity.nextGolemPanic < 0) entity.nextGolemPanic = time + 100; + if (--entity.nextGolemPanic < time) { + entity.nextGolemPanic = -1; + // Pufferfish end entity.spawnGolemIfNeeded(world, time, 3); } } 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..59e1014cb7604e7a56096284be24a33f87fb580b 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 @@ -43,9 +43,11 @@ public class GoalSelector { } // Paper start - public boolean inactiveTick() { + public boolean inactiveTick(int tickRate, boolean inactive) { // Pufferfish start + if (inactive && !gg.pufferfish.pufferfish.PufferfishConfig.dearEnabled) tickRate = 3; // reset to Paper's this.curRate++; - return this.curRate % 3 == 0; // TODO newGoalRate was already unused in 1.20.4, check if this is correct + return this.curRate % tickRate == 0; // TODO newGoalRate was already unused in 1.20.4, check if this is correct + // Pufferfish end } public boolean hasTasks() { for (WrappedGoal task : this.availableGoals) { diff --git a/src/main/java/net/minecraft/world/entity/ai/goal/MoveToBlockGoal.java b/src/main/java/net/minecraft/world/entity/ai/goal/MoveToBlockGoal.java index aee0147649d458b87d92496eda0c1723ebe570d2..89e9ea999d2fbd81a1d74382ef3fcd675fc8b94e 100644 --- a/src/main/java/net/minecraft/world/entity/ai/goal/MoveToBlockGoal.java +++ b/src/main/java/net/minecraft/world/entity/ai/goal/MoveToBlockGoal.java @@ -121,6 +121,7 @@ public abstract class MoveToBlockGoal extends Goal { for (int m = 0; m <= l; m = m > 0 ? -m : 1 - m) { for (int n = m < l && m > -l ? l : 0; n <= l; n = n > 0 ? -n : 1 - n) { mutableBlockPos.setWithOffset(blockPos, m, k - 1, n); + if (!this.mob.level().hasChunkAt(mutableBlockPos)) continue; // Pufferfish - if this block isn't loaded, continue if (this.mob.isWithinRestriction(mutableBlockPos) && this.isValidTarget(this.mob.level(), mutableBlockPos)) { this.blockPos = mutableBlockPos; this.mob.movingTarget = mutableBlockPos == BlockPos.ZERO ? null : mutableBlockPos.immutable(); // Paper 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..28cff997a1b263784e245f692adbff2a888a2d53 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 @@ -76,9 +76,18 @@ public class TargetingConditions { } if (this.range > 0.0) { - double d = this.testInvisible ? targetEntity.getVisibilityPercent(baseEntity) : 1.0; - double e = Math.max((this.useFollowRange ? this.getFollowRange(baseEntity) : this.range) * d, 2.0); // Paper - Fix MC-145656 + // Pufferfish start - check range before getting visibility + // d = invisibility percent, e = follow range adjusted for invisibility, f = distance double f = baseEntity.distanceToSqr(targetEntity.getX(), targetEntity.getY(), targetEntity.getZ()); + double followRangeRaw = this.useFollowRange ? this.getFollowRange(baseEntity) : this.range; + + if (f > followRangeRaw * followRangeRaw) { // the actual follow range will always be this value or smaller, so if the distance is larger then it never will return true after getting invis + return false; + } + + double d = this.testInvisible ? targetEntity.getVisibilityPercent(baseEntity) : 1.0; + double e = Math.max((followRangeRaw) * d, 2.0); // Paper - Fix MC-145656 + // Pufferfish end if (f > e * e) { return false; } 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..e44af851263f27aa0009b14a60bb2d0642a5ce74 100644 --- a/src/main/java/net/minecraft/world/entity/ambient/Bat.java +++ b/src/main/java/net/minecraft/world/entity/ambient/Bat.java @@ -239,13 +239,22 @@ public class Bat extends AmbientCreature { } } + // Pufferfish start - only check for spooky season once an hour + private static boolean isSpookySeason = false; + private static final int ONE_HOUR = 20 * 60 * 60; + private static int lastSpookyCheck = -ONE_HOUR; private static boolean isHalloween() { + if (net.minecraft.server.MinecraftServer.currentTick - lastSpookyCheck > ONE_HOUR) { LocalDate localdate = LocalDate.now(); int i = localdate.get(ChronoField.DAY_OF_MONTH); int j = localdate.get(ChronoField.MONTH_OF_YEAR); - return j == 10 && i >= 20 || j == 11 && i <= 3; + isSpookySeason = j == 10 && i >= 20 || j == 11 && i <= 3; + lastSpookyCheck = net.minecraft.server.MinecraftServer.currentTick; + } + return isSpookySeason; } + // Pufferfish end private void setupAnimationStates() { if (this.isResting()) { 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 991e3274091c4e25eebc6debd44653e5b566eedb..789d3469ce4983868e4b880b6053bd2d8c33a10a 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 @@ -215,9 +215,11 @@ public class Allay extends PathfinderMob implements InventoryCarrier, VibrationS return 0.4F; } + private int behaviorTick = 0; // Pufferfish @Override protected void customServerAiStep() { this.level().getProfiler().push("allayBrain"); + if (this.behaviorTick++ % this.activatedPriority == 0) // Pufferfish this.getBrain().tick((ServerLevel) this.level(), this); this.level().getProfiler().pop(); this.level().getProfiler().push("allayActivityUpdate"); 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..98766696556b520cf565ccadc8dba207832b4ae3 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 @@ -268,9 +268,11 @@ public class Axolotl extends Animal implements LerpingModel, VariantHolder> { .ifPresent(this::setVariant); } + private int behaviorTick = 0; // Pufferfish @Override protected void customServerAiStep() { this.level().getProfiler().push("frogBrain"); + if (this.behaviorTick++ % this.activatedPriority == 0) // Pufferfish this.getBrain().tick((ServerLevel)this.level(), this); this.level().getProfiler().pop(); this.level().getProfiler().push("frogActivityUpdate"); 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..0d34e9e0f7ce35c3c28a9216cf3cdd5eddfe90f6 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 @@ -83,9 +83,11 @@ public class Tadpole extends AbstractFish { return SoundEvents.TADPOLE_FLOP; } + private int behaviorTick = 0; // Pufferfish @Override protected void customServerAiStep() { this.level().getProfiler().push("tadpoleBrain"); + if (this.behaviorTick++ % this.activatedPriority == 0) // Pufferfish this.getBrain().tick((ServerLevel) this.level(), this); this.level().getProfiler().pop(); this.level().getProfiler().push("tadpoleActivityUpdate"); 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 376bcbc189008464f4d518c1e07643431ba96306..07bdea8a7d6706839a758afe0242202c7e841416 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 @@ -190,9 +190,11 @@ public class Goat extends Animal { return (Brain) super.getBrain(); // CraftBukkit - decompile error } + private int behaviorTick = 0; // Pufferfish @Override protected void customServerAiStep() { this.level().getProfiler().push("goatBrain"); + if (this.behaviorTick++ % this.activatedPriority == 0) // Pufferfish this.getBrain().tick((ServerLevel) this.level(), this); this.level().getProfiler().pop(); this.level().getProfiler().push("goatActivityUpdate"); 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..5040b1a7125dd9647a98fd812f558ec3a2171092 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 @@ -154,6 +154,13 @@ public class WitherBoss extends Monster implements PowerableMob, RangedAttackMob this.bossEvent.setName(this.getDisplayName()); } + // Pufferfish start - optimize suffocation + @Override + public boolean shouldCheckForSuffocation() { + return true; + } + // Pufferfish end + @Override protected SoundEvent getAmbientSound() { return SoundEvents.WITHER_AMBIENT; 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..9cf4e2f309e182c69e9592ac606c0ae85a1200b6 100644 --- a/src/main/java/net/minecraft/world/entity/monster/EnderMan.java +++ b/src/main/java/net/minecraft/world/entity/monster/EnderMan.java @@ -318,11 +318,17 @@ public class EnderMan extends Monster implements NeutralMob { private boolean teleport(double x, double y, double z) { BlockPos.MutableBlockPos blockposition_mutableblockposition = new BlockPos.MutableBlockPos(x, y, z); - while (blockposition_mutableblockposition.getY() > this.level().getMinBuildHeight() && !this.level().getBlockState(blockposition_mutableblockposition).blocksMotion()) { + // Pufferfish start - single chunk lookup + net.minecraft.world.level.chunk.LevelChunk chunk = this.level().getChunkIfLoaded(blockposition_mutableblockposition); + if (chunk == null) { + return false; + } + // Pufferfish end + while (blockposition_mutableblockposition.getY() > this.level().getMinBuildHeight() && !chunk.getBlockState(blockposition_mutableblockposition).blocksMotion()) { // Pufferfish blockposition_mutableblockposition.move(Direction.DOWN); } - BlockState iblockdata = this.level().getBlockState(blockposition_mutableblockposition); + BlockState iblockdata = chunk.getBlockState(blockposition_mutableblockposition); // Pufferfish boolean flag = iblockdata.blocksMotion(); boolean flag1 = iblockdata.getFluidState().is(FluidTags.WATER); 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..6f8a22c51ba5e2713dcdfc61c61b35123f9bf326 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 @@ -153,9 +153,11 @@ public class Hoglin extends Animal implements Enemy, HoglinBase { return (Brain)super.getBrain(); } + private int behaviorTick; // Pufferfish @Override protected void customServerAiStep() { this.level().getProfiler().push("hoglinBrain"); + if (this.behaviorTick++ % this.activatedPriority == 0) // Pufferfish this.getBrain().tick((ServerLevel)this.level(), this); this.level().getProfiler().pop(); HoglinAi.updateActivity(this); 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..5878a6ecadc2b9f7ac9f70622a8c85e2940e139a 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 @@ -294,9 +294,11 @@ public class Piglin extends AbstractPiglin implements CrossbowAttackMob, Invento return !this.cannotHunt; } + private int behaviorTick; // Pufferfish @Override protected void customServerAiStep() { this.level().getProfiler().push("piglinBrain"); + if (this.behaviorTick++ % this.activatedPriority == 0) // Pufferfish this.getBrain().tick((ServerLevel) this.level(), this); this.level().getProfiler().pop(); PiglinAi.updateActivity(this); 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..9cf56f2ada025aae0710099bcc3b5c62fd7bbb9e 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 @@ -271,11 +271,13 @@ public class Warden extends Monster implements VibrationSystem { } + private int behaviorTick = 0; // Pufferfish @Override protected void customServerAiStep() { ServerLevel worldserver = (ServerLevel) this.level(); worldserver.getProfiler().push("wardenBrain"); + if (this.behaviorTick++ % this.activatedPriority == 0) // Pufferfish this.getBrain().tick(worldserver, this); this.level().getProfiler().pop(); super.customServerAiStep(); 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..81c2fb02779d416933502abd416324ad4c7ef22b 100644 --- a/src/main/java/net/minecraft/world/entity/npc/Villager.java +++ b/src/main/java/net/minecraft/world/entity/npc/Villager.java @@ -143,6 +143,8 @@ public class Villager extends AbstractVillager implements ReputationEventHandler return holder.is(PoiTypes.MEETING); }); + public long nextGolemPanic = -1; // Pufferfish + public Villager(EntityType entityType, Level world) { this(entityType, world, VillagerType.PLAINS); } @@ -246,6 +248,7 @@ public class Villager extends AbstractVillager implements ReputationEventHandler } // Spigot End + private int behaviorTick = 0; // Pufferfish @Override @Deprecated // Paper protected void customServerAiStep() { @@ -255,7 +258,11 @@ public class Villager extends AbstractVillager implements ReputationEventHandler protected void customServerAiStep(final boolean inactive) { // Paper end this.level().getProfiler().push("villagerBrain"); - if (!inactive) this.getBrain().tick((ServerLevel) this.level(), this); // Paper + // Pufferfish start + if (!inactive && this.behaviorTick++ % this.activatedPriority == 0) { + this.getBrain().tick((ServerLevel) this.level(), this); // Paper + } + // Pufferfish end this.level().getProfiler().pop(); if (this.assignProfessionWhenSpawned) { this.assignProfessionWhenSpawned = false; diff --git a/src/main/java/net/minecraft/world/entity/player/Inventory.java b/src/main/java/net/minecraft/world/entity/player/Inventory.java index ca7fbe4f8c1e1d2fb90095aa35be4dda3029c23e..38c9fc3b740fcfaacae8c39defe2627c7442ca96 100644 --- a/src/main/java/net/minecraft/world/entity/player/Inventory.java +++ b/src/main/java/net/minecraft/world/entity/player/Inventory.java @@ -643,6 +643,8 @@ public class Inventory implements Container, Nameable { } public boolean contains(ItemStack stack) { + // Pufferfish start - don't allocate iterators + /* Iterator iterator = this.compartments.iterator(); while (iterator.hasNext()) { @@ -657,11 +659,26 @@ public class Inventory implements Container, Nameable { } } } + */ + for (int i = 0; i < this.compartments.size(); i++) { + List list = this.compartments.get(i); + + for (int j = 0; j < list.size(); j++) { + ItemStack itemstack1 = list.get(j); + + if (!itemstack1.isEmpty() && ItemStack.isSameItemSameComponents(itemstack1, stack)) { + return true; + } + } + } + // Pufferfish end return false; } public boolean contains(TagKey tag) { + // Pufferfish start - don't allocate iterators + /* Iterator iterator = this.compartments.iterator(); while (iterator.hasNext()) { @@ -676,11 +693,26 @@ public class Inventory implements Container, Nameable { } } } + */ + for (int i = 0; i < this.compartments.size(); i++) { + List list = this.compartments.get(i); + + for (int j = 0; j < list.size(); j++) { + ItemStack itemstack = list.get(j); + + if (!itemstack.isEmpty() && itemstack.is(tag)) { + return true; + } + } + } + // Pufferfish end return false; } public boolean contains(Predicate predicate) { + // Pufferfish start - don't allocate iterators + /* Iterator iterator = this.compartments.iterator(); while (iterator.hasNext()) { @@ -695,6 +727,19 @@ public class Inventory implements Container, Nameable { } } } + */ + for (int i = 0; i < this.compartments.size(); i++) { + List list = this.compartments.get(i); + + for (int j = 0; j < list.size(); j++) { + ItemStack itemstack = list.get(j); + + if (predicate.test(itemstack)) { + return true; + } + } + } + // Pufferfish end return false; } 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..0e4d180d257d7180a8e1300f35924d0c52ce421a 100644 --- a/src/main/java/net/minecraft/world/entity/projectile/Projectile.java +++ b/src/main/java/net/minecraft/world/entity/projectile/Projectile.java @@ -46,6 +46,36 @@ public abstract class Projectile extends Entity implements TraceableEntity { super(type, world); } + // Pufferfish start + private static int loadedThisTick = 0; + private static int loadedTick; + + private int loadedLifetime = 0; + @Override + public void setPos(double x, double y, double z) { + int currentTick = net.minecraft.server.MinecraftServer.currentTick; + if (loadedTick != currentTick) { + loadedTick = currentTick; + loadedThisTick = 0; + } + int previousX = Mth.floor(this.getX()) >> 4, previousZ = Mth.floor(this.getZ()) >> 4; + int newX = Mth.floor(x) >> 4, newZ = Mth.floor(z) >> 4; + if (previousX != newX || previousZ != newZ) { + boolean isLoaded = ((net.minecraft.server.level.ServerChunkCache) this.level().getChunkSource()).getChunkAtIfLoadedMainThread(newX, newZ) != null; + if (!isLoaded) { + if (Projectile.loadedThisTick > gg.pufferfish.pufferfish.PufferfishConfig.maxProjectileLoadsPerTick) { + if (++this.loadedLifetime > gg.pufferfish.pufferfish.PufferfishConfig.maxProjectileLoadsPerProjectile) { + this.discard(); + } + return; + } + Projectile.loadedThisTick++; + } + } + super.setPos(x, y, z); + } + // Pufferfish end + public void setOwner(@Nullable Entity entity) { if (entity != null) { this.ownerUUID = entity.getUUID(); diff --git a/src/main/java/net/minecraft/world/entity/vehicle/AbstractMinecartContainer.java b/src/main/java/net/minecraft/world/entity/vehicle/AbstractMinecartContainer.java index 9549eee0d92f322bd5232abd7e695213660c2e22..2cd8ac8840c2c0346fbf209c5ae3bb67eb3905e8 100644 --- a/src/main/java/net/minecraft/world/entity/vehicle/AbstractMinecartContainer.java +++ b/src/main/java/net/minecraft/world/entity/vehicle/AbstractMinecartContainer.java @@ -30,7 +30,10 @@ import org.bukkit.inventory.InventoryHolder; public abstract class AbstractMinecartContainer extends AbstractMinecart implements ContainerEntity { + // Pufferfish start private NonNullList itemStacks; + private gg.airplane.structs.ItemListWithBitset itemStacksOptimized; + // Pufferfish end @Nullable public ResourceKey lootTable; public long lootTableSeed; @@ -86,12 +89,18 @@ public abstract class AbstractMinecartContainer extends AbstractMinecart impleme protected AbstractMinecartContainer(EntityType type, Level world) { super(type, world); - this.itemStacks = NonNullList.withSize(this.getContainerSize(), ItemStack.EMPTY); // CraftBukkit - SPIGOT-3513 + // Pufferfish start + this.itemStacksOptimized = new gg.airplane.structs.ItemListWithBitset(this.getContainerSize()); // CraftBukkit - SPIGOT-3513 + this.itemStacks = this.itemStacksOptimized.nonNullList; + // Pufferfish end } protected AbstractMinecartContainer(EntityType type, double x, double y, double z, Level world) { super(type, world, x, y, z); - this.itemStacks = NonNullList.withSize(this.getContainerSize(), ItemStack.EMPTY); // CraftBukkit - SPIGOT-3513 + // Pufferfish start + this.itemStacksOptimized = new gg.airplane.structs.ItemListWithBitset(this.getContainerSize()); // CraftBukkit - SPIGOT-3513 + this.itemStacks = this.itemStacksOptimized.nonNullList; + // Pufferfish end } @Override @@ -158,6 +167,10 @@ public abstract class AbstractMinecartContainer extends AbstractMinecart impleme @Override protected void readAdditionalSaveData(CompoundTag nbt) { super.readAdditionalSaveData(nbt); + // Pufferfish start + this.itemStacksOptimized = new gg.airplane.structs.ItemListWithBitset(this.getContainerSize()); + this.itemStacks = this.itemStacksOptimized.nonNullList; + // Pufferfish end this.readChestVehicleSaveData(nbt, this.registryAccess()); } diff --git a/src/main/java/net/minecraft/world/item/EndCrystalItem.java b/src/main/java/net/minecraft/world/item/EndCrystalItem.java index 5f51e64cb0611a4ba6bdcdcacbcba1063a7f3a5c..cc1e5882bee94864ad189d7f01ce78223411e51d 100644 --- a/src/main/java/net/minecraft/world/item/EndCrystalItem.java +++ b/src/main/java/net/minecraft/world/item/EndCrystalItem.java @@ -57,7 +57,7 @@ public class EndCrystalItem extends Item { world.gameEvent((Entity) context.getPlayer(), (Holder) GameEvent.ENTITY_PLACE, blockposition1); EndDragonFight enderdragonbattle = ((ServerLevel) world).getDragonFight(); - if (enderdragonbattle != null) { + if (enderdragonbattle != null && gg.pufferfish.pufferfish.PufferfishConfig.allowEndCrystalRespawn) { // Pufferfish enderdragonbattle.tryRespawn(aboveBlockPosition); // Paper - Perf: Do crystal-portal proximity check before entity lookup } } 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 3554109bcc4651ca93b6275c914e57e007e2204e..60c7a9520335de8e2638572100affcfdd776c5a3 100644 --- a/src/main/java/net/minecraft/world/item/crafting/ShapelessRecipe.java +++ b/src/main/java/net/minecraft/world/item/crafting/ShapelessRecipe.java @@ -27,8 +27,13 @@ public class ShapelessRecipe extends io.papermc.paper.inventory.recipe.RecipeBoo final CraftingBookCategory category; final ItemStack result; final NonNullList ingredients; + private final boolean isBukkit; // Pufferfish + // Pufferfish start public ShapelessRecipe(String group, CraftingBookCategory category, ItemStack result, NonNullList ingredients) { + this(group, category, result, ingredients, false); + } + public ShapelessRecipe(String group, CraftingBookCategory category, ItemStack result, NonNullList ingredients, boolean isBukkit) { this.isBukkit = isBukkit; // Pufferfish end this.group = group; this.category = category; this.result = result; @@ -78,6 +83,28 @@ 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])); + + inventory: for (int index = 0; index < inventory.getContainerSize(); index++) { + ItemStack itemStack = inventory.getItem(index); + + if (!itemStack.isEmpty()) { + for (int i = 0; i < ingredients.size(); i++) { + if (ingredients.get(i).test(itemStack)) { + ingredients.remove(i); + continue inventory; + } + } + return false; + } + } + + return ingredients.isEmpty(); + } + // Pufferfish end + StackedContents autorecipestackmanager = new StackedContents(); autorecipestackmanager.initialize(this); // Paper - better exact choice recipes int i = 0; diff --git a/src/main/java/net/minecraft/world/level/Level.java b/src/main/java/net/minecraft/world/level/Level.java index 14281a4e72f49dc4eb2ca3da8479c1f81a3a175d..4146bf151ffa5abf006f65a7290f6214dda3392f 100644 --- a/src/main/java/net/minecraft/world/level/Level.java +++ b/src/main/java/net/minecraft/world/level/Level.java @@ -204,6 +204,8 @@ public abstract class Level implements LevelAccessor, AutoCloseable { // Paper end 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 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 @@ -1300,13 +1302,13 @@ public abstract class Level implements LevelAccessor, AutoCloseable { try { tickConsumer.accept(entity); MinecraftServer.getServer().executeMidTickTasks(); // Paper - execute chunk tasks mid tick - } catch (Throwable throwable) { + } catch (Throwable throwable) { // Pufferfish - diff on change ServerLevel.tick if (throwable instanceof ThreadDeath) throw throwable; // Paper // Paper start - Prevent block entity and entity crashes final String msg = String.format("Entity threw exception at %s:%s,%s,%s", entity.level().getWorld().getName(), entity.getX(), entity.getY(), entity.getZ()); MinecraftServer.LOGGER.error(msg, throwable); getCraftServer().getPluginManager().callEvent(new com.destroystokyo.paper.event.server.ServerExceptionEvent(new com.destroystokyo.paper.exception.ServerInternalException(msg, throwable))); // Paper - ServerExceptionEvent - entity.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.DISCARD); + entity.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.DISCARD); // Pufferfish - diff on change ServerLevel.tick // Paper end - Prevent block entity and entity crashes } } @@ -1781,6 +1783,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable { } public ProfilerFiller getProfiler() { + if (gg.pufferfish.pufferfish.PufferfishConfig.disableMethodProfiler) return net.minecraft.util.profiling.InactiveProfiler.INSTANCE; // Pufferfish return (ProfilerFiller) this.profiler.get(); } diff --git a/src/main/java/net/minecraft/world/level/NaturalSpawner.java b/src/main/java/net/minecraft/world/level/NaturalSpawner.java index ed8032495af9ce9c23419224814b8d27e4a97c17..382c5773f773837f8182ee5d4f97f588c15eaacd 100644 --- a/src/main/java/net/minecraft/world/level/NaturalSpawner.java +++ b/src/main/java/net/minecraft/world/level/NaturalSpawner.java @@ -408,12 +408,12 @@ public final class NaturalSpawner { } } - private static BlockPos getRandomPosWithin(Level world, LevelChunk chunk) { + private static BlockPos getRandomPosWithin(ServerLevel world, LevelChunk chunk) { // Pufferfish - accept serverlevel ChunkPos chunkcoordintpair = chunk.getPos(); - int i = chunkcoordintpair.getMinBlockX() + world.random.nextInt(16); - int j = chunkcoordintpair.getMinBlockZ() + world.random.nextInt(16); + int i = chunkcoordintpair.getMinBlockX() + world.getThreadUnsafeRandom().nextInt(16); // Pufferfish - use thread unsafe random + int j = chunkcoordintpair.getMinBlockZ() + world.getThreadUnsafeRandom().nextInt(16); // Pufferfish int k = chunk.getHeight(Heightmap.Types.WORLD_SURFACE, i, j) + 1; - int l = Mth.randomBetweenInclusive(world.random, world.getMinBuildHeight(), k); + int l = Mth.randomBetweenInclusive(world.getThreadUnsafeRandom(), world.getMinBuildHeight(), k); // Pufferfish return new BlockPos(i, l, j); } diff --git a/src/main/java/net/minecraft/world/level/biome/Biome.java b/src/main/java/net/minecraft/world/level/biome/Biome.java index 15f82c9a1ce1fef2e951d1b3c7a65e64b82061ea..f9fbfb63f19decb3b15284306d7edda072e609af 100644 --- a/src/main/java/net/minecraft/world/level/biome/Biome.java +++ b/src/main/java/net/minecraft/world/level/biome/Biome.java @@ -63,13 +63,18 @@ public final class Biome { private final BiomeGenerationSettings generationSettings; private final MobSpawnSettings mobSettings; private final BiomeSpecialEffects specialEffects; - private final ThreadLocal temperatureCache = ThreadLocal.withInitial(() -> Util.make(() -> { + // Pufferfish start - use our cache + private final ThreadLocal temperatureCache = ThreadLocal.withInitial(() -> Util.make(() -> { + /* Long2FloatLinkedOpenHashMap long2FloatLinkedOpenHashMap = new Long2FloatLinkedOpenHashMap(1024, 0.25F) { protected void rehash(int i) { } }; long2FloatLinkedOpenHashMap.defaultReturnValue(Float.NaN); return long2FloatLinkedOpenHashMap; + */ + return new gg.airplane.structs.Long2FloatAgingCache(TEMPERATURE_CACHE_SIZE); + // Pufferfish end })); Biome(Biome.ClimateSettings weather, BiomeSpecialEffects effects, BiomeGenerationSettings generationSettings, MobSpawnSettings spawnSettings) { @@ -112,17 +117,15 @@ public final class Biome { @Deprecated public float getTemperature(BlockPos blockPos) { long l = blockPos.asLong(); - Long2FloatLinkedOpenHashMap long2FloatLinkedOpenHashMap = this.temperatureCache.get(); - float f = long2FloatLinkedOpenHashMap.get(l); + // Pufferfish start + gg.airplane.structs.Long2FloatAgingCache cache = this.temperatureCache.get(); + float f = cache.getValue(l); if (!Float.isNaN(f)) { return f; } else { float g = this.getHeightAdjustedTemperature(blockPos); - if (long2FloatLinkedOpenHashMap.size() == 1024) { - long2FloatLinkedOpenHashMap.removeFirstFloat(); - } - - long2FloatLinkedOpenHashMap.put(l, g); + cache.putValue(l, g); + // Pufferfish end return g; } } diff --git a/src/main/java/net/minecraft/world/level/block/entity/ChestBlockEntity.java b/src/main/java/net/minecraft/world/level/block/entity/ChestBlockEntity.java index b88aa184cd06a0485146f58a5b61a56a50911209..7b64c57795fe27d8397500bfa41f71f9d2103b20 100644 --- a/src/main/java/net/minecraft/world/level/block/entity/ChestBlockEntity.java +++ b/src/main/java/net/minecraft/world/level/block/entity/ChestBlockEntity.java @@ -32,7 +32,10 @@ import org.bukkit.entity.HumanEntity; public class ChestBlockEntity extends RandomizableContainerBlockEntity implements LidBlockEntity { private static final int EVENT_SET_OPEN_COUNT = 1; + // Pufferfish start private NonNullList items; + private gg.airplane.structs.ItemListWithBitset optimizedItems; + // Pufferfish end public final ContainerOpenersCounter openersCounter; private final ChestLidController chestLidController; @@ -66,9 +69,13 @@ public class ChestBlockEntity extends RandomizableContainerBlockEntity implement } // CraftBukkit end + private final boolean isNative = getClass().equals(ChestBlockEntity.class); // Pufferfish protected ChestBlockEntity(BlockEntityType type, BlockPos pos, BlockState state) { super(type, pos, state); - this.items = NonNullList.withSize(27, ItemStack.EMPTY); + // Pufferfish start + this.optimizedItems = new gg.airplane.structs.ItemListWithBitset(27); + this.items = this.optimizedItems.nonNullList; + // Pufferfish end this.openersCounter = new ContainerOpenersCounter() { @Override protected void onOpen(Level world, BlockPos pos, BlockState state) { @@ -99,6 +106,23 @@ public class ChestBlockEntity extends RandomizableContainerBlockEntity implement this.chestLidController = new ChestLidController(); } + // Pufferfish start + @Override + public boolean hasEmptySlot(Direction enumdirection) { + return isNative ? !this.optimizedItems.hasFullStacks() : super.hasEmptySlot(enumdirection); + } + + @Override + public boolean isCompletelyFull(Direction enumdirection) { + return isNative ? this.optimizedItems.hasFullStacks() && super.isCompletelyFull(enumdirection) : super.isCompletelyFull(enumdirection); + } + + @Override + public boolean isCompletelyEmpty(Direction enumdirection) { + return isNative && this.optimizedItems.isCompletelyEmpty() || super.isCompletelyEmpty(enumdirection); + } + // Pufferfish end + public ChestBlockEntity(BlockPos pos, BlockState state) { this(BlockEntityType.CHEST, pos, state); } @@ -116,7 +140,10 @@ public class ChestBlockEntity extends RandomizableContainerBlockEntity implement @Override protected void loadAdditional(CompoundTag nbt, HolderLookup.Provider registryLookup) { super.loadAdditional(nbt, registryLookup); - this.items = NonNullList.withSize(this.getContainerSize(), ItemStack.EMPTY); + // Pufferfish start + this.optimizedItems = new gg.airplane.structs.ItemListWithBitset(this.getContainerSize()); + this.items = this.optimizedItems.nonNullList; + // Pufferfish end if (!this.tryLoadLootTable(nbt)) { ContainerHelper.loadAllItems(nbt, this.items, registryLookup); } @@ -188,7 +215,10 @@ public class ChestBlockEntity extends RandomizableContainerBlockEntity implement @Override protected void setItems(NonNullList inventory) { - this.items = inventory; + // Pufferfish start + this.optimizedItems = gg.airplane.structs.ItemListWithBitset.fromList(inventory); + this.items = this.optimizedItems.nonNullList; + // Pufferfish end } @Override diff --git a/src/main/java/net/minecraft/world/level/block/entity/HopperBlockEntity.java b/src/main/java/net/minecraft/world/level/block/entity/HopperBlockEntity.java index 8310d132006043e93c612890514c4c7f3eb1c74d..1bd1f88aea2c841c20e21f1472f2ccfbde953bd4 100644 --- a/src/main/java/net/minecraft/world/level/block/entity/HopperBlockEntity.java +++ b/src/main/java/net/minecraft/world/level/block/entity/HopperBlockEntity.java @@ -47,7 +47,10 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen public static final int MOVE_ITEM_SPEED = 8; public static final int HOPPER_CONTAINER_SIZE = 5; private static final int[][] CACHED_SLOTS = new int[54][]; + // Pufferfish start private NonNullList items; + private gg.airplane.structs.ItemListWithBitset optimizedItems; + // Pufferfish end public int cooldownTime; private long tickedGameTime; private Direction facing; @@ -84,15 +87,38 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen public HopperBlockEntity(BlockPos pos, BlockState state) { super(BlockEntityType.HOPPER, pos, state); - this.items = NonNullList.withSize(5, ItemStack.EMPTY); + // Pufferfish start + this.optimizedItems = new gg.airplane.structs.ItemListWithBitset(5); + this.items = this.optimizedItems.nonNullList; + // Pufferfish end this.cooldownTime = -1; this.facing = (Direction) state.getValue(HopperBlock.FACING); } + // Pufferfish start + @Override + public boolean hasEmptySlot(Direction enumdirection) { + return !this.optimizedItems.hasFullStacks(); + } + + @Override + public boolean isCompletelyFull(Direction enumdirection) { + return this.optimizedItems.hasFullStacks() && super.isCompletelyFull(enumdirection); + } + + @Override + public boolean isCompletelyEmpty(Direction enumdirection) { + return this.optimizedItems.isCompletelyEmpty() || super.isCompletelyEmpty(enumdirection); + } + // Pufferfish end + @Override protected void loadAdditional(CompoundTag nbt, HolderLookup.Provider registryLookup) { super.loadAdditional(nbt, registryLookup); - this.items = NonNullList.withSize(this.getContainerSize(), ItemStack.EMPTY); + // Pufferfish start + this.optimizedItems = new gg.airplane.structs.ItemListWithBitset(this.getContainerSize()); + this.items = this.optimizedItems.nonNullList; + // Pufferfish end if (!this.tryLoadLootTable(nbt)) { ContainerHelper.loadAllItems(nbt, this.items, registryLookup); } @@ -525,6 +551,7 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen } private static boolean isFullContainer(Container inventory, Direction direction) { + if (true) return inventory.isCompletelyFull(direction); // Pufferfish - use bitsets int[] aint = HopperBlockEntity.getSlots(inventory, direction); int[] aint1 = aint; int i = aint.length; @@ -723,7 +750,7 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen if (HopperBlockEntity.canPlaceItemInContainer(to, stack, slot, side)) { boolean flag = false; - boolean flag1 = to.isEmpty(); + boolean flag1 = to.isCompletelyEmpty(side); // Pufferfish if (itemstack1.isEmpty()) { // Spigot start - SPIGOT-6693, InventorySubcontainer#setItem @@ -911,7 +938,10 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen @Override protected void setItems(NonNullList inventory) { - this.items = inventory; + // Pufferfish start + this.optimizedItems = gg.airplane.structs.ItemListWithBitset.fromList(inventory); + this.items = this.optimizedItems.nonNullList; + // Pufferfish end } public static void entityInside(Level world, BlockPos pos, BlockState state, Entity entity, HopperBlockEntity blockEntity) { diff --git a/src/main/java/net/minecraft/world/level/block/entity/RandomizableContainerBlockEntity.java b/src/main/java/net/minecraft/world/level/block/entity/RandomizableContainerBlockEntity.java index e2752752417c50b06f7c15b7d00bda0eaad3b0ae..23cb5d00e9f171f445f3dc8cd3947450da979da3 100644 --- a/src/main/java/net/minecraft/world/level/block/entity/RandomizableContainerBlockEntity.java +++ b/src/main/java/net/minecraft/world/level/block/entity/RandomizableContainerBlockEntity.java @@ -48,7 +48,7 @@ public abstract class RandomizableContainerBlockEntity extends BaseContainerBloc @Override public boolean isEmpty() { this.unpackLootTable(null); - return super.isEmpty(); + return this.isCompletelyEmpty(null); // Pufferfish - use super } @Override 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 14ee7b5b9b804bebd4e2a846b238547a28a36035..e5b7a14c0325cf0c35a03890d2526a90a1b17fad 100644 --- a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java +++ b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java @@ -88,6 +88,18 @@ public class LevelChunk extends ChunkAccess { private final LevelChunkTicks fluidTicks; public volatile FullChunkStatus chunkStatus = FullChunkStatus.INACCESSIBLE; // Paper - rewrite chunk system + // Pufferfish start - instead of using a random every time the chunk is ticked, define when lightning strikes preemptively + private int lightningTick; + // shouldDoLightning compiles down to 29 bytes, which with the default of 35 byte inlining should guarantee an inline + public final boolean shouldDoLightning(net.minecraft.util.RandomSource random) { + if (this.lightningTick-- <= 0) { + this.lightningTick = random.nextInt(this.level.spigotConfig.thunderChance) << 1; + return true; + } + return false; + } + // Pufferfish end + public LevelChunk(Level world, ChunkPos pos) { this(world, pos, UpgradeData.EMPTY, new LevelChunkTicks<>(), new LevelChunkTicks<>(), 0L, (LevelChunkSection[]) null, (LevelChunk.PostLoadProcessor) null, (BlendingData) null); } @@ -111,6 +123,8 @@ public class LevelChunk extends ChunkAccess { this.postLoad = entityLoader; this.blockTicks = blockTickScheduler; this.fluidTicks = fluidTickScheduler; + + this.lightningTick = this.level.getThreadUnsafeRandom().nextInt(100000) << 1; // Pufferfish - initialize lightning tick } // CraftBukkit start diff --git a/src/main/java/net/minecraft/world/level/chunk/LevelChunkSection.java b/src/main/java/net/minecraft/world/level/chunk/LevelChunkSection.java index a2a5aef769ee8bb638a5a9f3da9812fa4a85dda5..7288261b8924d08e93abecb664e2273c624a325b 100644 --- a/src/main/java/net/minecraft/world/level/chunk/LevelChunkSection.java +++ b/src/main/java/net/minecraft/world/level/chunk/LevelChunkSection.java @@ -25,6 +25,7 @@ public class LevelChunkSection { public final PalettedContainer states; // CraftBukkit start - read/write private PalettedContainer> biomes; + public short fluidStateCount; // Pufferfish public final com.destroystokyo.paper.util.maplist.IBlockDataList tickingList = new com.destroystokyo.paper.util.maplist.IBlockDataList(); // Paper // Paper start - optimise collisions private int specialCollidingBlocks; @@ -102,6 +103,7 @@ public class LevelChunkSection { if (!fluid.isEmpty()) { --this.tickingFluidCount; + --this.fluidStateCount; // Pufferfish } if (!state.isAir()) { @@ -116,6 +118,7 @@ public class LevelChunkSection { if (!fluid1.isEmpty()) { ++this.tickingFluidCount; + ++this.fluidStateCount; // Pufferfish } this.updateBlockCallback(x, y, z, iblockdata1, state); // Paper - optimise collisions @@ -162,6 +165,7 @@ public class LevelChunkSection { if (fluid.isRandomlyTicking()) { this.tickingFluidCount = (short) (this.tickingFluidCount + 1); } + this.fluidStateCount++; // Pufferfish } // Paper start - optimise collisions diff --git a/src/main/java/net/minecraft/world/level/entity/EntityTickList.java b/src/main/java/net/minecraft/world/level/entity/EntityTickList.java index 83a39f900551e39d5af6f17a339a386ddee4feef..0c8c534fc69172387f188af5282accfed7597ac7 100644 --- a/src/main/java/net/minecraft/world/level/entity/EntityTickList.java +++ b/src/main/java/net/minecraft/world/level/entity/EntityTickList.java @@ -9,7 +9,7 @@ import javax.annotation.Nullable; import net.minecraft.world.entity.Entity; public class EntityTickList { - private final io.papermc.paper.util.maplist.IteratorSafeOrderedReferenceSet entities = new io.papermc.paper.util.maplist.IteratorSafeOrderedReferenceSet<>(true); // Paper - rewrite this, always keep this updated - why would we EVER tick an entity that's not ticking? + public final io.papermc.paper.util.maplist.IteratorSafeOrderedReferenceSet entities = new io.papermc.paper.util.maplist.IteratorSafeOrderedReferenceSet<>(true); // Paper - rewrite this, always keep this updated - why would we EVER tick an entity that's not ticking? // Pufferfish - private->public private void ensureActiveIsNotIterated() { // Paper - replace with better logic, do not delay removals 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..58296b67f80587af485b0068e461cfd3d8d6f96f 100644 --- a/src/main/java/net/minecraft/world/level/material/FlowingFluid.java +++ b/src/main/java/net/minecraft/world/level/material/FlowingFluid.java @@ -45,6 +45,8 @@ public abstract class FlowingFluid extends Fluid { public static final BooleanProperty FALLING = BlockStateProperties.FALLING; public static final IntegerProperty LEVEL = BlockStateProperties.LEVEL_FLOWING; private static final int CACHE_SIZE = 200; + // Pufferfish start - use our own cache + /* private static final ThreadLocal> OCCLUSION_CACHE = ThreadLocal.withInitial(() -> { Object2ByteLinkedOpenHashMap object2bytelinkedopenhashmap = new Object2ByteLinkedOpenHashMap(200) { protected void rehash(int i) {} @@ -53,6 +55,14 @@ public abstract class FlowingFluid extends Fluid { object2bytelinkedopenhashmap.defaultReturnValue((byte) 127); return object2bytelinkedopenhashmap; }); + */ + + private static final ThreadLocal> localFluidDirectionCache = ThreadLocal.withInitial(() -> { + // Pufferfish todo - mess with this number for performance + // with 2048 it seems very infrequent on a small world that it has to remove old entries + return new gg.airplane.structs.FluidDirectionCache<>(2048); + }); + // Pufferfish end private final Map shapes = Maps.newIdentityHashMap(); public FlowingFluid() {} @@ -251,6 +261,8 @@ public abstract class FlowingFluid extends Fluid { return false; } // Paper end - optimise collisions + // Pufferfish start - modify to use our cache + /* Object2ByteLinkedOpenHashMap object2bytelinkedopenhashmap; if (!state.getBlock().hasDynamicShape() && !fromState.getBlock().hasDynamicShape()) { @@ -258,9 +270,16 @@ public abstract class FlowingFluid extends Fluid { } else { object2bytelinkedopenhashmap = null; } + */ + gg.airplane.structs.FluidDirectionCache cache = null; + + if (!state.getBlock().hasDynamicShape() && !fromState.getBlock().hasDynamicShape()) { + cache = localFluidDirectionCache.get(); + } Block.BlockStatePairKey block_a; + /* if (object2bytelinkedopenhashmap != null) { block_a = new Block.BlockStatePairKey(state, fromState, face); byte b0 = object2bytelinkedopenhashmap.getAndMoveToFirst(block_a); @@ -271,11 +290,22 @@ public abstract class FlowingFluid extends Fluid { } else { block_a = null; } + */ + if (cache != null) { + block_a = new Block.BlockStatePairKey(state, fromState, face); + Boolean flag = cache.getValue(block_a); + if (flag != null) { + return flag; + } + } else { + block_a = null; + } VoxelShape voxelshape = state.getCollisionShape(world, pos); VoxelShape voxelshape1 = fromState.getCollisionShape(world, fromPos); boolean flag = !Shapes.mergedFaceOccludes(voxelshape, voxelshape1, face); + /* if (object2bytelinkedopenhashmap != null) { if (object2bytelinkedopenhashmap.size() == 200) { object2bytelinkedopenhashmap.removeLastByte(); @@ -283,6 +313,11 @@ public abstract class FlowingFluid extends Fluid { object2bytelinkedopenhashmap.putAndMoveToFirst(block_a, (byte) (flag ? 1 : 0)); } + */ + if (cache != null) { + cache.putValue(block_a, flag); + } + // Pufferfish end return flag; } diff --git a/src/main/java/net/minecraft/world/level/storage/loot/LootParams.java b/src/main/java/net/minecraft/world/level/storage/loot/LootParams.java index 37a0002bbe6539648db5219bb373e0404ae48dc0..ca0571d232e102c4b177a1ea44b96f5f0f440211 100644 --- a/src/main/java/net/minecraft/world/level/storage/loot/LootParams.java +++ b/src/main/java/net/minecraft/world/level/storage/loot/LootParams.java @@ -21,8 +21,10 @@ public class LootParams { public LootParams(ServerLevel world, Map, Object> parameters, Map dynamicDrops, float luck) { this.level = world; - this.params = parameters; - this.dynamicDrops = dynamicDrops; + // Pufferfish start - use unmodifiable maps instead of immutable ones to skip the copy + this.params = java.util.Collections.unmodifiableMap(parameters); + this.dynamicDrops = java.util.Collections.unmodifiableMap(dynamicDrops); + // Pufferfish end this.luck = luck; } diff --git a/src/main/java/net/minecraft/world/phys/shapes/EntityCollisionContext.java b/src/main/java/net/minecraft/world/phys/shapes/EntityCollisionContext.java index 88a4a72bb390947dc17e5da09a99b2d1b3ac4621..284c76ddb9724b44bb2e93f590685c728e843e6d 100644 --- a/src/main/java/net/minecraft/world/phys/shapes/EntityCollisionContext.java +++ b/src/main/java/net/minecraft/world/phys/shapes/EntityCollisionContext.java @@ -17,50 +17,69 @@ public class EntityCollisionContext implements CollisionContext { return defaultValue; } }; - private final boolean descending; - private final double entityBottom; - private final ItemStack heldItem; - private final Predicate canStandOnFluid; + // Pufferfish start - remove these and pray no plugin uses them + // private final boolean descending; + // private final double entityBottom; + // private final ItemStack heldItem; + // private final Predicate canStandOnFluid; + // Pufferfish end @Nullable private final Entity entity; protected EntityCollisionContext(boolean descending, double minY, ItemStack heldItem, Predicate walkOnFluidPredicate, @Nullable Entity entity) { - this.descending = descending; - this.entityBottom = minY; - this.heldItem = heldItem; - this.canStandOnFluid = walkOnFluidPredicate; + // Pufferfish start - remove these + // this.descending = descending; + // this.entityBottom = minY; + // this.heldItem = heldItem; + // this.canStandOnFluid = walkOnFluidPredicate; + // Pufferfish end this.entity = entity; } @Deprecated protected EntityCollisionContext(Entity entity) { - this( - entity.isDescending(), - entity.getY(), - entity instanceof LivingEntity ? ((LivingEntity)entity).getMainHandItem() : ItemStack.EMPTY, - entity instanceof LivingEntity ? ((LivingEntity)entity)::canStandOnFluid : fluidState -> false, - entity - ); + // Pufferfish start - remove this + // this( + // entity.isDescending(), + // entity.getY(), + // entity instanceof LivingEntity ? ((LivingEntity)entity).getMainHandItem() : ItemStack.EMPTY, + // entity instanceof LivingEntity ? ((LivingEntity)entity)::canStandOnFluid : fluidState -> false, + // entity + // ); + // Pufferfish end + this.entity = entity; } @Override public boolean isHoldingItem(Item item) { - return this.heldItem.is(item); + // Pufferfish start + Entity entity = this.entity; + if (entity instanceof LivingEntity livingEntity) { + return livingEntity.getMainHandItem().is(item); + } + return ItemStack.EMPTY.is(item); + // Pufferfish end } @Override public boolean canStandOnFluid(FluidState stateAbove, FluidState state) { - return this.canStandOnFluid.test(state) && !stateAbove.getType().isSame(state.getType()); + // Pufferfish start + Entity entity = this.entity; + if (entity instanceof LivingEntity livingEntity) { + return livingEntity.canStandOnFluid(state) && !stateAbove.getType().isSame(state.getType()); + } + return false; + // Pufferfish end } @Override public boolean isDescending() { - return this.descending; + return this.entity != null && this.entity.isDescending(); // Pufferfish } @Override public boolean isAbove(VoxelShape shape, BlockPos pos, boolean defaultValue) { - return this.entityBottom > (double)pos.getY() + shape.max(Direction.Axis.Y) - 1.0E-5F; + return (this.entity == null ? -Double.MAX_VALUE : entity.getY()) > (double)pos.getY() + shape.max(Direction.Axis.Y) - 1.0E-5F; // Pufferfish } @Nullable diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java index 94a31c8f903eb61eb6d203e8e6fe8fb0beca28b1..5562e2a34d6baf5949435d6c9e233d86a51d8da3 100644 --- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java +++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java @@ -1124,6 +1124,11 @@ public final class CraftServer implements Server { plugin.getPluginMeta().getDisplayName(), "This plugin is not properly shutting down its async tasks when it is being shut down. This task may throw errors during the final shutdown logs and might not complete before process dies." )); + getLogger().log(Level.SEVERE, String.format("%s Stacktrace", worker.getThread().getName())); + StackTraceElement[] stackTrace = worker.getThread().getStackTrace(); + for (StackTraceElement element : stackTrace) { + getLogger().log(Level.SEVERE, " " + element.toString()); + } if (console.isDebugging()) io.papermc.paper.util.TraceUtil.dumpTraceForThread(worker.getThread(), "still running"); // Paper - Debugging } } diff --git a/src/main/java/org/bukkit/craftbukkit/inventory/CraftShapelessRecipe.java b/src/main/java/org/bukkit/craftbukkit/inventory/CraftShapelessRecipe.java index 96d772eb02f79f8c478f5e6f065e387aa7665b18..c5ce412f321b8b4f31cc042893659e213b081f29 100644 --- a/src/main/java/org/bukkit/craftbukkit/inventory/CraftShapelessRecipe.java +++ b/src/main/java/org/bukkit/craftbukkit/inventory/CraftShapelessRecipe.java @@ -45,6 +45,6 @@ public class CraftShapelessRecipe extends ShapelessRecipe implements CraftRecipe data.set(i, this.toNMS(ingred.get(i), true)); } - MinecraftServer.getServer().getRecipeManager().addRecipe(new RecipeHolder<>(CraftNamespacedKey.toMinecraft(this.getKey()), new net.minecraft.world.item.crafting.ShapelessRecipe(this.getGroup(), CraftRecipe.getCategory(this.getCategory()), CraftItemStack.asNMSCopy(this.getResult()), data))); + MinecraftServer.getServer().getRecipeManager().addRecipe(new RecipeHolder<>(CraftNamespacedKey.toMinecraft(this.getKey()), new net.minecraft.world.item.crafting.ShapelessRecipe(this.getGroup(), CraftRecipe.getCategory(this.getCategory()), CraftItemStack.asNMSCopy(this.getResult()), data, true))); // Pufferfish } } diff --git a/src/main/java/org/bukkit/craftbukkit/scheduler/MinecraftInternalPlugin.java b/src/main/java/org/bukkit/craftbukkit/scheduler/MinecraftInternalPlugin.java index 66bdac50130f523f9dc4379b103b7a469f9ca36b..ef780e0566dbac07da88ba26dc595b49c8ff5ab3 100644 --- a/src/main/java/org/bukkit/craftbukkit/scheduler/MinecraftInternalPlugin.java +++ b/src/main/java/org/bukkit/craftbukkit/scheduler/MinecraftInternalPlugin.java @@ -22,7 +22,8 @@ public class MinecraftInternalPlugin extends PluginBase { private boolean enabled = true; private final String pluginName; - private PluginDescriptionFile pdf; + private org.bukkit.plugin.PluginLogger logger; + private PluginDescriptionFile pdf; // Pufferfish public MinecraftInternalPlugin() { this.pluginName = "Minecraft"; @@ -81,7 +82,12 @@ public class MinecraftInternalPlugin extends PluginBase { @Override public PluginLogger getLogger() { - throw new UnsupportedOperationException("Not supported."); + // Pufferfish start + if (this.logger == null) { + this.logger = new org.bukkit.plugin.PluginLogger(this); // Pufferfish + } + return this.logger; + // Pufferfish end } @Override @@ -91,7 +97,7 @@ public class MinecraftInternalPlugin extends PluginBase { @Override public Server getServer() { - throw new UnsupportedOperationException("Not supported."); + return org.bukkit.Bukkit.getServer(); // Pufferfish - impl } @Override diff --git a/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java b/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java index 4788a591f40f506d81b10fd9f6ab68f308a68e23..aa9e901b243a0296966254f802437cb7c1a3a846 100644 --- a/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java +++ b/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java @@ -503,7 +503,7 @@ public final class CraftMagicNumbers implements UnsafeValues { @Override public com.destroystokyo.paper.util.VersionFetcher getVersionFetcher() { - return new com.destroystokyo.paper.PaperVersionFetcher(); + return new gg.pufferfish.pufferfish.PufferfishVersionFetcher(); // Pufferfish } @Override diff --git a/src/main/java/org/bukkit/craftbukkit/util/ServerShutdownThread.java b/src/main/java/org/bukkit/craftbukkit/util/ServerShutdownThread.java index e8e93538dfd71de86515d9405f728db1631e949a..3dff02fd97f001508e2f81192817bf1b0ef92446 100644 --- a/src/main/java/org/bukkit/craftbukkit/util/ServerShutdownThread.java +++ b/src/main/java/org/bukkit/craftbukkit/util/ServerShutdownThread.java @@ -11,6 +11,7 @@ public class ServerShutdownThread extends Thread { @Override public void run() { + try { gg.pufferfish.pufferfish.flare.ProfilingManager.stop(); } catch (Throwable t) {} // Pufferfish - shut down Flare if it's running try { // Paper start - try to shutdown on main server.safeShutdown(false, false); diff --git a/src/main/java/org/bukkit/craftbukkit/util/Versioning.java b/src/main/java/org/bukkit/craftbukkit/util/Versioning.java index 774556a62eb240da42e84db4502e2ed43495be17..80553face9c70c2a3d897681e7761df85b22d464 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/gg.pufferfish.pufferfish/pufferfish-api/pom.properties"); // Pufferfish Properties properties = new Properties(); if (stream != null) { diff --git a/src/main/java/org/spigotmc/ActivationRange.java b/src/main/java/org/spigotmc/ActivationRange.java index 3283ed99c35ffed6805567705e0518d9f84feedc..d7c7e12c0b8f77e59d94de130972f762ed227726 100644 --- a/src/main/java/org/spigotmc/ActivationRange.java +++ b/src/main/java/org/spigotmc/ActivationRange.java @@ -38,6 +38,10 @@ import co.aikar.timings.MinecraftTimings; import net.minecraft.world.entity.schedule.Activity; import net.minecraft.world.level.Level; import net.minecraft.world.phys.AABB; +// Pufferfish start +import net.minecraft.world.phys.Vec3; +import java.util.List; +// Pufferfish end public class ActivationRange { @@ -223,6 +227,25 @@ public class ActivationRange } // Paper end - Configurable marker ticking ActivationRange.activateEntity(entity); + + // Pufferfish start + if (gg.pufferfish.pufferfish.PufferfishConfig.dearEnabled && entity.getType().dabEnabled) { + if (!entity.activatedPriorityReset) { + entity.activatedPriorityReset = true; + entity.activatedPriority = gg.pufferfish.pufferfish.PufferfishConfig.maximumActivationPrio; + } + Vec3 playerVec = player.position(); + Vec3 entityVec = entity.position(); + double diffX = playerVec.x - entityVec.x, diffY = playerVec.y - entityVec.y, diffZ = playerVec.z - entityVec.z; + int squaredDistance = (int) (diffX * diffX + diffY * diffY + diffZ * diffZ); + entity.activatedPriority = squaredDistance > gg.pufferfish.pufferfish.PufferfishConfig.startDistanceSquared ? + Math.max(1, Math.min(squaredDistance >> gg.pufferfish.pufferfish.PufferfishConfig.activationDistanceMod, entity.activatedPriority)) : + 1; + } else { + entity.activatedPriority = 1; + } + // Pufferfish end + } // Paper end } @@ -239,12 +262,12 @@ public class ActivationRange if ( MinecraftServer.currentTick > entity.activatedTick ) { if ( entity.defaultActivationState ) - { + { // Pufferfish - diff on change entity.activatedTick = MinecraftServer.currentTick; return; } if ( entity.activationType.boundingBox.intersects( entity.getBoundingBox() ) ) - { + { // Pufferfish - diff on change entity.activatedTick = MinecraftServer.currentTick; } } @@ -298,7 +321,7 @@ public class ActivationRange if ( entity instanceof LivingEntity ) { LivingEntity living = (LivingEntity) entity; - if ( living.onClimbable() || living.jumping || living.hurtTime > 0 || living.activeEffects.size() > 0 || living.isFreezing()) // Paper + if ( living.onClimableCached() || living.jumping || living.hurtTime > 0 || living.activeEffects.size() > 0 || living.isFreezing() ) // Paper // Pufferfish - use cached { return 1; // Paper }