default-features config

This commit is contained in:
tanyaofei 2024-08-13 15:03:38 +08:00
parent 11b9326090
commit 859519eb3f
28 changed files with 487 additions and 498 deletions

View File

@ -1,7 +1,12 @@
package io.github.hello09x.fakeplayer.api.spi;
import org.jetbrains.annotations.NotNull;
public interface ActionTicker {
@NotNull
ActionSetting getSetting();
/**
* 时刻计算
*

View File

@ -99,7 +99,7 @@ public final class Main extends JavaPlugin {
}
} catch (Throwable e) {
getLogger().warning("检测新版本发生异常: " + e.getMessage());
getLogger().warning("Error on checking for updates: " + e.getMessage());
}
});
}

View File

@ -10,7 +10,7 @@ import io.github.hello09x.fakeplayer.api.spi.ActionType;
import io.github.hello09x.fakeplayer.core.command.impl.*;
import io.github.hello09x.fakeplayer.core.config.FakeplayerConfig;
import io.github.hello09x.fakeplayer.core.constant.Direction;
import io.github.hello09x.fakeplayer.core.repository.model.Config;
import io.github.hello09x.fakeplayer.core.repository.model.FeatureKey;
import org.bukkit.entity.Entity;
import org.bukkit.entity.LivingEntity;
@ -172,8 +172,8 @@ public class CommandRegistry {
.withShortDescription("fakeplayer.command.set.description")
.withPermission(Permission.set)
.withArguments(
config("config", Config::hasAccessor),
configValue("config", "value")
configKey("feature", FeatureKey::hasModifier),
configValue("feature", "option")
)
.withOptionalArguments(fakeplayer("name"))
.executes(setCommand::set),
@ -183,13 +183,13 @@ public class CommandRegistry {
.withSubcommands(
command("set")
.withArguments(
config("config"),
configValue("config", "value"))
configKey("feature"),
configValue("feature", "option"))
.executesPlayer(configCommand::setConfig),
command("list")
.executes(configCommand::listConfig)
.executesPlayer(configCommand::listConfig)
)
.executes(configCommand::listConfig),
.executesPlayer(configCommand::listConfig),
command("expme")
.withPermission(Permission.expme)

View File

@ -11,13 +11,16 @@ import io.github.hello09x.fakeplayer.core.Main;
import io.github.hello09x.fakeplayer.core.command.impl.ActionCommand;
import io.github.hello09x.fakeplayer.core.config.FakeplayerConfig;
import io.github.hello09x.fakeplayer.core.manager.FakeplayerManager;
import io.github.hello09x.fakeplayer.core.repository.model.Config;
import io.github.hello09x.fakeplayer.core.repository.model.FeatureKey;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.CompletableFuture;
import java.util.function.Predicate;
import java.util.stream.Stream;
@ -123,42 +126,56 @@ public abstract class CommandSupports {
}));
}
public static @NotNull Argument<Config<Object>> config(@NotNull String nodeName) {
return config(nodeName, null);
public static @NotNull Argument<FeatureKey> configKey(@NotNull String nodeName) {
return configKey(nodeName, ignored -> true);
}
public static @NotNull Argument<Config<Object>> config(@NotNull String nodeName, @Nullable Predicate<Config<?>> predicate) {
public static @NotNull Argument<FeatureKey> configKey(@NotNull String nodeName, @NotNull Predicate<FeatureKey> predicate) {
return new CustomArgument<>(new StringArgument(nodeName), info -> {
var arg = info.currentInput();
Config<Object> config;
FeatureKey key;
try {
config = io.github.hello09x.fakeplayer.core.repository.model.Config.valueOf(arg);
key = FeatureKey.valueOf(arg);
} catch (Exception e) {
throw CustomArgument.CustomArgumentException.fromAdventureComponent(translatable("fakeplayer.command.config.set.error.invalid-option"));
throw CustomArgument.CustomArgumentException.fromAdventureComponent(translatable("fakeplayer.command.config.set.error.invalid-key"));
}
if (predicate != null && !predicate.test(config)) {
throw CustomArgument.CustomArgumentException.fromAdventureComponent(translatable("fakeplayer.command.config.set.error.invalid-option"));
if (!predicate.test(key)) {
throw CustomArgument.CustomArgumentException.fromAdventureComponent(translatable("fakeplayer.command.config.set.error.invalid-key"));
}
return config;
}).replaceSuggestions(ArgumentSuggestions.strings(Arrays.stream(Config.values()).filter(Optional.ofNullable(predicate).orElse(ignored -> true)).map(io.github.hello09x.fakeplayer.core.repository.model.Config::key).toList()));
if (!key.testPermissions(info.sender())) {
throw CustomArgument.CustomArgumentException.fromAdventureComponent(translatable("fakeplayer.command.config.set.error.no-permission"));
}
return key;
}).replaceSuggestions(ArgumentSuggestions.strings(Arrays.stream(FeatureKey.values()).filter(predicate).map(Enum::name).toArray(String[]::new)));
}
public static @NotNull Argument<Object> configValue(@NotNull String configNodeName, @NotNull String nodeName) {
return new CustomArgument<>(new StringArgument(nodeName), info -> {
@SuppressWarnings("unchecked")
var config = Objects.requireNonNull((Config<Object>) info.previousArgs().get(configNodeName));
public static @NotNull Argument<String> configValue(@NotNull String configKeyNodeName, @NotNull String nodeName) {
return new CustomArgument<String, String>(new StringArgument(nodeName), info -> {
var key = (FeatureKey) info.previousArgs().get(configKeyNodeName);
if (key == null) {
throw CustomArgument.CustomArgumentException.fromAdventureComponent(translatable("fakeplayer.command.config.set.error.invalid-key"));
}
var arg = info.currentInput();
if (!config.options().contains(arg)) {
if (!key.getOptions().contains(arg)) {
throw CustomArgument.CustomArgumentException.fromAdventureComponent(translatable("fakeplayer.command.config.set.error.invalid-value"));
}
return config.parser().apply(arg);
}).replaceSuggestions(ArgumentSuggestions.stringsAsync(info -> CompletableFuture.supplyAsync(() -> {var config = Objects.requireNonNull((io.github.hello09x.fakeplayer.core.repository.model.Config<?>) info.previousArgs().get(configNodeName));
var arg = info.currentArg().toLowerCase();
var options = config.options().stream();
if (!arg.isEmpty()) {
options = options.filter(o -> o.toLowerCase().contains(arg));
return arg;
}).replaceSuggestions(ArgumentSuggestions.stringCollectionAsync(info -> CompletableFuture.supplyAsync(() -> {
var key = (FeatureKey) info.previousArgs().get(configKeyNodeName);
if (key == null) {
return Collections.emptyList();
}
return options.toArray(String[]::new);
var arg = info.currentArg().toLowerCase(Locale.ENGLISH);
if (arg.isEmpty()) {
return key.getOptions();
}
return key.getOptions().stream().filter(option -> option.contains(arg)).toList();
})));
}

View File

@ -8,17 +8,14 @@ import dev.jorel.commandapi.executors.CommandArguments;
import io.github.hello09x.devtools.core.translation.TranslatorUtils;
import io.github.hello09x.devtools.core.utils.ComponentUtils;
import io.github.hello09x.fakeplayer.core.Main;
import io.github.hello09x.fakeplayer.core.manager.UserConfigManager;
import io.github.hello09x.fakeplayer.core.repository.model.Config;
import io.github.hello09x.fakeplayer.core.manager.feature.FakeplayerFeatureManager;
import io.github.hello09x.fakeplayer.core.repository.model.FeatureKey;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.Style;
import org.bukkit.Bukkit;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
@ -31,57 +28,52 @@ import static net.kyori.adventure.text.format.TextDecoration.UNDERLINED;
@Singleton
public class ConfigCommand extends AbstractCommand {
private final UserConfigManager configManager;
private final FakeplayerFeatureManager featureManager;
@Inject
public ConfigCommand(UserConfigManager configManager) {
this.configManager = configManager;
public ConfigCommand(FakeplayerFeatureManager featureManager) {
this.featureManager = featureManager;
}
/**
* 设置配置
*/
public void setConfig(@NotNull Player sender, @NotNull CommandArguments args) throws WrapperCommandSyntaxException {
@SuppressWarnings("unchecked")
var config = (Config<Object>) Objects.requireNonNull(args.get("config"));
if (!config.hasPermission(sender)) {
var key = (FeatureKey) Objects.requireNonNull(args.get("feature"));
if (!key.testPermissions(sender)) {
throw CommandAPI.failWithString(ComponentUtils.toString(
translatable("fakeplayer.command.config.set.error.no-permission"),
TranslatorUtils.getLocale(sender)
));
}
var value = Objects.requireNonNull(args.get("value"));
configManager.setConfig(sender, config, value);
var option = (String) Objects.requireNonNull(args.get("option"));
featureManager.setFeature(sender, key, option);
sender.sendMessage(translatable(
"fakeplayer.command.config.set.success",
translatable(config.translationKey(), GOLD),
text(value.toString(), WHITE)
translatable(key.translationKey(), GOLD),
text(option, WHITE)
).color(GRAY));
}
/**
* 获取所有配置
*/
public void listConfig(@NotNull CommandSender sender, @NotNull CommandArguments args) {
public void listConfig(@NotNull Player sender, @NotNull CommandArguments args) {
CompletableFuture.runAsync(() -> {
var components = Arrays.stream(Config.values()).map(config -> {
var options = new ArrayList<>(config.options());
var value = String.valueOf(configManager.getConfig(sender, config));
return textOfChildren(
translatable(config, GOLD),
text(": ", GRAY),
join(separator(space()), options.stream().map(option -> {
var style = option.equals(value) ? Style.style(GREEN, UNDERLINED) : Style.style(GRAY);
return text("[" + option + "]").style(style).clickEvent(
runCommand("/fp config set " + config.key() + " " + option)
);
}).toList())
);
}).toList();
var message = Component.join(separator(newline()), components);
var lines = featureManager.getFeatures(sender).values().stream().map(feature -> textOfChildren(
translatable(feature.key(), GOLD),
text(": ", GRAY),
join(separator(space()), feature.key().getOptions().stream().map(option -> {
var style = option.equals(feature.value())
? Style.style(GREEN, UNDERLINED)
: Style.style(GRAY);
return text("[" + option + "]").style(style).clickEvent(
runCommand("/fp config set " + feature.key() + " " + option)
);
}).toList())
)).toList();
var message = Component.join(separator(newline()), lines);
Bukkit.getScheduler().runTask(Main.getInstance(), () -> sender.sendMessage(message));
});
}

View File

@ -8,9 +8,8 @@ import io.github.hello09x.fakeplayer.core.config.FakeplayerConfig;
import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.NotNull;
import static net.kyori.adventure.text.Component.text;
import static net.kyori.adventure.text.Component.translatable;
import static net.kyori.adventure.text.format.NamedTextColor.*;
import static net.kyori.adventure.text.format.NamedTextColor.GRAY;
@Singleton
public class ReloadCommand extends AbstractCommand {
@ -27,15 +26,8 @@ public class ReloadCommand extends AbstractCommand {
public void reload(@NotNull CommandSender sender, @NotNull CommandArguments args) {
config.reload();
if (!config.isFileExists()) {
sender.sendMessage(translatable(
"fakeplayer.command.reload.config-file-not-found",
text(FakeplayerConfig.CONFIG_FILE_NAME, WHITE),
text(FakeplayerConfig.CONFIG_TMPL_FILE_NAME, WHITE)
).color(GOLD));
}
sender.sendMessage(translatable("fakeplayer.command.generic.success", GRAY));
if (config.isFileConfigurationOutOfDate()) {
if (config.isConfigFileOutOfDate()) {
sender.sendMessage(translatable("fakeplayer.configuration.out-of-date", GRAY));
}
}

View File

@ -3,7 +3,7 @@ package io.github.hello09x.fakeplayer.core.command.impl;
import com.google.inject.Singleton;
import dev.jorel.commandapi.exceptions.WrapperCommandSyntaxException;
import dev.jorel.commandapi.executors.CommandArguments;
import io.github.hello09x.fakeplayer.core.repository.model.Config;
import io.github.hello09x.fakeplayer.core.repository.model.FeatureKey;
import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.NotNull;
@ -18,26 +18,24 @@ public class SetCommand extends AbstractCommand {
public void set(@NotNull CommandSender sender, @NotNull CommandArguments args) throws WrapperCommandSyntaxException {
var target = super.getFakeplayer(sender, args);
var key = (FeatureKey) Objects.requireNonNull(args.get("feature"));
var value = (String) Objects.requireNonNull(args.get("option"));
@SuppressWarnings("unchecked")
var config = Objects.requireNonNull((Config<Object>) args.get("config"));
if (!config.hasPermission(sender)) {
sender.sendMessage(translatable("fakeplayer.command.config.set.error.no-permission", RED));
var modifier = key.getModifier();
if (modifier == null) {
sender.sendMessage(translatable("fakeplayer.command.config.set.error.invalid-key", RED));
return;
}
if (!config.hasAccessor()) {
sender.sendMessage(translatable("fakeplayer.command.config.set.error.invalid-option", RED));
return;
}
var value = Objects.requireNonNull(args.get("value"));
config.accessor().setter().accept(target, value);
modifier.accept(target, value);
sender.sendMessage(translatable(
"fakeplayer.command.set.success",
text(target.getName(), WHITE),
translatable(config, GOLD),
text(value.toString(), WHITE)
translatable(key, GOLD),
text(value, WHITE)
).color(GRAY));
}
}

View File

@ -5,7 +5,7 @@ import dev.jorel.commandapi.exceptions.WrapperCommandSyntaxException;
import dev.jorel.commandapi.executors.CommandArguments;
import io.github.hello09x.devtools.core.utils.ExperienceUtils;
import io.github.hello09x.fakeplayer.core.command.Permission;
import io.github.hello09x.fakeplayer.core.repository.model.Config;
import io.github.hello09x.fakeplayer.core.repository.model.FeatureKey;
import io.github.hello09x.fakeplayer.core.util.Mth;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.JoinConfiguration;
@ -19,11 +19,10 @@ import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Optional;
import java.util.stream.Collectors;
import static net.kyori.adventure.text.Component.*;
import static net.kyori.adventure.text.JoinConfiguration.separator;
import static net.kyori.adventure.text.event.ClickEvent.runCommand;
import static net.kyori.adventure.text.format.NamedTextColor.*;
import static net.kyori.adventure.text.format.TextDecoration.UNDERLINED;
@ -68,7 +67,7 @@ public class StatusCommand extends AbstractCommand {
lines.add(this.getExperienceLine(fake));
}
lines.add(LINE_SPLITTER);
lines.add(getConfigLine(fake));
lines.add(getFeatureLine(fake));
sender.sendMessage(join(JoinConfiguration.newlines(), lines));
}
@ -117,23 +116,26 @@ public class StatusCommand extends AbstractCommand {
);
}
private @NotNull Component getConfigLine(@NotNull Player target) {
var configs = Arrays.stream(Config.values()).filter(Config::hasAccessor).toList();
private @NotNull Component getFeatureLine(@NotNull Player faker) {
var messages = new ArrayList<Component>();
for (var config : configs) {
var name = translatable(config.translationKey(), WHITE);
var options = config.options();
var status = config.accessor().getter().apply(target).toString();
for (var key : FeatureKey.values()) {
var detector = key.getDetector();
if (detector == null) {
continue;
}
var name = translatable(key, WHITE);
var options = key.getOptions();
var status = detector.apply(faker);
messages.add(textOfChildren(
name,
space(),
join(JoinConfiguration.separator(space()), options.stream().map(option -> {
join(separator(space()), options.stream().map(option -> {
var style = option.equals(status) ? Style.style(GREEN, UNDERLINED) : Style.style(GRAY);
return text("[" + option + "]").style(style).clickEvent(
runCommand("/fp set %s %s %s".formatted(config.key(), option, target.getName()))
runCommand("/fp set %s %s %s".formatted(key.name(), option, faker.getName()))
);
}).collect(Collectors.toList()))
}).toList())
));
}
return join(JoinConfiguration.newlines(), messages);

View File

@ -7,6 +7,7 @@ import com.google.inject.Singleton;
import io.github.hello09x.devtools.core.config.ConfigUtils;
import io.github.hello09x.devtools.core.config.PluginConfig;
import io.github.hello09x.fakeplayer.core.Main;
import io.github.hello09x.fakeplayer.core.repository.model.FeatureKey;
import lombok.Getter;
import lombok.ToString;
import org.bukkit.Bukkit;
@ -15,8 +16,11 @@ import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.time.Duration;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.logging.Logger;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
@ -33,6 +37,8 @@ public class FakeplayerConfig extends PluginConfig {
private final static String defaultNameChars = "^[a-zA-Z0-9_]+$";
public final static String SECTION_KEY_DEFAULT_FEATURES = "default-features";
/**
* 每位玩家最多多少个假人
*/
@ -151,6 +157,8 @@ public class FakeplayerConfig extends PluginConfig {
@Beta
private boolean defaultOnlineSkin;
private Map<FeatureKey, String> defaultFeatures;
@Inject
public FakeplayerConfig() {
super(Main.getInstance());
@ -189,10 +197,12 @@ public class FakeplayerConfig extends PluginConfig {
.collect(Collectors.toSet());
this.defaultOnlineSkin = file.getBoolean("default-online-skin", false);
this.defaultFeatures = Arrays.stream(FeatureKey.values())
.collect(Collectors.toMap(Function.identity(), key -> file.getString("default-features." + key.name(), key.getDefaultOption())));
this.invseeImplement = ConfigUtils.getEnum(file, "invsee-implement", InvseeImplement.class, InvseeImplement.AUTO);
this.debug = file.getBoolean("debug", false);
if (this.isFileConfigurationOutOfDate()) {
if (this.isConfigFileOutOfDate()) {
Bukkit.getScheduler().runTaskLater(Main.getInstance(), () -> {
if (Main.getInstance().isEnabled()) {
Main.getInstance().getComponentLogger().warn(translatable("fakeplayer.configuration.out-of-date"));

View File

@ -10,7 +10,6 @@ import io.github.hello09x.fakeplayer.core.config.FakeplayerConfig;
import io.github.hello09x.fakeplayer.core.config.PreventKicking;
import io.github.hello09x.fakeplayer.core.constant.MetadataKeys;
import io.github.hello09x.fakeplayer.core.manager.FakeplayerAutofishManager;
import io.github.hello09x.fakeplayer.core.manager.FakeplayerManager;
import io.github.hello09x.fakeplayer.core.manager.FakeplayerReplenishManager;
import io.github.hello09x.fakeplayer.core.manager.FakeplayerSkinManager;
import io.github.hello09x.fakeplayer.core.manager.action.ActionManager;
@ -44,10 +43,10 @@ public class Fakeplayer {
private final static InternalAddressGenerator ipGen = new InternalAddressGenerator();
private final static FakeplayerConfig config = Main.getInjector().getInstance(FakeplayerConfig.class);
private final static NMSBridge bridge = Main.getInjector().getInstance(NMSBridge.class);
private final static FakeplayerManager manager = Main.getInjector().getInstance(FakeplayerManager.class);
private final static FakeplayerSkinManager skinManager = Main.getInjector().getInstance(FakeplayerSkinManager.class);
private final static FakeplayerReplenishManager replenishManager = Main.getInjector().getInstance(FakeplayerReplenishManager.class);
private final static FakeplayerAutofishManager autofishManager = Main.getInjector().getInstance(FakeplayerAutofishManager.class);
private final static ActionManager actionManager = Main.getInjector().getInstance(ActionManager.class);
@NotNull
@ -73,15 +72,15 @@ public class Fakeplayer {
@NotNull
private final FakeplayerTicker ticker;
@NotNull
@Getter
@NotNull
private final String name;
@NotNull
private final UUID uuid;
@UnknownNullability
@Getter
@UnknownNullability
private NMSNetwork network;
/**
@ -152,7 +151,7 @@ public class Fakeplayer {
this.player.setCollidable(option.collidable());
this.player.setCanPickupItems(option.pickupItems());
if (option.lookAtEntity()) {
Main.getInjector().getInstance(ActionManager.class).setAction(player, ActionType.LOOK_AT_NEAREST_ENTITY, ActionSetting.continuous());
actionManager.setAction(player, ActionType.LOOK_AT_NEAREST_ENTITY, ActionSetting.continuous());
}
if (option.skin()) {
skinManager.useDefaultSkin(creator, player);

View File

@ -12,6 +12,7 @@ import org.jetbrains.annotations.NotNull;
* @param replenish 自动补货
*/
public record SpawnOption(
@NotNull
Location spawnAt,

View File

@ -2,6 +2,7 @@ package io.github.hello09x.fakeplayer.core.entity.action;
import io.github.hello09x.fakeplayer.api.spi.*;
import io.github.hello09x.fakeplayer.core.entity.action.impl.*;
import lombok.Getter;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.UnknownNullability;
@ -17,6 +18,7 @@ public abstract class BaseActionTicker implements ActionTicker {
protected Action action;
@NotNull
@Getter
protected ActionSetting setting;
public BaseActionTicker(NMSBridge nms, @NotNull Player player, @NotNull ActionType action, @NotNull ActionSetting setting) {

View File

@ -13,8 +13,9 @@ import io.github.hello09x.fakeplayer.core.config.FakeplayerConfig;
import io.github.hello09x.fakeplayer.core.constant.MetadataKeys;
import io.github.hello09x.fakeplayer.core.entity.Fakeplayer;
import io.github.hello09x.fakeplayer.core.entity.SpawnOption;
import io.github.hello09x.fakeplayer.core.manager.feature.FakeplayerFeatureManager;
import io.github.hello09x.fakeplayer.core.manager.naming.NameManager;
import io.github.hello09x.fakeplayer.core.repository.model.Config;
import io.github.hello09x.fakeplayer.core.repository.model.FeatureKey;
import io.github.hello09x.fakeplayer.core.util.AddressUtils;
import io.github.hello09x.fakeplayer.core.util.Commands;
import net.kyori.adventure.text.Component;
@ -51,16 +52,16 @@ public class FakeplayerManager {
private final NameManager nameManager;
private final FakeplayerList playerList;
private final UserConfigManager configManager;
private final FakeplayerFeatureManager featureManager;
private final NMSBridge nms;
private final FakeplayerConfig config;
private final ScheduledExecutorService lagMonitor;
@Inject
public FakeplayerManager(NameManager nameManager, FakeplayerList playerList, UserConfigManager configManager, NMSBridge nms, FakeplayerConfig config) {
public FakeplayerManager(NameManager nameManager, FakeplayerList playerList, FakeplayerFeatureManager featureManager, NMSBridge nms, FakeplayerConfig config) {
this.nameManager = nameManager;
this.playerList = playerList;
this.configManager = configManager;
this.featureManager = featureManager;
this.nms = nms;
this.config = config;
@ -107,16 +108,16 @@ public class FakeplayerManager {
this.dispatchCommandsEarly(fp, this.config.getPreSpawnCommands());
return CompletableFuture
.supplyAsync(() -> {
var configs = configManager.getConfigs(creator);
var configs = featureManager.getFeatures(creator);
return new SpawnOption(
spawnAt,
configs.getOrDefault(Config.invulnerable),
configs.getOrDefault(Config.collidable),
configs.getOrDefault(Config.look_at_entity),
configs.getOrDefault(Config.pickup_items),
configs.getOrDefault(Config.skin),
configs.getOrDefault(Config.replenish),
configs.getOrDefault(Config.autofish)
configs.get(FeatureKey.invulnerable).asBoolean(),
configs.get(FeatureKey.collidable).asBoolean(),
configs.get(FeatureKey.look_at_entity).asBoolean(),
configs.get(FeatureKey.pickup_items).asBoolean(),
configs.get(FeatureKey.skin).asBoolean(),
configs.get(FeatureKey.replenish).asBoolean(),
configs.get(FeatureKey.autofish).asBoolean()
);
})
.thenComposeAsync(fp::spawnAsync)

View File

@ -1,123 +0,0 @@
package io.github.hello09x.fakeplayer.core.manager;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import io.github.hello09x.fakeplayer.core.repository.UserConfigRepository;
import io.github.hello09x.fakeplayer.core.repository.model.Config;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
@Singleton
public class UserConfigManager {
private final UserConfigRepository repository;
@Inject
public UserConfigManager(UserConfigRepository repository) {
this.repository = repository;
}
/**
* 获取配置值
* <p>
* 如果玩家曾经设置过但随后没有了权限, 则返回默认值
* </p>
*
* @param player 玩家
* @param config 配置项
* @return 配置值
*/
public <T> T getConfig(@NotNull Player player, @NotNull Config<T> config) {
if (!config.hasPermission(player)) {
return config.defaultValue();
}
var value = repository.select(player.getUniqueId(), config);
if (value == null) {
return config.defaultValue();
}
return config.parser().apply(value);
}
/**
* 获取玩家所有配置项
* <p>
* 如果玩家曾经设置过但随后没有了权限, 则不会包含在其中
* </p>
*
* @param sender 玩家
* @return 玩家有权限的配置项
*/
public @NotNull Configs getConfigs(@NotNull CommandSender sender) {
if (!(sender instanceof Player player)) {
return new Configs(Collections.emptyMap());
}
var configs = repository.selectList(player.getUniqueId());
var values = new HashMap<Config<?>, Object>();
for (var config : configs) {
var key = Config.valueOfOpt(config.key()).orElse(null);
if (key == null || !key.hasPermission(player)) {
continue;
}
values.put(key, key.parser().apply(config.value()));
}
return new Configs(values);
}
/**
* 获取配置值
* <ul>
* <li>如果玩家曾经设置过但随后没有了权限, 则为默认值</li>
* <li>如果不是玩家, 则为默认值</li>
* </ul>
*
* @param sender 玩家
* @param config 配置项
* @return 配置值
*/
public <T> T getConfig(@NotNull CommandSender sender, @NotNull Config<T> config) {
if (sender instanceof Player p) {
return this.getConfig(p, config);
}
return config.defaultValue();
}
/**
* 设置配置值
*
* @param player 玩家
* @param config 配置
* @param value 配置值
*/
public <T> boolean setConfig(@NotNull Player player, @NotNull Config<T> config, @NotNull T value) {
if (!config.hasPermission(player)) {
return false;
}
repository.saveOrUpdate(player.getUniqueId(), config, value);
return true;
}
public static class Configs {
private final Map<Config<?>, Object> values;
private Configs(@NotNull Map<Config<?>, Object> values) {
this.values = values;
}
public <T> T getOrDefault(@NotNull Config<T> key) {
var config = values.get(key);
if (config == null) {
return key.defaultValue();
}
return key.type().cast(config);
}
}
}

View File

@ -14,6 +14,7 @@ import org.jetbrains.annotations.NotNull;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.logging.Logger;
@ -33,6 +34,16 @@ public class ActionManager {
Bukkit.getScheduler().runTaskTimer(Main.getInstance(), this::tick, 0, 1);
}
public boolean hasActiveAction(
@NotNull Player player,
@NotNull ActionType action
) {
return Optional.ofNullable(this.managers.get(player.getUniqueId()))
.map(manager -> manager.get(action))
.filter(ac -> ac.getSetting().remains > 0)
.isPresent();
}
public void setAction(
@NotNull Player player,
@NotNull ActionType action,

View File

@ -0,0 +1,79 @@
package io.github.hello09x.fakeplayer.core.manager.feature;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import io.github.hello09x.fakeplayer.core.config.FakeplayerConfig;
import io.github.hello09x.fakeplayer.core.repository.UserConfigRepository;
import io.github.hello09x.fakeplayer.core.repository.model.FeatureKey;
import io.github.hello09x.fakeplayer.core.repository.model.UserConfig;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Collectors;
@Singleton
public class FakeplayerFeatureManager {
private final UserConfigRepository repository;
private final FakeplayerConfig config;
@Inject
public FakeplayerFeatureManager(UserConfigRepository repository, FakeplayerConfig config) {
this.repository = repository;
this.config = config;
}
private @NotNull String getDefaultOption(@NotNull FeatureKey key) {
return Optional.ofNullable(config.getDefaultFeatures().get(key)).filter(option -> key.getOptions().contains(option)).orElse(key.getDefaultOption());
}
public @NotNull Feature getFeature(@NotNull Player player, @NotNull FeatureKey key) {
if (!key.testPermissions(player)) {
return new Feature(key, this.getDefaultOption(key));
}
String value = Optional.ofNullable(repository.selectByPlayerIdAndKey(player.getUniqueId(), key))
.map(UserConfig::value)
.orElseGet(() -> this.getDefaultOption(key));
return new Feature(key, value);
}
public @NotNull Map<FeatureKey, Feature> getFeatures(@NotNull CommandSender sender) {
Map<FeatureKey, UserConfig> userConfigs;
if (sender instanceof Player player) {
userConfigs = repository.selectByPlayerId(player.getUniqueId()).stream().collect(Collectors.toMap(UserConfig::key, Function.identity()));
} else {
userConfigs = Collections.emptyMap();
}
var configs = new LinkedHashMap<FeatureKey, Feature>(FeatureKey.values().length, 1.0F);
for (var key : FeatureKey.values()) {
String value;
if (!key.testPermissions(sender)) {
value = this.getDefaultOption(key);
} else {
value = Optional.ofNullable(userConfigs.get(key)).map(UserConfig::value).orElseGet(() -> this.getDefaultOption(key));
}
configs.put(key, new Feature(key, value));
}
return configs;
}
public void setFeature(@NotNull Player player, @NotNull FeatureKey key, @NotNull String value) {
this.repository.saveOrUpdate(new UserConfig(
null,
player.getUniqueId(),
key,
value
));
}
}

View File

@ -0,0 +1,28 @@
package io.github.hello09x.fakeplayer.core.manager.feature;
import io.github.hello09x.fakeplayer.core.repository.model.FeatureKey;
import org.jetbrains.annotations.NotNull;
/**
* @author tanyaofei
* @since 2024/8/13
**/
public record Feature(
@NotNull
FeatureKey key,
@NotNull
String value
) {
public @NotNull String asString() {
return value;
}
public boolean asBoolean() {
return Boolean.parseBoolean(value);
}
}

View File

@ -3,14 +3,13 @@ package io.github.hello09x.fakeplayer.core.repository;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import io.github.hello09x.devtools.database.jdbc.JdbcTemplate;
import io.github.hello09x.fakeplayer.core.repository.model.Config;
import io.github.hello09x.fakeplayer.core.repository.model.FeatureKey;
import io.github.hello09x.fakeplayer.core.repository.model.UserConfig;
import io.github.hello09x.fakeplayer.core.repository.model.UserConfigRowMapper;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
@Singleton
@ -24,55 +23,31 @@ public class UserConfigRepository {
this.initTables();
}
public @Nullable String select(@NotNull UUID playerId, @NotNull Config<?> config) {
var sql = """
select * from user_config
where player_id = ?
and `key` = ?
""";
return Optional
.ofNullable(jdbc.queryForObject(
sql,
UserConfigRowMapper.instance,
playerId.toString(),
config.key())
)
.map(UserConfig::value)
.orElse(null);
public @Nullable UserConfig selectByPlayerIdAndKey(@NotNull UUID playerId, @NotNull FeatureKey featureKey) {
var sql = "select * from user_config where player_id = ? and `key` = ?";
return jdbc.queryForObject(sql, UserConfigRowMapper.instance, playerId.toString(), featureKey.name());
}
public @NotNull List<UserConfig> selectList(@NotNull UUID playerId) {
public int saveOrUpdate(@NotNull UserConfig config) {
var sql = """
select * from user_config
where player_id = ?
""";
insert or replace into user_config(
id, player_id, `key`, `value`
) values (
(select id from user_config where player_id = ? and `key` = ?),
?,
?,
?
)
""";
return jdbc.update(sql, config.playerId().toString(), config.key().name(), config.playerId(), config.key().name(), config.value());
}
public @NotNull List<UserConfig> selectByPlayerId(@NotNull UUID playerId) {
var sql = "select * from user_config where player_id = ?";
return jdbc.query(sql, UserConfigRowMapper.instance, playerId.toString());
}
public <T> int saveOrUpdate(@NotNull UUID playerId, @NotNull Config<T> config, @NotNull T value) {
var sql = """
insert or replace into user_config(
id, player_id, `key`, `value`
) values (
(select id from user_config where player_id = ? and `key` = ?),
?,
?,
?
)
""";
return jdbc.update(
sql,
playerId.toString(),
config.key(),
playerId.toString(),
config.key(),
value.toString()
);
}
protected void initTables() {
jdbc.execute("""
create table if not exists user_config

View File

@ -1,204 +0,0 @@
package io.github.hello09x.fakeplayer.core.repository.model;
import io.github.hello09x.devtools.core.utils.SingletonSupplier;
import io.github.hello09x.fakeplayer.core.Main;
import io.github.hello09x.fakeplayer.core.command.Permission;
import io.github.hello09x.fakeplayer.core.manager.FakeplayerAutofishManager;
import io.github.hello09x.fakeplayer.core.manager.FakeplayerReplenishManager;
import net.kyori.adventure.translation.Translatable;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.LivingEntity;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.UnknownNullability;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.BiConsumer;
import java.util.function.Function;
/**
* @param key 配置项 key
* @param translationKey 翻译 key
* @param defaultValue 默认值
* @param options 可选值
* @param parser 转换器
* @param accessor 访问器, 访问或者设置假人当前配置
*/
public record Config<T>(
@NotNull
String key,
@NotNull
String translationKey,
@NotNull Class<T> type,
@NotNull
T defaultValue,
@NotNull
List<String> options,
@Nullable
String permission,
@NotNull
Function<String, T> parser,
@UnknownNullability
Accessor<T> accessor
) implements Translatable {
private static final Map<String, Config<?>> values = new HashMap<>();
private static final SingletonSupplier<FakeplayerAutofishManager> autofishManager = new SingletonSupplier<>(() -> Main.getInjector().getInstance(FakeplayerAutofishManager.class));
private static final SingletonSupplier<FakeplayerReplenishManager> replenishManager = new SingletonSupplier<>(() -> Main.getInjector().getInstance(FakeplayerReplenishManager.class));
public static Config<Boolean> collidable = build(
"collidable",
"fakeplayer.config.collidable",
Boolean.class,
true,
List.of("true", "false"),
null,
Boolean::valueOf,
new Accessor<>(LivingEntity::isCollidable, LivingEntity::setCollidable)
);
/**
* 无敌
*/
public static Config<Boolean> invulnerable = build(
"invulnerable",
"fakeplayer.config.invulnerable",
Boolean.class,
true,
List.of("true", "false"),
null,
Boolean::valueOf,
new Accessor<>(LivingEntity::isInvulnerable, LivingEntity::setInvulnerable)
);
/**
* 看向实体
*/
public static Config<Boolean> look_at_entity = build(
"look_at_entity",
"fakeplayer.config.look_at_entity",
Boolean.class,
true,
List.of("true", "false"),
null,
Boolean::valueOf,
null
);
/**
* 拾取物品
*/
public static Config<Boolean> pickup_items = build(
"pickup_items",
"fakeplayer.config.pickup_items",
Boolean.class,
true,
List.of("true", "false"),
null,
Boolean::valueOf,
new Accessor<>(LivingEntity::getCanPickupItems, LivingEntity::setCanPickupItems)
);
/**
* 使用皮肤
*/
public static Config<Boolean> skin = build(
"skin",
"fakeplayer.config.skin",
Boolean.class,
true,
List.of("true", "false"),
null,
Boolean::valueOf,
null
);
/**
* 自动补货
*/
public static Config<Boolean> replenish = build(
"replenish",
"fakeplayer.config.replenish",
Boolean.class,
false,
List.of("true", "false"),
Permission.replenish,
Boolean::valueOf,
new Accessor<>(replenishManager.get()::isReplenish, replenishManager.get()::setReplenish)
);
public static Config<Boolean> autofish = build(
"autofish",
"fakeplayer.config.autofish",
Boolean.class,
false,
List.of("true", "false"),
Permission.autofish,
Boolean::valueOf,
new Accessor<>(autofishManager.get()::isAutofish, autofishManager.get()::setAutofish)
);
@SuppressWarnings("unchecked")
public static @NotNull <T> Config<T> valueOf(@NotNull String name) {
return (Config<T>) valueOfOpt(name).orElseThrow(() -> new IllegalArgumentException("No config named: " + name));
}
@SuppressWarnings("unchecked")
public static @NotNull <T> Optional<Config<T>> valueOfOpt(@NotNull String name) {
return Optional.ofNullable((Config<T>) values.get(name));
}
public static @NotNull Config<?>[] values() {
return values.values().toArray(Config[]::new);
}
private static <T> Config<T> build(
@NotNull String name,
@NotNull String translationKey,
@NotNull Class<T> type,
@NotNull T defaultValue,
@NotNull List<String> options,
@Nullable String permission,
@NotNull Function<String, T> converter,
@Nullable Accessor<T> accessor
) {
var config = new Config<>(name, translationKey, type, defaultValue, options, permission, converter, accessor);
values.put(name, config);
return config;
}
public boolean hasPermission(@NotNull CommandSender player) {
return this.permission == null || player.hasPermission(this.permission);
}
@Override
public @NotNull String translationKey() {
return this.translationKey;
}
public boolean hasAccessor() {
return this.accessor != null;
}
public record Accessor<T>(
@NotNull Function<Player, T> getter,
@NotNull BiConsumer<Player, T> setter
) {
}
}

View File

@ -0,0 +1,164 @@
package io.github.hello09x.fakeplayer.core.repository.model;
import io.github.hello09x.fakeplayer.api.spi.ActionSetting;
import io.github.hello09x.fakeplayer.api.spi.ActionType;
import io.github.hello09x.fakeplayer.core.command.Permission;
import lombok.AllArgsConstructor;
import lombok.Getter;
import net.kyori.adventure.translation.Translatable;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List;
import java.util.function.BiConsumer;
import java.util.function.Function;
/**
* @author tanyaofei
* @since 2024/8/13
**/
@Getter
@AllArgsConstructor
public enum FeatureKey implements Translatable, Singletons {
/**
* 是否具有碰撞箱
*/
collidable(
"fakeplayer.config.collidable",
List.of(Permission.config),
List.of("true", "false"),
"true",
faker -> String.valueOf(faker.isCollidable()),
(faker, value) -> faker.setCollidable(Boolean.parseBoolean(value))
),
/**
* 是否无敌
*/
invulnerable(
"fakeplayer.config.invulnerable",
List.of(Permission.config),
List.of("true", "false"),
"true",
faker -> String.valueOf(faker.isInvulnerable()),
(faker, value) -> faker.setInvulnerable(Boolean.parseBoolean(value))
),
/**
* 是否自动看向实体
*/
look_at_entity(
"fakeplayer.config.look_at_entity",
List.of(Permission.config),
List.of("true", "false"),
"false",
faker -> String.valueOf(actionManager.get().hasActiveAction(faker, ActionType.LOOK_AT_NEAREST_ENTITY)),
(faker, value) -> {
if (Boolean.parseBoolean(value)) {
actionManager.get().setAction(faker, ActionType.LOOK_AT_NEAREST_ENTITY, ActionSetting.continuous());
} else {
actionManager.get().setAction(faker, ActionType.LOOK_AT_NEAREST_ENTITY, ActionSetting.stop());
}
}
),
/**
* 是否能够拾取物品
*/
pickup_items(
"fakeplayer.config.pickup_items",
List.of(Permission.config),
List.of("true", "false"),
"true",
faker -> String.valueOf(faker.getCanPickupItems()),
(faker, value) -> faker.setCanPickupItems(Boolean.parseBoolean(value))
),
/**
* 是否使用皮肤
*/
skin(
"fakeplayer.config.skin",
List.of(Permission.config),
List.of("true", "false"),
"true",
null,
null
),
/**
* 是否自动补货
*/
replenish(
"fakeplayer.config.replenish",
List.of(Permission.config, Permission.replenish),
List.of("true", "false"),
"false",
faker -> String.valueOf(replenishManager.get().isReplenish(faker)),
(faker, value) -> replenishManager.get().setReplenish(faker, Boolean.parseBoolean(value))
),
/**
* 是否自动钓鱼
*/
autofish(
"fakeplayer.config.autofish",
List.of(Permission.config, Permission.autofish),
List.of("true", "false"),
"false",
faker -> String.valueOf(autofishManager.get().isAutofish(faker)),
(faker, value) -> autofishManager.get().setAutofish(faker, Boolean.parseBoolean(value))
),
;
@NotNull
final String translationKey;
@NotNull
final List<String> permissions;
@NotNull
final List<String> options;
@NotNull
final String defaultOption;
@Nullable
final Function<Player, String> detector;
@Nullable
final BiConsumer<Player, String> modifier;
@Override
public @NotNull String translationKey() {
return this.translationKey;
}
public boolean hasDetector() {
return this.detector != null;
}
public boolean hasModifier() {
return this.modifier != null;
}
public boolean testPermissions(@NotNull CommandSender sender) {
if (this.permissions.isEmpty()) {
return true;
}
for (var permission : this.permissions) {
if (!sender.hasPermission(permission)) {
return false;
}
}
return true;
}
}

View File

@ -0,0 +1,21 @@
package io.github.hello09x.fakeplayer.core.repository.model;
import io.github.hello09x.devtools.core.utils.SingletonSupplier;
import io.github.hello09x.fakeplayer.core.Main;
import io.github.hello09x.fakeplayer.core.manager.FakeplayerAutofishManager;
import io.github.hello09x.fakeplayer.core.manager.FakeplayerReplenishManager;
import io.github.hello09x.fakeplayer.core.manager.action.ActionManager;
import java.util.function.Supplier;
/**
* @author tanyaofei
* @since 2024/8/13
**/
public interface Singletons {
Supplier<ActionManager> actionManager = new SingletonSupplier<>(() -> Main.getInjector().getInstance(ActionManager.class));
Supplier<FakeplayerReplenishManager> replenishManager = new SingletonSupplier<>(() -> Main.getInjector().getInstance(FakeplayerReplenishManager.class));
Supplier<FakeplayerAutofishManager> autofishManager = new SingletonSupplier<>(() -> Main.getInjector().getInstance(FakeplayerAutofishManager.class));
}

View File

@ -1,14 +1,21 @@
package io.github.hello09x.fakeplayer.core.repository.model;
import org.jetbrains.annotations.NotNull;
import java.util.UUID;
public record UserConfig(
Integer id,
String playerId,
@NotNull
UUID playerId,
String key,
@NotNull
FeatureKey key,
@NotNull
String value
) {

View File

@ -6,6 +6,7 @@ import org.jetbrains.annotations.Nullable;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.UUID;
/**
* @author tanyaofei
@ -19,8 +20,8 @@ public class UserConfigRowMapper implements RowMapper<UserConfig> {
public @Nullable UserConfig mapRow(@NotNull ResultSet rs, int rowNum) throws SQLException {
return new UserConfig(
rs.getInt("id"),
rs.getString("player_id"),
rs.getString("key"),
UUID.fromString(rs.getString("player_id")),
FeatureKey.valueOf(rs.getString("key")),
rs.getString("value")
);
}

View File

@ -252,6 +252,17 @@ allow-commands:
- ''
- ''
# Default Features of fake players
# 默认假人特性
default-features:
collidable: true
pickup_items: true
skin: true
look_at_entity: false
invulnerable: false
replenish: false
autofish: false
# 检测更新
# 仅仅是检测, 并不会帮你下载

View File

@ -16,7 +16,7 @@ fakeplayer.command.cmd.error.execute-failed=Failed to execute the command. Pleas
fakeplayer.command.cmd.error.fakeplayer-has-no-permission={0} doesn't have permission
fakeplayer.command.cmd.error.no-permission=You don't have permission
fakeplayer.command.config.description=Check or change your personal config
fakeplayer.command.config.set.error.invalid-option=Unknown option
fakeplayer.command.config.set.error.invalid-key=Unknown config
fakeplayer.command.config.set.error.invalid-value=Unknown value
fakeplayer.command.config.set.error.no-permission=You don't have permission to change this config
fakeplayer.command.config.set.success={0} changed to {1}, it will take effect the next time you spawn a fake player

View File

@ -16,7 +16,7 @@ fakeplayer.command.cmd.error.execute-failed=\u6267\u884C\u547D\u4EE4\u5931\u8D25
fakeplayer.command.cmd.error.fakeplayer-has-no-permission={0} \u6CA1\u6709\u6743\u9650\u6267\u884C\u6B64\u547D\u4EE4
fakeplayer.command.cmd.error.no-permission=\u6CA1\u6709\u6743\u9650\u6267\u884C\u6B64\u547D\u4EE4
fakeplayer.command.config.description=\u67E5\u770B\u6216\u8005\u914D\u7F6E\u4E2A\u4EBA\u8BBE\u7F6E
fakeplayer.command.config.set.error.invalid-option=\u672A\u77E5\u914D\u7F6E\u9879
fakeplayer.command.config.set.error.invalid-key=\u672A\u77E5\u914D\u7F6E\u9879
fakeplayer.command.config.set.error.invalid-value=\u672A\u77E5\u914D\u7F6E\u503C
fakeplayer.command.config.set.error.no-permission=\u4F60\u6CA1\u6709\u6743\u9650\u4FEE\u6539\u8FD9\u9879\u914D\u7F6E
fakeplayer.command.config.set.success={0} \u53D8\u66F4\u4E3A {1}, \u4E0B\u6B21\u53EC\u5524\u5047\u4EBA\u65F6\u751F\u6548

View File

@ -16,7 +16,7 @@ fakeplayer.command.cmd.error.execute-failed=\u57F7\u884C\u6307\u4EE4\u5931\u6557
fakeplayer.command.cmd.error.fakeplayer-has-no-permission={0} \u5187\u6B0A\u9650\u57F7\u884C\u6B64\u6307\u4EE4
fakeplayer.command.cmd.error.no-permission=\u5187\u6B0A\u9650\u57F7\u884C\u6B64\u6307\u4EE4
fakeplayer.command.config.description=\u67E5\u770B\u500B\u4EBA\u914D\u7F6E
fakeplayer.command.config.set.error.invalid-option=\u672A\u77E5\u914D\u7F6E\u9805
fakeplayer.command.config.set.error.invalid-key=\u672A\u77E5\u914D\u7F6E\u9805
fakeplayer.command.config.set.error.invalid-value=\u672A\u77E5\u914D\u7F6E\u503C
fakeplayer.command.config.set.error.no-permission=\u4F60\u5187\u6B0A\u9650\u4FEE\u6539\u6B64\u914D\u7F6E
fakeplayer.command.config.set.success={0} \u8B8A\u66F4\u70BA {1}, \u4E0B\u6B21\u53EC\u559A\u5047\u4EBA\u6642\u751F\u6548

View File

@ -16,7 +16,7 @@ fakeplayer.command.cmd.error.execute-failed=\u57F7\u884C\u547D\u4EE4\u5931\u6557
fakeplayer.command.cmd.error.fakeplayer-has-no-permission={0} \u6C92\u6709\u6B0A\u9650\u57F7\u884C\u6B64\u547D\u4EE4
fakeplayer.command.cmd.error.no-permission=\u6C92\u6709\u6B0A\u9650\u57F7\u884C\u6B64\u547D\u4EE4
fakeplayer.command.config.description=\u67E5\u770B\u6216\u8005\u914D\u7F6E\u500B\u4EBA\u8A2D\u7F6E
fakeplayer.command.config.set.error.invalid-option=\u672A\u77E5\u914D\u7F6E\u9805
fakeplayer.command.config.set.error.invalid-key=\u672A\u77E5\u914D\u7F6E\u9805
fakeplayer.command.config.set.error.invalid-value=\u672A\u77E5\u914D\u7F6E\u503C
fakeplayer.command.config.set.error.no-permission=\u4F60\u6C92\u6709\u6B0A\u9650\u4FEE\u6539\u9019\u9805\u914D\u7F6E
fakeplayer.command.config.set.success={0} \u8B8A\u66F4\u70BA {1}, \u4E0B\u6B21\u53EC\u559A\u5047\u4EBA\u6642\u751F\u6548