Notify creator when his fake player is dead

This commit is contained in:
tanyaofei 2024-08-07 14:49:27 +08:00
parent 492348420d
commit a778ae4b78
13 changed files with 75 additions and 71 deletions

View File

@ -2,6 +2,7 @@ package io.github.hello09x.fakeplayer.core;
import com.google.inject.Guice; import com.google.inject.Guice;
import com.google.inject.Injector; import com.google.inject.Injector;
import io.github.hello09x.devtools.command.CommandModule;
import io.github.hello09x.devtools.core.TranslationModule; import io.github.hello09x.devtools.core.TranslationModule;
import io.github.hello09x.devtools.core.translation.TranslationConfig; import io.github.hello09x.devtools.core.translation.TranslationConfig;
import io.github.hello09x.devtools.core.translation.TranslatorUtils; import io.github.hello09x.devtools.core.translation.TranslatorUtils;
@ -39,6 +40,7 @@ public final class Main extends JavaPlugin {
injector = Guice.createInjector( injector = Guice.createInjector(
new FakeplayerModule(), new FakeplayerModule(),
new CommandModule(),
new DatabaseModule(), new DatabaseModule(),
new TranslationModule(new TranslationConfig( new TranslationModule(new TranslationConfig(
"message/message", "message/message",

View File

@ -7,6 +7,8 @@ import org.bukkit.command.CommandSender;
import org.bukkit.entity.Entity; import org.bukkit.entity.Entity;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import static net.kyori.adventure.text.Component.translatable;
@Singleton @Singleton
public class RespawnCommand extends AbstractCommand { public class RespawnCommand extends AbstractCommand {
@ -16,6 +18,7 @@ public class RespawnCommand extends AbstractCommand {
public void respawn(@NotNull CommandSender sender, @NotNull CommandArguments args) throws WrapperCommandSyntaxException { public void respawn(@NotNull CommandSender sender, @NotNull CommandArguments args) throws WrapperCommandSyntaxException {
var target = super.getTarget(sender, args, Entity::isDead); var target = super.getTarget(sender, args, Entity::isDead);
bridge.fromPlayer(target).respawn(); bridge.fromPlayer(target).respawn();
sender.sendMessage(translatable("fakeplayer.command.generic.success"));
} }
} }

View File

@ -3,8 +3,8 @@ package io.github.hello09x.fakeplayer.core.command.impl;
import com.google.common.base.Throwables; import com.google.common.base.Throwables;
import com.google.inject.Singleton; import com.google.inject.Singleton;
import dev.jorel.commandapi.executors.CommandArguments; import dev.jorel.commandapi.executors.CommandArguments;
import io.github.hello09x.devtools.core.message.IMessageException; import io.github.hello09x.devtools.command.exception.CommandException;
import io.github.hello09x.devtools.core.message.MessageException; import io.github.hello09x.devtools.command.exception.HandleCommandException;
import io.github.hello09x.fakeplayer.core.Main; import io.github.hello09x.fakeplayer.core.Main;
import io.github.hello09x.fakeplayer.core.util.Mth; import io.github.hello09x.fakeplayer.core.util.Mth;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
@ -14,6 +14,7 @@ import org.bukkit.Location;
import org.bukkit.World; import org.bukkit.World;
import org.bukkit.command.CommandSender; import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.scheduler.BukkitScheduler;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.time.Duration; import java.time.Duration;
@ -29,6 +30,7 @@ import static net.kyori.adventure.text.format.NamedTextColor.*;
public class SpawnCommand extends AbstractCommand { public class SpawnCommand extends AbstractCommand {
private final static DateTimeFormatter REMOVE_AT_FORMATTER = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm"); private final static DateTimeFormatter REMOVE_AT_FORMATTER = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm");
private final static BukkitScheduler scheduler = Bukkit.getScheduler();
private static String toLocationString(@NotNull Location location) { private static String toLocationString(@NotNull Location location) {
return location.getWorld().getName() return location.getWorld().getName()
@ -42,6 +44,7 @@ public class SpawnCommand extends AbstractCommand {
/** /**
* 创建假人 * 创建假人
*/ */
@HandleCommandException
public void spawn(@NotNull CommandSender sender, @NotNull CommandArguments args) { public void spawn(@NotNull CommandSender sender, @NotNull CommandArguments args) {
var name = (String) args.get("name"); var name = (String) args.get("name");
if (name != null && name.isEmpty()) { if (name != null && name.isEmpty()) {
@ -65,47 +68,42 @@ public class SpawnCommand extends AbstractCommand {
} }
var removedAt = Optional.ofNullable(config.getLifespan()).map(lifespan -> LocalDateTime.now().plus(lifespan)).orElse(null); var removedAt = Optional.ofNullable(config.getLifespan()).map(lifespan -> LocalDateTime.now().plus(lifespan)).orElse(null);
try { manager.spawnAsync(sender, name, spawnpoint, Optional.ofNullable(config.getLifespan()).map(Duration::toMillis).orElse(-1L))
manager.spawnAsync(sender, name, spawnpoint, Optional.ofNullable(config.getLifespan()).map(Duration::toMillis).orElse(-1L)) .thenAcceptAsync(player -> {
.thenAcceptAsync(player -> { if (player == null) {
if (player == null) { return;
return; }
Component message;
if (removedAt == null) {
message = translatable(
"fakeplayer.command.spawn.success.without-lifespan",
text(player.getName(), WHITE),
text(toLocationString(spawnpoint), WHITE)
).color(GRAY);
} else {
message = translatable(
"fakeplayer.command.spawn.success.with-lifespan",
text(player.getName(), WHITE),
text(toLocationString(spawnpoint), WHITE),
text(REMOVE_AT_FORMATTER.format(removedAt))
).color(GRAY);
}
scheduler.runTask(Main.getInstance(), () -> {
sender.sendMessage(message);
if (sender instanceof Player p && manager.countByCreator(sender) == 1) {
// 有些命令在有假人的时候才会显示, 因此需要强制刷新一下
p.updateCommands();
} }
Component message;
if (removedAt == null) {
message = translatable(
"fakeplayer.command.spawn.success.without-lifespan",
text(player.getName(), WHITE),
text(toLocationString(spawnpoint), WHITE)
).color(GRAY);
} else {
message = translatable(
"fakeplayer.command.spawn.success.with-lifespan",
text(player.getName(), WHITE),
text(toLocationString(spawnpoint), WHITE),
text(REMOVE_AT_FORMATTER.format(removedAt))
).color(GRAY);
}
Bukkit.getScheduler().runTask(Main.getInstance(), () -> {
sender.sendMessage(message);
if (sender instanceof Player p && manager.countByCreator(sender) == 1) {
// 有些命令在有假人的时候才会显示, 因此需要强制刷新一下
p.updateCommands();
}
});
}).exceptionally(e -> {
if (Throwables.getRootCause(e) instanceof IMessageException me) {
Bukkit.getScheduler().runTask(Main.getInstance(), () -> sender.sendMessage(me.getComponent()));
} else {
Bukkit.getScheduler().runTask(Main.getInstance(), () -> sender.sendMessage(translatable("fakeplayer.command.spawn.error.unknown", RED)));
log.severe(Throwables.getStackTraceAsString(e));
}
return null;
}); });
} catch (MessageException e) { }).exceptionally(e -> {
sender.sendMessage(e.getComponent()); if (Throwables.getRootCause(e) instanceof CommandException ce) {
} scheduler.runTask(Main.getInstance(), () -> sender.sendMessage(ce.component()));
} else {
scheduler.runTask(Main.getInstance(), () -> sender.sendMessage(translatable("fakeplayer.command.spawn.error.unknown", RED)));
log.severe(Throwables.getStackTraceAsString(e));
}
return null;
});
} }

View File

@ -1,6 +1,6 @@
package io.github.hello09x.fakeplayer.core.entity; package io.github.hello09x.fakeplayer.core.entity;
import io.github.hello09x.devtools.core.message.RuntimeMessageException; import io.github.hello09x.devtools.command.exception.CommandException;
import io.github.hello09x.devtools.core.utils.EntityUtils; import io.github.hello09x.devtools.core.utils.EntityUtils;
import io.github.hello09x.devtools.core.utils.SchedulerUtils; import io.github.hello09x.devtools.core.utils.SchedulerUtils;
import io.github.hello09x.devtools.core.utils.WorldUtils; import io.github.hello09x.devtools.core.utils.WorldUtils;
@ -121,7 +121,7 @@ public class FakePlayer {
.runTaskAsynchronously(Main.getInstance(), () -> { .runTaskAsynchronously(Main.getInstance(), () -> {
var event = this.callPreLoginEvent(address); var event = this.callPreLoginEvent(address);
if (event.getLoginResult() != AsyncPlayerPreLoginEvent.Result.ALLOWED) { if (event.getLoginResult() != AsyncPlayerPreLoginEvent.Result.ALLOWED) {
throw new RuntimeMessageException(translatable( throw new CommandException(translatable(
"fakeplayer.command.spawn.error.disallowed", "fakeplayer.command.spawn.error.disallowed",
text(player.getName(), WHITE), text(player.getName(), WHITE),
event.kickMessage() event.kickMessage()
@ -132,7 +132,7 @@ public class FakePlayer {
{ {
var event = this.callLoginEvent(address); var event = this.callLoginEvent(address);
if (event.getResult() != PlayerLoginEvent.Result.ALLOWED && config.getPreventKicking().ordinal() < PreventKicking.ON_SPAWNING.ordinal()) { if (event.getResult() != PlayerLoginEvent.Result.ALLOWED && config.getPreventKicking().ordinal() < PreventKicking.ON_SPAWNING.ordinal()) {
throw new RuntimeMessageException(translatable( throw new CommandException(translatable(
"fakeplayer.command.spawn.error.disallowed", RED, "fakeplayer.command.spawn.error.disallowed", RED,
text(player.getName(), WHITE), text(player.getName(), WHITE),
event.kickMessage() event.kickMessage()

View File

@ -32,8 +32,9 @@ import java.util.Optional;
import java.util.logging.Logger; import java.util.logging.Logger;
import static net.kyori.adventure.text.Component.*; import static net.kyori.adventure.text.Component.*;
import static net.kyori.adventure.text.format.NamedTextColor.GRAY; import static net.kyori.adventure.text.event.ClickEvent.runCommand;
import static net.kyori.adventure.text.format.NamedTextColor.RED; import static net.kyori.adventure.text.format.NamedTextColor.*;
import static net.kyori.adventure.text.format.TextDecoration.UNDERLINED;
@Singleton @Singleton
public class FakeplayerListener implements Listener { public class FakeplayerListener implements Listener {
@ -118,12 +119,20 @@ public class FakeplayerListener implements Listener {
* 死亡退出游戏 * 死亡退出游戏
*/ */
@EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST)
public void kickOnDead(@NotNull PlayerDeathEvent event) { public void kickOrNotifyOnDead(@NotNull PlayerDeathEvent event) {
var player = event.getPlayer(); var player = event.getPlayer();
if (manager.isNotFake(player)) { if (manager.isNotFake(player)) {
return; return;
} }
if (!config.isKickOnDead()) { if (!config.isKickOnDead()) {
var creator = manager.getCreator(player);
if (creator != null) {
creator.sendMessage(translatable(
"fakeplayer.listener.death.notify",
text(player.getName(), GOLD),
text("/fp respawn", DARK_GREEN, UNDERLINED).clickEvent(runCommand("/fp respawn " + player.getName()))
).color(RED));
}
return; return;
} }

View File

@ -2,8 +2,7 @@ package io.github.hello09x.fakeplayer.core.manager;
import com.google.inject.Inject; import com.google.inject.Inject;
import com.google.inject.Singleton; import com.google.inject.Singleton;
import io.github.hello09x.devtools.core.message.MessageException; import io.github.hello09x.devtools.command.exception.CommandException;
import io.github.hello09x.devtools.core.message.RuntimeMessageException;
import io.github.hello09x.devtools.core.utils.Exceptions; import io.github.hello09x.devtools.core.utils.Exceptions;
import io.github.hello09x.devtools.core.utils.MetadataUtils; import io.github.hello09x.devtools.core.utils.MetadataUtils;
import io.github.hello09x.fakeplayer.api.spi.Action; import io.github.hello09x.fakeplayer.api.spi.Action;
@ -15,8 +14,6 @@ import io.github.hello09x.fakeplayer.core.entity.FakePlayer;
import io.github.hello09x.fakeplayer.core.entity.SpawnOption; import io.github.hello09x.fakeplayer.core.entity.SpawnOption;
import io.github.hello09x.fakeplayer.core.manager.invsee.Invsee; import io.github.hello09x.fakeplayer.core.manager.invsee.Invsee;
import io.github.hello09x.fakeplayer.core.manager.naming.NameManager; import io.github.hello09x.fakeplayer.core.manager.naming.NameManager;
import io.github.hello09x.fakeplayer.core.manager.naming.SequenceName;
import io.github.hello09x.fakeplayer.core.manager.naming.exception.IllegalCustomNameException;
import io.github.hello09x.fakeplayer.core.repository.model.Config; import io.github.hello09x.fakeplayer.core.repository.model.Config;
import io.github.hello09x.fakeplayer.core.util.AddressUtils; import io.github.hello09x.fakeplayer.core.util.AddressUtils;
import io.github.hello09x.fakeplayer.core.util.Commands; import io.github.hello09x.fakeplayer.core.util.Commands;
@ -93,15 +90,10 @@ public class FakeplayerManager {
@Nullable String name, @Nullable String name,
@NotNull Location spawnAt, @NotNull Location spawnAt,
long lifespan long lifespan
) throws MessageException { ) {
this.checkLimit(creator); this.checkLimit(creator);
SequenceName sn; var sn = name == null ? nameManager.register(creator) : nameManager.specify(name);
try {
sn = name == null ? nameManager.register(creator) : nameManager.specify(name);
} catch (IllegalCustomNameException e) {
throw new RuntimeMessageException(e.getMessage());
}
var fp = new FakePlayer( var fp = new FakePlayer(
creator, creator,
@ -527,23 +519,22 @@ public class FakeplayerManager {
* 检测限制, 不满足条件则抛出异常 * 检测限制, 不满足条件则抛出异常
* *
* @param creator 创建者 * @param creator 创建者
* @throws MessageException 消息
*/ */
private void checkLimit(@NotNull CommandSender creator) throws MessageException { private void checkLimit(@NotNull CommandSender creator) throws CommandException {
if (creator.isOp()) { if (creator.isOp()) {
return; return;
} }
if (this.playerList.count() >= this.config.getServerLimit()) { if (this.playerList.count() >= this.config.getServerLimit()) {
throw new MessageException(translatable("fakeplayer.command.spawn.error.server-limit")); throw new CommandException(translatable("fakeplayer.command.spawn.error.server-limit"));
} }
if (this.playerList.getByCreator(creator.getName()).size() >= this.config.getPlayerLimit()) { if (this.playerList.getByCreator(creator.getName()).size() >= this.config.getPlayerLimit()) {
throw new MessageException(translatable("fakeplayer.command.spawn.error.player-limit")); throw new CommandException(translatable("fakeplayer.command.spawn.error.player-limit"));
} }
if (this.config.isDetectIp() && this.countByAddress(AddressUtils.getAddress(creator)) >= this.config.getPlayerLimit()) { if (this.config.isDetectIp() && this.countByAddress(AddressUtils.getAddress(creator)) >= this.config.getPlayerLimit()) {
throw new MessageException(translatable("fakeplayer.command.spawn.error.ip-limit")); throw new CommandException(translatable("fakeplayer.command.spawn.error.ip-limit"));
} }
} }

View File

@ -1,18 +1,13 @@
package io.github.hello09x.fakeplayer.core.manager.naming.exception; package io.github.hello09x.fakeplayer.core.manager.naming.exception;
import lombok.Getter; import io.github.hello09x.devtools.command.exception.CommandException;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
public class IllegalCustomNameException extends IllegalArgumentException { public class IllegalCustomNameException extends CommandException {
@Getter
private final Component text;
public IllegalCustomNameException(@NotNull Component message) { public IllegalCustomNameException(@NotNull Component message) {
super(PlainTextComponentSerializer.plainText().serialize(message)); super(message);
this.text = message;
} }
} }

View File

@ -128,6 +128,7 @@ fakeplayer.direction.south=south
fakeplayer.direction.up=up fakeplayer.direction.up=up
fakeplayer.direction.west=west fakeplayer.direction.west=west
fakeplayer.listener.login.deny-used-uuid=Your UUID was used by fake player, disallow to login fakeplayer.listener.login.deny-used-uuid=Your UUID was used by fake player, disallow to login
fakeplayer.listener.death.notify=Fake player {0} is dead, you could use {1} to respawn him
fakeplayer.manager.inventory.title=Inventory of {0} fakeplayer.manager.inventory.title=Inventory of {0}
fakeplayer.manager.remove-all-on-low-tps=[Low tps, all fake players have been removed] fakeplayer.manager.remove-all-on-low-tps=[Low tps, all fake players have been removed]
fakeplayer.spawn.error.name.existed=This name already existed fakeplayer.spawn.error.name.existed=This name already existed

View File

@ -127,6 +127,7 @@ fakeplayer.direction.north=north
fakeplayer.direction.south=south fakeplayer.direction.south=south
fakeplayer.direction.up=up fakeplayer.direction.up=up
fakeplayer.direction.west=west fakeplayer.direction.west=west
fakeplayer.listener.death.notify=Fake player {0} is dead, you could use {1} to respawn him
fakeplayer.listener.login.deny-used-uuid=Your UUID was used by fake player, disallow to login fakeplayer.listener.login.deny-used-uuid=Your UUID was used by fake player, disallow to login
fakeplayer.manager.inventory.title=Inventory of {0} fakeplayer.manager.inventory.title=Inventory of {0}
fakeplayer.manager.remove-all-on-low-tps=[Low tps, all fake players have been removed] fakeplayer.manager.remove-all-on-low-tps=[Low tps, all fake players have been removed]

View File

@ -127,6 +127,7 @@ fakeplayer.direction.north=\u5317\u65B9
fakeplayer.direction.south=\u5357\u65B9 fakeplayer.direction.south=\u5357\u65B9
fakeplayer.direction.up=\u4E0A\u65B9 fakeplayer.direction.up=\u4E0A\u65B9
fakeplayer.direction.west=\u897F\u65B9 fakeplayer.direction.west=\u897F\u65B9
fakeplayer.listener.death.notify=\u5047\u4EBA {0} \u6B7B\u4EA1, \u53EF\u4EE5\u4F7F\u7528 {1} \u53BB\u590D\u6D3B\u4ED6
fakeplayer.listener.login.deny-used-uuid=\u4F60\u7684 UUID \u88AB\u4F7F\u7528\u8FC7, \u4E0D\u80FD\u767B\u5F55\u670D\u52A1\u5668 fakeplayer.listener.login.deny-used-uuid=\u4F60\u7684 UUID \u88AB\u4F7F\u7528\u8FC7, \u4E0D\u80FD\u767B\u5F55\u670D\u52A1\u5668
fakeplayer.manager.inventory.title={0} \u7684\u7269\u54C1\u680F fakeplayer.manager.inventory.title={0} \u7684\u7269\u54C1\u680F
fakeplayer.manager.remove-all-on-low-tps=[\u670D\u52A1\u5668\u8FC7\u4E8E\u5361\u987F, \u5DF2\u79FB\u9664\u6240\u6709\u5047\u4EBA] fakeplayer.manager.remove-all-on-low-tps=[\u670D\u52A1\u5668\u8FC7\u4E8E\u5361\u987F, \u5DF2\u79FB\u9664\u6240\u6709\u5047\u4EBA]

View File

@ -127,6 +127,7 @@ fakeplayer.direction.north=\u5317\u65B9
fakeplayer.direction.south=\u5357\u65B9 fakeplayer.direction.south=\u5357\u65B9
fakeplayer.direction.up=\u4E0A\u65B9 fakeplayer.direction.up=\u4E0A\u65B9
fakeplayer.direction.west=\u897F\u65B9 fakeplayer.direction.west=\u897F\u65B9
fakeplayer.listener.death.notify=\u5047\u4EBA {0} \u6B7B\u4EA1, \u53EF\u4EE5\u4F7F\u7528 {1} \u6765\u590D\u6D3B\u4ED6
fakeplayer.listener.login.deny-used-uuid=\u4F60\u7684 UUID \u88AB\u4F7F\u7528\u8FC7, \u4E0D\u80FD\u767B\u5F55\u670D\u52A1\u5668 fakeplayer.listener.login.deny-used-uuid=\u4F60\u7684 UUID \u88AB\u4F7F\u7528\u8FC7, \u4E0D\u80FD\u767B\u5F55\u670D\u52A1\u5668
fakeplayer.manager.inventory.title={0} \u7684\u7269\u54C1\u680F fakeplayer.manager.inventory.title={0} \u7684\u7269\u54C1\u680F
fakeplayer.manager.remove-all-on-low-tps=[\u670D\u52A1\u5668\u8FC7\u4E8E\u5361\u987F, \u5DF2\u79FB\u9664\u6240\u6709\u5047\u4EBA] fakeplayer.manager.remove-all-on-low-tps=[\u670D\u52A1\u5668\u8FC7\u4E8E\u5361\u987F, \u5DF2\u79FB\u9664\u6240\u6709\u5047\u4EBA]

View File

@ -127,6 +127,7 @@ fakeplayer.direction.north=\u5317\u65B9
fakeplayer.direction.south=\u5357\u65B9 fakeplayer.direction.south=\u5357\u65B9
fakeplayer.direction.up=\u4E0A\u65B9 fakeplayer.direction.up=\u4E0A\u65B9
fakeplayer.direction.west=\u897F\u65B9 fakeplayer.direction.west=\u897F\u65B9
fakeplayer.listener.death.notify=\u5047\u4EBA {0} \u6B7B\u4EA1, \u53EF\u4EE5\u4F7F\u7528 {1} \u569F\u5FA9\u6D3B\u4F62
fakeplayer.listener.login.deny-used-uuid=\u4F60\u5605 UUID \u88AB\u4F7F\u7528\u904E, \u5514\u53EF\u4EE5\u767B\u9304\u4F3A\u670D\u5668 fakeplayer.listener.login.deny-used-uuid=\u4F60\u5605 UUID \u88AB\u4F7F\u7528\u904E, \u5514\u53EF\u4EE5\u767B\u9304\u4F3A\u670D\u5668
fakeplayer.manager.inventory.title={0} \u7684\u7269\u54C1\u6B04 fakeplayer.manager.inventory.title={0} \u7684\u7269\u54C1\u6B04
fakeplayer.manager.remove-all-on-low-tps=[\u7531\u4E8E\u4F3A\u670D\u5668 lag \u6A5F, \u5DF2\u79FB\u9664\u6240\u6709\u5047\u4EBA] fakeplayer.manager.remove-all-on-low-tps=[\u7531\u4E8E\u4F3A\u670D\u5668 lag \u6A5F, \u5DF2\u79FB\u9664\u6240\u6709\u5047\u4EBA]

View File

@ -127,6 +127,7 @@ fakeplayer.direction.north=\u5317\u65B9
fakeplayer.direction.south=\u5357\u65B9 fakeplayer.direction.south=\u5357\u65B9
fakeplayer.direction.up=\u4E0A\u65B9 fakeplayer.direction.up=\u4E0A\u65B9
fakeplayer.direction.west=\u897F\u65B9 fakeplayer.direction.west=\u897F\u65B9
fakeplayer.listener.death.notify=\u5047\u4EBA {0} \u6B7B\u4EA1, \u53EF\u4EE5\u4F7F\u7528 \u00A7f/fp respawn\u00A7r \u4F86\u5FA9\u6D3B\u4ED6
fakeplayer.listener.login.deny-used-uuid=\u4F60\u7684 UUID \u88AB\u4F7F\u7528\u904E, \u4E0D\u80FD\u767B\u9304\u4F3A\u670D\u5668 fakeplayer.listener.login.deny-used-uuid=\u4F60\u7684 UUID \u88AB\u4F7F\u7528\u904E, \u4E0D\u80FD\u767B\u9304\u4F3A\u670D\u5668
fakeplayer.manager.inventory.title={0} \u7684\u7269\u54C1\u6B04 fakeplayer.manager.inventory.title={0} \u7684\u7269\u54C1\u6B04
fakeplayer.manager.remove-all-on-low-tps=[\u4F3A\u670D\u5668\u904E\u65BC\u5361\u9813, \u5DF2\u79FB\u9664\u6240\u6709\u5047\u4EBA] fakeplayer.manager.remove-all-on-low-tps=[\u4F3A\u670D\u5668\u904E\u65BC\u5361\u9813, \u5DF2\u79FB\u9664\u6240\u6709\u5047\u4EBA]