mirror of
https://github.com/PaperMC/Paper.git
synced 2024-11-21 18:15:54 +01:00
8c5b837e05
Firstly, the old methods all routed to the CompletableFuture method. However, the CF method could not guarantee that if the caller was off-main that the future would be "completed" on-main. Since the callback methods used the CF one, this meant that the callback methods did not guarantee that the callbacks were to be called on the main thread. Now, all methods route to getChunkAtAsync(x, z, gen, urgent, cb) so that the methods with the callback are guaranteed to invoke the callback on the main thread. The CF behavior remains unchanged; it may still appear to complete on main if invoked off-main. Secondly, remove the scheduleOnMain invocation in the async chunk completion. This unnecessarily delays the callback by 1 tick. Thirdly, add getChunksAtAsync(minX, minZ, maxX, maxZ, ...) which will load chunks within an area. This method is provided as a helper as keeping all chunks loaded within an area can be complicated to implement for plugins (due to the lacking ticket API), and is already implemented internally anyways. Fourthly, remove the ticket addition that occured with getChunkAt and getChunkAtAsync. The ticket addition may delay the unloading of the chunk unnecessarily. It also fixes a very rare timing bug where the future/callback would be completed after the chunk unloads.
344 lines
16 KiB
Diff
344 lines
16 KiB
Diff
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
|
From: Jason Penilla <11360596+jpenilla@users.noreply.github.com>
|
|
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/io/papermc/paper/command/PaperCommand.java b/src/main/java/io/papermc/paper/command/PaperCommand.java
|
|
index cdad0fd5257ae842f83b9c1c98b4565b468d4f54..fd4f37711989431f997d77fb0917f8a9232ce53f 100644
|
|
--- a/src/main/java/io/papermc/paper/command/PaperCommand.java
|
|
+++ b/src/main/java/io/papermc/paper/command/PaperCommand.java
|
|
@@ -40,6 +40,7 @@ public final class PaperCommand extends Command {
|
|
commands.put(Set.of("dumpplugins"), new DumpPluginsCommand());
|
|
commands.put(Set.of("syncloadinfo"), new SyncLoadInfoCommand());
|
|
commands.put(Set.of("dumpitem"), new DumpItemCommand());
|
|
+ commands.put(Set.of("mobcaps", "playermobcaps"), new MobcapsCommand());
|
|
|
|
return commands.entrySet().stream()
|
|
.flatMap(entry -> entry.getKey().stream().map(s -> Map.entry(s, entry.getValue())))
|
|
diff --git a/src/main/java/io/papermc/paper/command/subcommands/MobcapsCommand.java b/src/main/java/io/papermc/paper/command/subcommands/MobcapsCommand.java
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..d3b39d88a72ca25057fd8574d32f28db0d420818
|
|
--- /dev/null
|
|
+++ b/src/main/java/io/papermc/paper/command/subcommands/MobcapsCommand.java
|
|
@@ -0,0 +1,229 @@
|
|
+package io.papermc.paper.command.subcommands;
|
|
+
|
|
+import com.google.common.collect.ImmutableMap;
|
|
+import io.papermc.paper.command.CommandUtil;
|
|
+import io.papermc.paper.command.PaperSubcommand;
|
|
+import java.util.ArrayList;
|
|
+import java.util.Collections;
|
|
+import java.util.List;
|
|
+import java.util.Map;
|
|
+import java.util.function.ToIntFunction;
|
|
+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.registries.BuiltInRegistries;
|
|
+import net.minecraft.server.level.ServerLevel;
|
|
+import net.minecraft.server.level.ServerPlayer;
|
|
+import net.minecraft.world.entity.MobCategory;
|
|
+import net.minecraft.world.level.NaturalSpawner;
|
|
+import org.bukkit.Bukkit;
|
|
+import org.bukkit.World;
|
|
+import org.bukkit.command.CommandSender;
|
|
+import org.bukkit.craftbukkit.CraftWorld;
|
|
+import org.bukkit.craftbukkit.entity.CraftPlayer;
|
|
+import org.bukkit.entity.Player;
|
|
+import org.checkerframework.checker.nullness.qual.NonNull;
|
|
+import org.checkerframework.checker.nullness.qual.Nullable;
|
|
+import org.checkerframework.framework.qual.DefaultQualifier;
|
|
+
|
|
+@DefaultQualifier(NonNull.class)
|
|
+public final class MobcapsCommand implements PaperSubcommand {
|
|
+ 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();
|
|
+
|
|
+ @Override
|
|
+ public boolean execute(final CommandSender sender, final String subCommand, final String[] args) {
|
|
+ switch (subCommand) {
|
|
+ case "mobcaps" -> this.printMobcaps(sender, args);
|
|
+ case "playermobcaps" -> this.printPlayerMobcaps(sender, args);
|
|
+ }
|
|
+ return true;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public List<String> tabComplete(final CommandSender sender, final String subCommand, final String[] args) {
|
|
+ return switch (subCommand) {
|
|
+ case "mobcaps" -> CommandUtil.getListMatchingLast(sender, args, this.suggestMobcaps(args));
|
|
+ case "playermobcaps" -> CommandUtil.getListMatchingLast(sender, args, this.suggestPlayerMobcaps(sender, args));
|
|
+ default -> throw new IllegalArgumentException();
|
|
+ };
|
|
+ }
|
|
+
|
|
+ private List<String> suggestMobcaps(final String[] args) {
|
|
+ if (args.length == 1) {
|
|
+ final List<String> worlds = new ArrayList<>(Bukkit.getWorlds().stream().map(World::getName).toList());
|
|
+ worlds.add("*");
|
|
+ return worlds;
|
|
+ }
|
|
+
|
|
+ return Collections.emptyList();
|
|
+ }
|
|
+
|
|
+ private List<String> suggestPlayerMobcaps(final CommandSender sender, final String[] args) {
|
|
+ if (args.length == 1) {
|
|
+ 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(final CommandSender sender, final String[] args) {
|
|
+ final List<World> worlds;
|
|
+ if (args.length == 0) {
|
|
+ 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 == 1) {
|
|
+ final String input = args[0];
|
|
+ if (input.equals("*")) {
|
|
+ worlds = Bukkit.getWorlds();
|
|
+ } else {
|
|
+ final @Nullable 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.@Nullable 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(createMobcapsComponent(
|
|
+ category -> {
|
|
+ if (state == null) {
|
|
+ return 0;
|
|
+ } else {
|
|
+ return state.getMobCategoryCounts().getOrDefault(category, 0);
|
|
+ }
|
|
+ },
|
|
+ category -> NaturalSpawner.globalLimitForCategory(level, category, chunks)
|
|
+ ));
|
|
+ }
|
|
+ }
|
|
+
|
|
+ private void printPlayerMobcaps(final CommandSender sender, final String[] args) {
|
|
+ final @Nullable Player player;
|
|
+ if (args.length == 0) {
|
|
+ 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 == 1) {
|
|
+ final String input = args[0];
|
|
+ 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.serverLevel();
|
|
+
|
|
+ if (!level.paperConfig().entities.spawning.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(createMobcapsComponent(
|
|
+ category -> level.chunkSource.chunkMap.getMobCountNear(serverPlayer, category),
|
|
+ category -> level.getWorld().getSpawnLimitUnsafe(org.bukkit.craftbukkit.util.CraftSpawnCategory.toBukkit(category))
|
|
+ ));
|
|
+ }
|
|
+
|
|
+ private static Component createMobcapsComponent(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(),
|
|
+ BuiltInRegistries.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()));
|
|
+ }
|
|
+}
|
|
diff --git a/src/main/java/net/minecraft/world/level/NaturalSpawner.java b/src/main/java/net/minecraft/world/level/NaturalSpawner.java
|
|
index 606a60fe273974b71ed2bd40be819d848627e777..bf943feca387b77a3154773a59da7190d38d8621 100644
|
|
--- a/src/main/java/net/minecraft/world/level/NaturalSpawner.java
|
|
+++ b/src/main/java/net/minecraft/world/level/NaturalSpawner.java
|
|
@@ -172,6 +172,16 @@ public final class NaturalSpawner {
|
|
gameprofilerfiller.pop();
|
|
}
|
|
|
|
+ // Paper start - Add mobcaps commands
|
|
+ public static int globalLimitForCategory(final ServerLevel level, final MobCategory category, final int spawnableChunkCount) {
|
|
+ final int categoryLimit = level.getWorld().getSpawnLimitUnsafe(CraftSpawnCategory.toBukkit(category));
|
|
+ if (categoryLimit < 1) {
|
|
+ return categoryLimit;
|
|
+ }
|
|
+ return categoryLimit * spawnableChunkCount / NaturalSpawner.MAGIC_NUMBER;
|
|
+ }
|
|
+ // Paper end - Add mobcaps commands
|
|
+
|
|
public static void spawnCategoryForChunk(MobCategory group, ServerLevel world, LevelChunk chunk, NaturalSpawner.SpawnPredicate checker, NaturalSpawner.AfterSpawnCallback runner) {
|
|
BlockPos blockposition = NaturalSpawner.getRandomPosWithin(world, chunk);
|
|
|
|
diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java
|
|
index 45d887f4143444321f563cdd7d084b2b9ccf911e..33d9f3778996eedc83064332a2fbbdc7c6a8ba90 100644
|
|
--- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java
|
|
+++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java
|
|
@@ -2334,6 +2334,11 @@ public final class CraftServer implements Server {
|
|
|
|
@Override
|
|
public int getSpawnLimit(SpawnCategory spawnCategory) {
|
|
+ // Paper start - Add mobcaps commands
|
|
+ return this.getSpawnLimitUnsafe(spawnCategory);
|
|
+ }
|
|
+ public int getSpawnLimitUnsafe(final SpawnCategory spawnCategory) {
|
|
+ // Paper end - Add mobcaps commands
|
|
return this.spawnCategoryLimit.getOrDefault(spawnCategory, -1);
|
|
}
|
|
|
|
diff --git a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
|
|
index 8fdae6c7d37d2403d95d37fe0dc35f168ce0f256..a6cb8225338a6450f13116aaec587d97ecbd5197 100644
|
|
--- a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
|
|
+++ b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
|
|
@@ -1708,9 +1708,14 @@ public class CraftWorld extends CraftRegionAccessor implements World {
|
|
Preconditions.checkArgument(spawnCategory != null, "SpawnCategory cannot be null");
|
|
Preconditions.checkArgument(CraftSpawnCategory.isValidForLimits(spawnCategory), "SpawnCategory.%s are not supported", spawnCategory);
|
|
|
|
+ // Paper start - Add mobcaps commands
|
|
+ return this.getSpawnLimitUnsafe(spawnCategory);
|
|
+ }
|
|
+ public final int getSpawnLimitUnsafe(final SpawnCategory spawnCategory) {
|
|
int limit = this.spawnCategoryLimit.getOrDefault(spawnCategory, -1);
|
|
if (limit < 0) {
|
|
- limit = this.server.getSpawnLimit(spawnCategory);
|
|
+ limit = this.server.getSpawnLimitUnsafe(spawnCategory);
|
|
+ // Paper end - Add mobcaps commands
|
|
}
|
|
return limit;
|
|
}
|
|
diff --git a/src/test/java/io/papermc/paper/command/subcommands/MobcapsCommandTest.java b/src/test/java/io/papermc/paper/command/subcommands/MobcapsCommandTest.java
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..6fdc77caa74845786c78a6ba087062b4d698cb82
|
|
--- /dev/null
|
|
+++ b/src/test/java/io/papermc/paper/command/subcommands/MobcapsCommandTest.java
|
|
@@ -0,0 +1,22 @@
|
|
+package io.papermc.paper.command.subcommands;
|
|
+
|
|
+import java.util.HashSet;
|
|
+import java.util.Set;
|
|
+import net.minecraft.world.entity.MobCategory;
|
|
+import org.bukkit.support.environment.Normal;
|
|
+import org.junit.jupiter.api.Assertions;
|
|
+import org.junit.jupiter.api.Test;
|
|
+
|
|
+@Normal
|
|
+public class MobcapsCommandTest {
|
|
+ @Test
|
|
+ public void testMobCategoryColors() {
|
|
+ final Set<String> missing = new HashSet<>();
|
|
+ for (final MobCategory value : MobCategory.values()) {
|
|
+ if (!MobcapsCommand.MOB_CATEGORY_COLORS.containsKey(value)) {
|
|
+ missing.add(value.getName());
|
|
+ }
|
|
+ }
|
|
+ Assertions.assertTrue(missing.isEmpty(), "MobcapsCommand.MOB_CATEGORY_COLORS map missing TextColors for [" + String.join(", ", missing + "]"));
|
|
+ }
|
|
+}
|