aboutsummaryrefslogtreecommitdiffhomepage
path: root/patches/server/0747-Add-paper-mobcaps-and-paper-playermobcaps.patch
diff options
context:
space:
mode:
Diffstat (limited to 'patches/server/0747-Add-paper-mobcaps-and-paper-playermobcaps.patch')
-rw-r--r--patches/server/0747-Add-paper-mobcaps-and-paper-playermobcaps.patch375
1 files changed, 375 insertions, 0 deletions
diff --git a/patches/server/0747-Add-paper-mobcaps-and-paper-playermobcaps.patch b/patches/server/0747-Add-paper-mobcaps-and-paper-playermobcaps.patch
new file mode 100644
index 0000000000..2983360250
--- /dev/null
+++ b/patches/server/0747-Add-paper-mobcaps-and-paper-playermobcaps.patch
@@ -0,0 +1,375 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Jason Penilla <[email protected]>
+Date: Mon, 16 Aug 2021 01:31:54 -0500
+Subject: [PATCH] Add '/paper mobcaps' and '/paper playermobcaps'
+
+Add commands to get the mobcaps for a world, as well as the mobcaps for
+each player when per-player mob spawning is enabled.
+
+Also has a hover text on each mob category listing what entity types are
+in said category
+
+diff --git a/src/main/java/com/destroystokyo/paper/PaperCommand.java b/src/main/java/com/destroystokyo/paper/PaperCommand.java
+index f436ab35798c9b6e6cb2eb60d2c02cbf9b742e69..4d7575087947f3b199dd895cd9aa02a7d61768b1 100644
+--- a/src/main/java/com/destroystokyo/paper/PaperCommand.java
++++ b/src/main/java/com/destroystokyo/paper/PaperCommand.java
+@@ -3,6 +3,7 @@ package com.destroystokyo.paper;
+ import com.destroystokyo.paper.io.SyncLoadFinder;
+ import com.google.common.base.Functions;
+ import com.google.common.base.Joiner;
++import com.google.common.collect.ImmutableMap;
+ import com.google.common.collect.ImmutableSet;
+ import com.google.common.collect.Iterables;
+ import com.google.common.collect.Lists;
+@@ -10,6 +11,13 @@ import com.google.common.collect.Maps;
+ import com.google.gson.JsonObject;
+ import com.google.gson.internal.Streams;
+ import com.google.gson.stream.JsonWriter;
++import net.kyori.adventure.text.Component;
++import net.kyori.adventure.text.ComponentLike;
++import net.kyori.adventure.text.JoinConfiguration;
++import net.kyori.adventure.text.TextComponent;
++import net.kyori.adventure.text.format.NamedTextColor;
++import net.kyori.adventure.text.format.TextColor;
++import net.minecraft.core.Registry;
+ import net.minecraft.resources.ResourceLocation;
+ import net.minecraft.server.MCUtil;
+ import net.minecraft.server.MinecraftServer;
+@@ -19,10 +27,12 @@ import net.minecraft.server.level.ServerLevel;
+ import net.minecraft.server.level.ServerPlayer;
+ import net.minecraft.server.level.ThreadedLevelLightEngine;
+ import net.minecraft.world.entity.EntityType;
++import net.minecraft.world.entity.MobCategory;
+ import net.minecraft.world.level.ChunkPos;
+ import net.minecraft.network.protocol.game.ClientboundLightUpdatePacket;
+ import net.minecraft.resources.ResourceLocation;
+ import net.minecraft.server.MCUtil;
++import net.minecraft.world.level.NaturalSpawner;
+ import org.apache.commons.lang3.tuple.MutablePair;
+ import org.apache.commons.lang3.tuple.Pair;
+ import org.bukkit.Bukkit;
+@@ -55,11 +65,12 @@ import java.util.List;
+ import java.util.Locale;
+ import java.util.Map;
+ import java.util.Set;
++import java.util.function.ToIntFunction;
+ import java.util.stream.Collectors;
+
+ public class PaperCommand extends Command {
+ private static final String BASE_PERM = "bukkit.command.paper.";
+- private static final ImmutableSet<String> SUBCOMMANDS = ImmutableSet.<String>builder().add("heap", "entity", "reload", "version", "debug", "chunkinfo", "fixlight", "syncloadinfo", "dumpitem").build();
++ private static final ImmutableSet<String> SUBCOMMANDS = ImmutableSet.<String>builder().add("heap", "entity", "reload", "version", "debug", "chunkinfo", "fixlight", "syncloadinfo", "dumpitem", "mobcaps", "playermobcaps").build();
+
+ public PaperCommand(String name) {
+ super(name);
+@@ -92,6 +103,10 @@ public class PaperCommand extends Command {
+ return getListMatchingLast(sender, args, "help", "chunks");
+ }
+ break;
++ case "mobcaps":
++ return getListMatchingLast(sender, args, this.suggestMobcaps(sender, args));
++ case "playermobcaps":
++ return getListMatchingLast(sender, args, this.suggestPlayerMobcaps(sender, args));
+ case "chunkinfo":
+ List<String> worldNames = new ArrayList<>();
+ worldNames.add("*");
+@@ -188,6 +203,12 @@ public class PaperCommand extends Command {
+ case "syncloadinfo":
+ this.doSyncLoadInfo(sender, args);
+ break;
++ case "mobcaps":
++ this.printMobcaps(sender, args);
++ break;
++ case "playermobcaps":
++ this.printPlayerMobcaps(sender, args);
++ break;
+ case "ver":
+ if (!testPermission(sender, "version")) break; // "ver" needs a special check because it's an alias. All other commands are checked up before the switch statement (because they are present in the SUBCOMMANDS set)
+ case "version":
+@@ -246,6 +267,184 @@ public class PaperCommand extends Command {
+ }
+ }
+
++ public static final Map<MobCategory, TextColor> MOB_CATEGORY_COLORS = ImmutableMap.<MobCategory, TextColor>builder()
++ .put(MobCategory.MONSTER, NamedTextColor.RED)
++ .put(MobCategory.CREATURE, NamedTextColor.GREEN)
++ .put(MobCategory.AMBIENT, NamedTextColor.GRAY)
++ .put(MobCategory.AXOLOTLS, TextColor.color(0x7324FF))
++ .put(MobCategory.UNDERGROUND_WATER_CREATURE, TextColor.color(0x3541E6))
++ .put(MobCategory.WATER_CREATURE, TextColor.color(0x006EFF))
++ .put(MobCategory.WATER_AMBIENT, TextColor.color(0x00B3FF))
++ .put(MobCategory.MISC, TextColor.color(0x636363))
++ .build();
++
++ private List<String> suggestMobcaps(CommandSender sender, String[] args) {
++ if (args.length == 2) {
++ final List<String> worlds = new ArrayList<>(Bukkit.getWorlds().stream().map(World::getName).toList());
++ worlds.add("*");
++ return worlds;
++ }
++
++ return Collections.emptyList();
++ }
++
++ private List<String> suggestPlayerMobcaps(CommandSender sender, String[] args) {
++ if (args.length == 2) {
++ final List<String> list = new ArrayList<>();
++ for (final Player player : Bukkit.getOnlinePlayers()) {
++ if (!(sender instanceof Player senderPlayer) || senderPlayer.canSee(player)) {
++ list.add(player.getName());
++ }
++ }
++ return list;
++ }
++
++ return Collections.emptyList();
++ }
++
++ private void printMobcaps(CommandSender sender, String[] args) {
++ final List<World> worlds;
++ if (args.length == 1) {
++ if (sender instanceof Player player) {
++ worlds = List.of(player.getWorld());
++ } else {
++ sender.sendMessage(Component.text("Must specify a world! ex: '/paper mobcaps world'", NamedTextColor.RED));
++ return;
++ }
++ } else if (args.length == 2) {
++ final String input = args[1];
++ if (input.equals("*")) {
++ worlds = Bukkit.getWorlds();
++ } else {
++ final World world = Bukkit.getWorld(input);
++ if (world == null) {
++ sender.sendMessage(Component.text("'" + input + "' is not a valid world!", NamedTextColor.RED));
++ return;
++ } else {
++ worlds = List.of(world);
++ }
++ }
++ } else {
++ sender.sendMessage(Component.text("Too many arguments!", NamedTextColor.RED));
++ return;
++ }
++
++ for (final World world : worlds) {
++ final ServerLevel level = ((CraftWorld) world).getHandle();
++ final NaturalSpawner.SpawnState state = level.getChunkSource().getLastSpawnState();
++
++ final int chunks;
++ if (state == null) {
++ chunks = 0;
++ } else {
++ chunks = state.getSpawnableChunkCount();
++ }
++ sender.sendMessage(Component.join(JoinConfiguration.noSeparators(),
++ Component.text("Mobcaps for world: "),
++ Component.text(world.getName(), NamedTextColor.AQUA),
++ Component.text(" (" + chunks + " spawnable chunks)")
++ ));
++
++ sender.sendMessage(this.buildMobcapsComponent(
++ category -> {
++ if (state == null) {
++ return 0;
++ } else {
++ return state.getMobCategoryCounts().getOrDefault(category, 0);
++ }
++ },
++ category -> NaturalSpawner.globalLimitForCategory(level, category, chunks)
++ ));
++ }
++ }
++
++ private void printPlayerMobcaps(CommandSender sender, String[] args) {
++ final Player player;
++ if (args.length == 1) {
++ if (sender instanceof Player pl) {
++ player = pl;
++ } else {
++ sender.sendMessage(Component.text("Must specify a player! ex: '/paper playermobcount playerName'", NamedTextColor.RED));
++ return;
++ }
++ } else if (args.length == 2) {
++ final String input = args[1];
++ player = Bukkit.getPlayerExact(input);
++ if (player == null) {
++ sender.sendMessage(Component.text("Could not find player named '" + input + "'", NamedTextColor.RED));
++ return;
++ }
++ } else {
++ sender.sendMessage(Component.text("Too many arguments!", NamedTextColor.RED));
++ return;
++ }
++
++ final ServerPlayer serverPlayer = ((CraftPlayer) player).getHandle();
++ final ServerLevel level = serverPlayer.getLevel();
++
++ if (!level.paperConfig.perPlayerMobSpawns) {
++ sender.sendMessage(Component.text("Use '/paper mobcaps' for worlds where per-player mob spawning is disabled.", NamedTextColor.RED));
++ return;
++ }
++
++ sender.sendMessage(Component.join(JoinConfiguration.noSeparators(), Component.text("Mobcaps for player: "), Component.text(player.getName(), NamedTextColor.GREEN)));
++ sender.sendMessage(this.buildMobcapsComponent(
++ category -> level.chunkSource.chunkMap.getMobCountNear(serverPlayer, category),
++ category -> NaturalSpawner.limitForCategory(level, category)
++ ));
++ }
++
++ private Component buildMobcapsComponent(final ToIntFunction<MobCategory> countGetter, final ToIntFunction<MobCategory> limitGetter) {
++ return MOB_CATEGORY_COLORS.entrySet().stream()
++ .map(entry -> {
++ final MobCategory category = entry.getKey();
++ final TextColor color = entry.getValue();
++
++ final Component categoryHover = Component.join(JoinConfiguration.noSeparators(),
++ Component.text("Entity types in category ", TextColor.color(0xE0E0E0)),
++ Component.text(category.getName(), color),
++ Component.text(':', NamedTextColor.GRAY),
++ Component.newline(),
++ Component.newline(),
++ Registry.ENTITY_TYPE.entrySet().stream()
++ .filter(it -> it.getValue().getCategory() == category)
++ .map(it -> Component.translatable(it.getValue().getDescriptionId()))
++ .collect(Component.toComponent(Component.text(", ", NamedTextColor.GRAY)))
++ );
++
++ final Component categoryComponent = Component.text()
++ .content(" " + category.getName())
++ .color(color)
++ .hoverEvent(categoryHover)
++ .build();
++
++ final TextComponent.Builder builder = Component.text()
++ .append(
++ categoryComponent,
++ Component.text(": ", NamedTextColor.GRAY)
++ );
++ final int limit = limitGetter.applyAsInt(category);
++ if (limit != -1) {
++ builder.append(
++ Component.text(countGetter.applyAsInt(category)),
++ Component.text("/", NamedTextColor.GRAY),
++ Component.text(limit)
++ );
++ } else {
++ builder.append(Component.text()
++ .append(
++ Component.text('n'),
++ Component.text("/", NamedTextColor.GRAY),
++ Component.text('a')
++ )
++ .hoverEvent(Component.text("This category does not naturally spawn.")));
++ }
++ return builder;
++ })
++ .map(ComponentLike::asComponent)
++ .collect(Component.toComponent(Component.newline()));
++ }
++
+ private void doChunkInfo(CommandSender sender, String[] args) {
+ List<org.bukkit.World> worlds;
+ if (args.length < 2 || args[1].equals("*")) {
+diff --git a/src/main/java/net/minecraft/world/level/NaturalSpawner.java b/src/main/java/net/minecraft/world/level/NaturalSpawner.java
+index c88bd5bc044b5f9722cb5826936e31811a8312c7..9b13244571807907fc0e14463d746724b0713c19 100644
+--- a/src/main/java/net/minecraft/world/level/NaturalSpawner.java
++++ b/src/main/java/net/minecraft/world/level/NaturalSpawner.java
+@@ -148,32 +148,16 @@ public final class NaturalSpawner {
+ MobCategory enumcreaturetype = aenumcreaturetype[j];
+ // CraftBukkit start - Use per-world spawn limits
+ boolean spawnThisTick = true;
+- int limit = enumcreaturetype.getMaxInstancesPerChunk();
++ final int limit = limitForCategory(world, enumcreaturetype); // Paper
+ switch (enumcreaturetype) {
+- case MONSTER:
+- spawnThisTick = spawnMonsterThisTick;
+- limit = world.getWorld().getMonsterSpawnLimit();
+- break;
+- case CREATURE:
+- spawnThisTick = spawnAnimalThisTick;
+- limit = world.getWorld().getAnimalSpawnLimit();
+- break;
+- case WATER_CREATURE:
+- spawnThisTick = spawnWaterThisTick;
+- limit = world.getWorld().getWaterAnimalSpawnLimit();
+- break;
+- case UNDERGROUND_WATER_CREATURE:
+- spawnThisTick = spawnWaterUndergroundCreatureThisTick;
+- limit = world.getWorld().getWaterUndergroundCreatureSpawnLimit();
+- break;
+- case AMBIENT:
+- spawnThisTick = spawnAmbientThisTick;
+- limit = world.getWorld().getAmbientSpawnLimit();
+- break;
+- case WATER_AMBIENT:
+- spawnThisTick = spawnWaterAmbientThisTick;
+- limit = world.getWorld().getWaterAmbientSpawnLimit();
+- break;
++ // Paper start - not mindiff so we get conflict on change
++ case MONSTER -> spawnThisTick = spawnMonsterThisTick;
++ case CREATURE -> spawnThisTick = spawnAnimalThisTick;
++ case WATER_CREATURE -> spawnThisTick = spawnWaterThisTick;
++ case UNDERGROUND_WATER_CREATURE -> spawnThisTick = spawnWaterUndergroundCreatureThisTick;
++ case AMBIENT -> spawnThisTick = spawnAmbientThisTick;
++ case WATER_AMBIENT -> spawnThisTick = spawnWaterAmbientThisTick;
++ // Paper end
+ }
+
+ if (!spawnThisTick || limit == 0) {
+@@ -211,6 +195,28 @@ public final class NaturalSpawner {
+ world.getProfiler().pop();
+ }
+
++ // Paper start
++ public static int limitForCategory(final ServerLevel world, final MobCategory enumcreaturetype) {
++ return switch (enumcreaturetype) {
++ case MONSTER -> world.getWorld().getMonsterSpawnLimit();
++ case CREATURE -> world.getWorld().getAnimalSpawnLimit();
++ case WATER_CREATURE -> world.getWorld().getWaterAnimalSpawnLimit();
++ case UNDERGROUND_WATER_CREATURE -> world.getWorld().getWaterUndergroundCreatureSpawnLimit();
++ case AMBIENT -> world.getWorld().getAmbientSpawnLimit();
++ case WATER_AMBIENT -> world.getWorld().getWaterAmbientSpawnLimit();
++ default -> enumcreaturetype.getMaxInstancesPerChunk();
++ };
++ }
++
++ public static int globalLimitForCategory(final ServerLevel level, final MobCategory category, final int spawnableChunkCount) {
++ final int categoryLimit = limitForCategory(level, category);
++ if (categoryLimit < 1) {
++ return categoryLimit;
++ }
++ return categoryLimit * spawnableChunkCount / NaturalSpawner.MAGIC_NUMBER;
++ }
++ // Paper end
++
+ // Paper start - add parameters and int ret type
+ public static void spawnCategoryForChunk(MobCategory group, ServerLevel world, LevelChunk chunk, NaturalSpawner.SpawnPredicate checker, NaturalSpawner.AfterSpawnCallback runner) {
+ spawnCategoryForChunk(group, world, chunk, checker, runner);
+diff --git a/src/test/java/io/papermc/paper/PaperCommandTest.java b/src/test/java/io/papermc/paper/PaperCommandTest.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..4b5b368ef17bdb90f50e6ccc1f814cf93c7c0590
+--- /dev/null
++++ b/src/test/java/io/papermc/paper/PaperCommandTest.java
+@@ -0,0 +1,21 @@
++package io.papermc.paper;
++
++import com.destroystokyo.paper.PaperCommand;
++import java.util.HashSet;
++import java.util.Set;
++import net.minecraft.world.entity.MobCategory;
++import org.junit.Assert;
++import org.junit.Test;
++
++public class PaperCommandTest {
++ @Test
++ public void testMobCategoryColors() {
++ final Set<String> missing = new HashSet<>();
++ for (final MobCategory value : MobCategory.values()) {
++ if (!PaperCommand.MOB_CATEGORY_COLORS.containsKey(value)) {
++ missing.add(value.getName());
++ }
++ }
++ Assert.assertTrue("PaperCommand.MOB_CATEGORY_COLORS map missing TextColors for [" + String.join(", ", missing + "]"), missing.isEmpty());
++ }
++}