mirror of
https://github.com/PaperMC/Paper.git
synced 2024-12-28 23:38:25 +01:00
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
This commit is contained in:
parent
c593e8510e
commit
b67ec825d2
6 changed files with 289 additions and 10 deletions
|
@ -62,7 +62,24 @@
|
|||
list.add(enumcreaturetype);
|
||||
}
|
||||
}
|
||||
@@ -164,9 +192,9 @@
|
||||
@@ -144,6 +172,16 @@
|
||||
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);
|
||||
|
||||
@@ -164,9 +202,9 @@
|
||||
StructureManager structuremanager = world.structureManager();
|
||||
ChunkGenerator chunkgenerator = world.getChunkSource().getGenerator();
|
||||
int i = pos.getY();
|
||||
|
@ -74,7 +91,7 @@
|
|||
BlockPos.MutableBlockPos blockposition_mutableblockposition = new BlockPos.MutableBlockPos();
|
||||
int j = 0;
|
||||
int k = 0;
|
||||
@@ -195,7 +223,7 @@
|
||||
@@ -195,7 +233,7 @@
|
||||
if (entityhuman != null) {
|
||||
double d2 = entityhuman.distanceToSqr(d0, (double) i, d1);
|
||||
|
||||
|
@ -83,7 +100,7 @@
|
|||
if (biomesettingsmobs_c == null) {
|
||||
Optional<MobSpawnSettings.SpawnerData> optional = NaturalSpawner.getRandomSpawnMobAt(world, structuremanager, chunkgenerator, group, world.random, blockposition_mutableblockposition);
|
||||
|
||||
@@ -207,7 +235,13 @@
|
||||
@@ -207,7 +245,13 @@
|
||||
j1 = biomesettingsmobs_c.minCount + world.random.nextInt(1 + biomesettingsmobs_c.maxCount - biomesettingsmobs_c.minCount);
|
||||
}
|
||||
|
||||
|
@ -98,7 +115,7 @@
|
|||
Mob entityinsentient = NaturalSpawner.getMobForSpawn(world, biomesettingsmobs_c.type);
|
||||
|
||||
if (entityinsentient == null) {
|
||||
@@ -217,10 +251,15 @@
|
||||
@@ -217,10 +261,15 @@
|
||||
entityinsentient.moveTo(d0, (double) i, d1, world.random.nextFloat() * 360.0F, 0.0F);
|
||||
if (NaturalSpawner.isValidPositionForMob(world, entityinsentient, d2)) {
|
||||
groupdataentity = entityinsentient.finalizeSpawn(world, world.getCurrentDifficultyAt(entityinsentient.blockPosition()), EntitySpawnReason.NATURAL, groupdataentity);
|
||||
|
@ -118,7 +135,7 @@
|
|||
if (j >= entityinsentient.getMaxSpawnClusterSize()) {
|
||||
return;
|
||||
}
|
||||
@@ -250,10 +289,31 @@
|
||||
@@ -250,10 +299,31 @@
|
||||
return squaredDistance <= 576.0D ? false : (world.getSharedSpawnPos().closerToCenterThan(new Vec3((double) pos.getX() + 0.5D, (double) pos.getY(), (double) pos.getZ() + 0.5D), 24.0D) ? false : Objects.equals(new ChunkPos(pos), chunk.getPos()) || world.isNaturalSpawningAllowed((BlockPos) pos));
|
||||
}
|
||||
|
||||
|
@ -152,7 +169,7 @@
|
|||
}
|
||||
|
||||
@Nullable
|
||||
@@ -268,6 +328,7 @@
|
||||
@@ -268,6 +338,7 @@
|
||||
NaturalSpawner.LOGGER.warn("Can't spawn entity of type: {}", BuiltInRegistries.ENTITY_TYPE.getKey(type));
|
||||
} catch (Exception exception) {
|
||||
NaturalSpawner.LOGGER.warn("Failed to create mob", exception);
|
||||
|
@ -160,7 +177,7 @@
|
|||
}
|
||||
|
||||
return null;
|
||||
@@ -356,6 +417,7 @@
|
||||
@@ -356,6 +427,7 @@
|
||||
entity = biomesettingsmobs_c.type.create(world.getLevel(), EntitySpawnReason.NATURAL);
|
||||
} catch (Exception exception) {
|
||||
NaturalSpawner.LOGGER.warn("Failed to create mob", exception);
|
||||
|
@ -168,7 +185,7 @@
|
|||
continue;
|
||||
}
|
||||
|
||||
@@ -369,7 +431,7 @@
|
||||
@@ -369,7 +441,7 @@
|
||||
|
||||
if (entityinsentient.checkSpawnRules(world, EntitySpawnReason.CHUNK_GENERATION) && entityinsentient.checkSpawnObstruction(world)) {
|
||||
groupdataentity = entityinsentient.finalizeSpawn(world, world.getCurrentDifficultyAt(entityinsentient.blockPosition()), EntitySpawnReason.CHUNK_GENERATION, groupdataentity);
|
||||
|
@ -177,7 +194,7 @@
|
|||
flag = true;
|
||||
}
|
||||
}
|
||||
@@ -482,10 +544,12 @@
|
||||
@@ -482,10 +554,12 @@
|
||||
return this.unmodifiableMobCategoryCounts;
|
||||
}
|
||||
|
||||
|
|
|
@ -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())))
|
||||
|
|
|
@ -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()));
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -1713,9 +1713,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;
|
||||
}
|
||||
|
|
|
@ -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 + "]"));
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue