diff options
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.patch | 375 |
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()); ++ } ++} |