1. 修复可能由于背包同步插件导致的物品复制、召唤时生命值为 0 的 bug

2. 修复判断是否有更新的逻辑
3. 添加更多注释
This commit is contained in:
tanyaofei 2023-09-13 09:56:32 +08:00
parent 9a2c7715d7
commit 3219a2634c
33 changed files with 464 additions and 340 deletions

View File

@ -6,7 +6,7 @@
<groupId>io.github.hello09x</groupId>
<artifactId>fakeplayer</artifactId>
<version>0.1.7</version>
<version>0.1.8</version>
<packaging>jar</packaging>
<name>fakeplayer</name>
@ -117,7 +117,7 @@
<relocations>
<relocation>
<pattern>dev.jorel.commandapi</pattern>
<shadedPattern>io.github.hello09x.shaded.commandapi</shadedPattern>
<shadedPattern>io.github.hello09x.fakeplayer.shaded.commandapi</shadedPattern>
</relocation>
</relocations>
</configuration>

View File

@ -69,18 +69,27 @@ public final class Main extends JavaPlugin {
public void checkForUpdatesAsync() {
CompletableFuture.runAsync(() -> {
var meta = getPluginMeta();
var checker = new UpdateChecker("tanyaofei", "minecraft-fakeplayer");
try {
var release = checker.getLastRelease();
if (!release.getTagName().equals(getPluginMeta().getVersion())) {
var current = meta.getVersion();
var other = release.getTagName();
if (other.charAt(0) == 'v') {
other = other.substring(1);
}
if (UpdateChecker.isNew(current, other)) {
var log = getLogger();
log.info("检测到新的版本: " + release.getTagName());
log.info("前往此处下载 https://github.com/tanyaofei/minecraft-fakeplayer");
log.info("前往此处下载 " + meta.getWebsite());
log.info("更新日志");
for (var line : release.getBody().split("\n")) {
log.info("\t" + line);
}
}
} catch (Throwable e) {
getLogger().warning("检测新版本发生异常: " + e.getMessage());
}
@ -90,7 +99,7 @@ public final class Main extends JavaPlugin {
@Override
public void onDisable() {
CommandAPI.onDisable();
FakeplayerManager.instance.removeAll();
FakeplayerManager.instance.removeAll("plugin disabled");
UsedIdRepository.instance.saveAll();
FakeplayerManager.instance.onDisable();
WildFakeplayerManager.instance.onDisable();

View File

@ -32,9 +32,9 @@ public class ActionCommand extends AbstractCommand {
private static String toLocationString(@NotNull Location location) {
return StringUtils.joinWith(", ",
Mth.round(location.getX(), 0.5),
Mth.round(location.getY(), 0.5),
Mth.round(location.getZ(), 0.5));
Mth.floor(location.getX(), 0.5),
Mth.floor(location.getY(), 0.5),
Mth.floor(location.getZ(), 0.5));
}
public CommandExecutor action(@NotNull Action action, @NotNull ActionSetting setting) {

View File

@ -35,29 +35,28 @@ public class CommandRegistry {
"可以创建模拟玩家的假人, 能保持附近区块的刷新、触发怪物生成。同时还提供了一些操作命令让你控制假人的物品、动作等等。"
)
.withUsage(
"§6? [页码] §7- §f查看帮助",
"§6spawn [名称] [世界] [坐标] §7- §f创建假人",
"§6kill §7- §f移除假人",
"§6list [页码] [数量] §7- §f查看所有假人",
"§6distance §7- §f查看与假人的距离",
"§6tp §7- §f传送到假人身边",
"§6tphere §7- §f将假人传送到身边",
"§6tps §7- §f与假人交换位置",
"§6config get <配置项> §7- §f查看配置项",
"§6config set <配置项> <配置值> §7- §f设置配置项",
"§6health §7- §f查看生命值",
"§6exp §7- §f查看经验值",
"§6expme §7- §f转移经验值",
"§6attack (once | continuous | interval | stop) §7- §f攻击/破坏",
"§6use (once | continuous | interval | stop) §7- §f使用/交互/放置",
"§6jump (once | continuous | interval | stop) §7- §f跳跃",
"§6drop [-a|--all] §7- §f丢弃手上物品",
"§6dropinv §7- §f丢弃背包物品",
"§6look (north | south | east | west | up | down | at | entity) §7- §f看向指定位置",
"§6turn (left | right | back | to) §7- §f转身到指定位置",
"§6move (forward | backward | left | right) §7- §f移动",
"§6cmd <假人> <命令> §7- §f执行命令",
"§6reload §7- §f重载配置文件"
usage("spawn [名称] [世界] [坐标]", "创建假人"),
usage("kill", "移除假人"),
usage("list [页码] [数量]", "查看所有假人"),
usage("distance", "查看与假人的距离"),
usage("tp", "传送到假人身边"),
usage("tphere", "将假人传送到身边"),
usage("tps", "与假人交换位置"),
usage("config get <配置项>", "查看配置项"),
usage("config set <配置项> <值>", "设置配置项"),
usage("health", "查看生命值"),
usage("exp", "查看经验值"),
usage("expme", "转移经验值"),
usage("attack (once | continuous | interval | stop)", "攻击/破坏"),
usage("use (once | continuous | interval | stop)", "使用/交互/放置"),
usage("jump (once | continuous | interval | stop)", ""),
usage("drop [-a|--all]", "丢弃手上物品"),
usage("dropinv", "丢弃背包物品"),
usage("look (north | south | east | west | up | down | at | entity)", "看向指定位置"),
usage("turn (left | right | back | to)", "转身"),
usage("move (forward | backward | left |right)", "移动"),
usage("cmd", "执行命令"),
usage("reload", "重新加载配置文件")
)
.withSubcommands(
command("help")

View File

@ -3,7 +3,7 @@ package io.github.hello09x.fakeplayer.command;
import dev.jorel.commandapi.exceptions.WrapperCommandSyntaxException;
import dev.jorel.commandapi.executors.CommandArguments;
import io.github.hello09x.fakeplayer.util.Experience;
import io.github.hello09x.bedrock.io.Experiences;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
@ -17,7 +17,7 @@ public class ExpCommand extends AbstractCommand {
public void expme(@NotNull Player sender, @NotNull CommandArguments args) throws WrapperCommandSyntaxException {
var target = getTarget(sender, args);
var exp = Experience.getExp(target);
var exp = Experiences.getExp(target);
if (exp == 0) {
sender.sendMessage(textOfChildren(
@ -27,9 +27,8 @@ public class ExpCommand extends AbstractCommand {
return;
}
Experience.clean(target);
Experience.changeExp(sender, Experience.getExp(sender) + exp);
Experiences.clean(target);
sender.giveExp(exp, false);
sender.sendMessage(textOfChildren(
text(target.getName(), WHITE),
text(" 转移 ", GRAY),

View File

@ -2,7 +2,7 @@ package io.github.hello09x.fakeplayer.command;
import dev.jorel.commandapi.exceptions.WrapperCommandSyntaxException;
import dev.jorel.commandapi.executors.CommandArguments;
import io.github.hello09x.fakeplayer.util.Experience;
import io.github.hello09x.bedrock.io.Experiences;
import io.github.hello09x.fakeplayer.util.Mth;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.attribute.AttributeInstance;
@ -24,7 +24,7 @@ public class ProfileCommand extends AbstractCommand {
var target = getTarget(sender, args);
var level = target.getLevel();
var total = Experience.getExp(target);
var total = Experiences.getExp(target);
sender.sendMessage(textOfChildren(
text(target.getName(), WHITE),
text(" 当前 ", GRAY),
@ -60,7 +60,7 @@ public class ProfileCommand extends AbstractCommand {
sender.sendMessage(textOfChildren(
text(target.getName(), WHITE),
text(" 当前生命值: ", GRAY),
text(Mth.round(health, 0.5), color),
text(Mth.floor(health, 0.5), color),
text("/", color),
text(max, color)
));

View File

@ -15,7 +15,7 @@ public class ReloadCommand extends AbstractCommand {
private final FakeplayerConfig config = FakeplayerConfig.instance;
public void reload(@NotNull CommandSender sender, @NotNull CommandArguments args) {
config.reload();
config.reload(true);
sender.sendMessage(text("重载配置文件完成", GRAY));
}

View File

@ -2,9 +2,9 @@ package io.github.hello09x.fakeplayer.command;
import dev.jorel.commandapi.exceptions.WrapperCommandSyntaxException;
import dev.jorel.commandapi.executors.CommandArguments;
import io.github.hello09x.bedrock.page.Page;
import io.github.hello09x.fakeplayer.command.Permission.Keepalive;
import io.github.hello09x.fakeplayer.util.Mth;
import io.github.tanyaofei.plugin.toolkit.database.Page;
import org.apache.commons.lang3.StringUtils;
import org.bukkit.Bukkit;
import org.bukkit.Location;
@ -20,6 +20,7 @@ import java.util.*;
import static net.kyori.adventure.text.Component.*;
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.BOLD;
public class SpawnCommand extends AbstractCommand {
@ -29,9 +30,9 @@ public class SpawnCommand extends AbstractCommand {
return location.getWorld().getName()
+ ": "
+ StringUtils.joinWith(", ",
Mth.round(location.getX(), 0.5),
Mth.round(location.getY(), 0.5),
Mth.round(location.getZ(), 0.5));
Mth.floor(location.getX(), 0.5),
Mth.floor(location.getY(), 0.5),
Mth.floor(location.getZ(), 0.5));
}
public void spawn(@NotNull CommandSender sender, @NotNull CommandArguments args) {
@ -65,7 +66,7 @@ public class SpawnCommand extends AbstractCommand {
text("你创建了假人 ", GRAY),
text(player.getName()),
text(", 位于 ", GRAY),
text(toLocationString(player.getLocation())),
text(toLocationString(spawnpoint)),
Keepalive.isPermanent(keepalive)
? empty()
: textOfChildren(
@ -98,7 +99,7 @@ public class SpawnCommand extends AbstractCommand {
var names = new StringJoiner(", ");
for (var target : targets) {
if (fakeplayerManager.remove(target.getName())) {
if (fakeplayerManager.remove(target.getName(), "command kill")) {
names.add(target.getName());
}
}
@ -116,19 +117,11 @@ public class SpawnCommand extends AbstractCommand {
? fakeplayerManager.getAll()
: fakeplayerManager.getAll(sender);
var total = fakers.size();
var pages = total == 0 ? 1 : (int) Math.ceil((double) total / size);
var p = new Page<>(
fakers.subList((page - 1) * size, Math.min(total, page * size)),
total,
pages,
page,
size
);
var p = Page.of(fakers, page, size);
var canTp = sender instanceof Player && sender.hasPermission(Permission.tp);
sender.sendMessage(p.toComponent(
"假人",
sender.sendMessage(p.asComponent(
text("假人", AQUA, BOLD),
fakeplayer -> textOfChildren(
text(fakeplayer.getName() + " (" + fakeplayerManager.getCreator(fakeplayer) + ")", GOLD),
text(" - ", GRAY),
@ -136,8 +129,7 @@ public class SpawnCommand extends AbstractCommand {
canTp ? text(" [<--传送]", AQUA).clickEvent(runCommand("/fp tp " + fakeplayer.getName())) : empty(),
text(" [<--移除]", RED).clickEvent(runCommand("/fp kill " + fakeplayer.getName()))
),
String.format("/fp list %d %d", page - 1, size),
String.format("/fp list %d %d", page + 1, size)
i -> "/fp list " + i + " " + size
));
}
@ -158,7 +150,7 @@ public class SpawnCommand extends AbstractCommand {
return;
}
var euclidean = Mth.round(from.distance(to), 0.5);
var euclidean = Mth.floor(from.distance(to), 0.5);
var x = Math.abs(from.getBlockX() - to.getBlockX());
var y = Math.abs(from.getBlockY() - to.getBlockY());
var z = Math.abs(from.getBlockZ() - to.getBlockZ());

View File

@ -21,6 +21,7 @@ import java.util.regex.PatternSyntaxException;
public class FakeplayerConfig extends Config<FakeplayerConfig> {
public final static FakeplayerConfig instance;
private final static Logger log;
private final static String defaultNameChars = "^[a-zA-Z0-9_]+$";

View File

@ -1,6 +1,7 @@
package io.github.hello09x.fakeplayer.entity;
import com.mojang.authlib.GameProfile;
import io.github.hello09x.bedrock.task.Tasks;
import io.github.hello09x.fakeplayer.Main;
import io.github.hello09x.fakeplayer.config.FakeplayerConfig;
import io.github.hello09x.fakeplayer.manager.action.Action;
@ -10,9 +11,8 @@ import io.github.hello09x.fakeplayer.manager.naming.SequenceName;
import io.github.hello09x.fakeplayer.network.EmptyConnection;
import io.github.hello09x.fakeplayer.network.EmptyLoginPacketListener;
import io.github.hello09x.fakeplayer.network.EmptyServerGamePacketListener;
import io.github.hello09x.fakeplayer.util.BK;
import io.github.hello09x.fakeplayer.util.Tasker;
import io.github.hello09x.fakeplayer.util.Teleportor;
import io.github.hello09x.fakeplayer.util.Worlds;
import io.github.hello09x.fakeplayer.util.nms.NMS;
import lombok.Getter;
import net.minecraft.network.protocol.PacketFlow;
@ -23,7 +23,6 @@ import org.bukkit.Location;
import org.bukkit.entity.Player;
import org.bukkit.event.player.AsyncPlayerPreLoginEvent;
import org.bukkit.event.player.PlayerLoginEvent;
import org.bukkit.scheduler.BukkitRunnable;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@ -47,28 +46,37 @@ public class FakePlayer {
private final FakeplayerConfig config = FakeplayerConfig.instance;
@NotNull
private final MinecraftServer server;
@NotNull
@Getter
private final String creator;
@NotNull
@Getter
private final ServerPlayer handle;
@NotNull
@Getter
private final Player bukkitPlayer;
@NotNull
@Getter
private final String creatorIp;
@NotNull
@Getter
private final SequenceName sequenceName;
@NotNull
private final FakeplayerTicker ticker;
@NotNull
@Getter
private final String name;
@NotNull
private final UUID uuid;
public FakePlayer(
@ -94,7 +102,7 @@ public class FakePlayer {
bukkitPlayer.setPersistent(false);
bukkitPlayer.setSleepingIgnored(true);
nms.setPlayBefore(bukkitPlayer);
nms.setPlayBefore(bukkitPlayer); // 可避免一些插件的第一次入服欢迎信息
nms.unpersistAdvancements(bukkitPlayer);
}
@ -103,29 +111,23 @@ public class FakePlayer {
*/
public void spawn(@NotNull SpawnOption option) {
if (config.isSimulateLogin()) {
new BukkitRunnable() {
@Override
public void run() {
var preLoginEvent = new AsyncPlayerPreLoginEvent(
Tasks.runAsync(Main.getInstance(), () -> {
Bukkit.getPluginManager().callEvent(new AsyncPlayerPreLoginEvent(
handle.getGameProfile().getName(),
fakeAddress,
fakeAddress,
handle.getUUID(),
bukkitPlayer.getPlayerProfile(),
fakeAddress.getHostName()
);
Bukkit.getPluginManager().callEvent(preLoginEvent);
}
}.runTaskAsynchronously(Main.getInstance());
));
});
{
Bukkit.getPluginManager().callEvent(new PlayerLoginEvent(
bukkitPlayer,
fakeAddress.getHostName(),
fakeAddress
));
}
}
{
var connection = new EmptyConnection(PacketFlow.CLIENTBOUND);
@ -144,6 +146,13 @@ public class FakePlayer {
connection.setListener(listener);
}
if (config.isDropInventoryOnQuiting()) {
// 跨服背包同步插件可能导致假人既丢弃了一份到地上在重新生成的时候又回来了
// 因此在生成的时候清空一次背包
// 但无法解决登陆后延迟同步背包的情况
bukkitPlayer.getInventory().clear();
}
bukkitPlayer.setInvulnerable(option.invulnerable());
bukkitPlayer.setCollidable(option.collidable());
bukkitPlayer.setCanPickupItems(option.pickupItems());
@ -152,7 +161,7 @@ public class FakePlayer {
}
var spawnAt = option.spawnAt().clone();
if (BK.isOverworld(spawnAt.getWorld())) {
if (Worlds.isOverworld(spawnAt.getWorld())) {
// 创建在主世界时需要跨越一次世界才能拥有刷怪能力
teleportToSpawnpointAfterChangingDimension(spawnAt);
} else {
@ -168,7 +177,7 @@ public class FakePlayer {
* @param spawnpoint 最终目的地, 即出生点
*/
private void teleportToSpawnpointAfterChangingDimension(@NotNull Location spawnpoint) {
var world = BK.getNonOverworld();
var world = Worlds.getNonOverworld();
if (world == null || !bukkitPlayer.teleport(world.getSpawnLocation())) {
Optional.ofNullable(Bukkit.getPlayerExact(creator))
.ifPresentOrElse(
@ -181,7 +190,7 @@ public class FakePlayer {
return;
}
Tasker.nextTick(() -> teleportToSpawnpoint(spawnpoint));
Tasks.runNextTick(Main.getInstance(), () -> teleportToSpawnpoint(spawnpoint));
}
private void teleportToSpawnpoint(@NotNull Location spawnpoint) {
@ -208,4 +217,8 @@ public class FakePlayer {
return Bukkit.getPlayerExact(this.creator);
}
public int getTickCount() {
return this.handle.tickCount;
}
}

View File

@ -1,6 +1,6 @@
package io.github.hello09x.fakeplayer.entity;
import net.minecraft.server.level.ServerPlayer;
import io.github.hello09x.fakeplayer.manager.FakeplayerManager;
import org.bukkit.Location;
import org.bukkit.entity.Player;
import org.bukkit.scheduler.BukkitRunnable;
@ -17,9 +17,17 @@ import static net.kyori.adventure.text.format.NamedTextColor.GRAY;
public class FakeplayerTicker extends BukkitRunnable {
@NotNull
private final FakePlayer player;
/**
* 移除时间
* <p>如果不需要定时移除则为 0</p>
*/
private final long removeAt;
private final FakeplayerManager manager = FakeplayerManager.instance;
public FakeplayerTicker(
@NotNull FakePlayer player,
@Nullable LocalDateTime removeAt
@ -30,15 +38,14 @@ public class FakeplayerTicker extends BukkitRunnable {
@Override
public void run() {
if (!getBukkitPlayer().isOnline()) {
if (!player.isOnline()) {
cancel();
return;
}
var player = getServerPlayer();
if (removeAt != 0 && player.tickCount % 20 == 0 && System.currentTimeMillis() > removeAt) {
getBukkitPlayer().kick(text("[fakeplayer] 存活时间到期"));
Optional.ofNullable(this.player.getCreatorPlayer())
if (removeAt != 0 && player.getTickCount() % 20 == 0 && System.currentTimeMillis() > removeAt) {
manager.remove(player.getName(), "存活时间到期");
Optional.ofNullable(player.getCreatorPlayer())
.ifPresent(p -> p.sendMessage(textOfChildren(
text("假人 ", GRAY),
text(this.player.getName()),
@ -48,22 +55,26 @@ public class FakeplayerTicker extends BukkitRunnable {
return;
}
if (player.tickCount == 0) {
var x = player.getX();
var y = player.getY();
var z = player.getZ();
var handle = player.getHandle();
if (handle.tickCount == 0) {
// region 处理第一次生成时被别的插件干预然后随机传送
var x = handle.getX();
var y = handle.getY();
var z = handle.getZ();
// 将本 tick 的移动取消
player.xo = x;
player.yo = y;
player.zo = z;
player.doTick();
handle.xo = x;
handle.yo = y;
handle.zo = z;
handle.doTick();
// clearFog 插件会在第一次传送的时候改变了玩家的位置, 因此必须进行一次传送
getBukkitPlayer().teleport(new Location(getBukkitPlayer().getWorld(), x, y, z, player.getYRot(), player.getXRot()));
player.absMoveTo(x, y, z, player.getYRot(), player.getXRot());
getBukkitPlayer().teleport(new Location(getBukkitPlayer().getWorld(), x, y, z, handle.getYRot(), handle.getXRot()));
handle.absMoveTo(x, y, z, handle.getYRot(), handle.getXRot());
// endregion
} else {
player.doTick();
handle.doTick();
}
}
@ -71,8 +82,4 @@ public class FakeplayerTicker extends BukkitRunnable {
return this.player.getBukkitPlayer();
}
private @NotNull ServerPlayer getServerPlayer() {
return this.player.getHandle();
}
}

View File

@ -5,6 +5,8 @@ import io.github.hello09x.fakeplayer.Main;
import io.github.hello09x.fakeplayer.config.FakeplayerConfig;
import io.github.hello09x.fakeplayer.manager.FakeplayerManager;
import io.github.hello09x.fakeplayer.repository.UsedIdRepository;
import org.bukkit.attribute.Attribute;
import org.bukkit.attribute.AttributeInstance;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
@ -13,6 +15,7 @@ import org.bukkit.event.player.AsyncPlayerPreLoginEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import org.jetbrains.annotations.NotNull;
import java.util.Optional;
import java.util.logging.Logger;
import static net.kyori.adventure.text.Component.text;
@ -49,9 +52,20 @@ public class PlayerListeners implements Listener {
/**
* 死亡退出游戏
*/
@EventHandler(ignoreCancelled = true, priority = EventPriority.MONITOR)
@EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST)
public void onDead(@NotNull PlayerDeathEvent event) {
manager.remove(event.getPlayer().getName());
var player = event.getPlayer();
if (!manager.isFake(player)) {
return;
}
// 有一些跨服同步插件会退出时同步生命值, 假人重新生成的时候同步为 0
// 因此在死亡时将生命值设置恢复满血先
Optional.ofNullable(player.getAttribute(Attribute.GENERIC_MAX_HEALTH))
.map(AttributeInstance::getValue)
.ifPresent(player::setHealth);
event.setCancelled(true);
manager.remove(event.getPlayer().getName(), "dead");
}
/**

View File

@ -3,6 +3,7 @@ package io.github.hello09x.fakeplayer.manager;
import io.github.hello09x.fakeplayer.entity.FakePlayer;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.Unmodifiable;
import java.util.*;
@ -16,6 +17,11 @@ public class FakeplayerList {
private final Map<String, List<FakePlayer>> playersByCreator = new HashMap<>();
/**
* 添加一个假人到假人清单
*
* @param player 假人
*/
public void add(@NotNull FakePlayer player) {
this.playersByName.put(player.getName(), player);
this.playersByUUID.put(player.getUUID(), player);
@ -23,41 +29,80 @@ public class FakeplayerList {
this.playersByCreator.get(player.getCreator()).add(player);
}
/**
* 通过假人的名称获取假人
*
* @param name 名称
* @return 假人
*/
public @Nullable FakePlayer getByName(@NotNull String name) {
return Optional.ofNullable(this.playersByName.get(name)).map(this::checkOnline).orElse(null);
}
/**
* 通过 UUID 获取假人
*
* @param uuid UUID
* @return 假人
*/
public @Nullable FakePlayer getByUUID(@NotNull UUID uuid) {
return Optional.ofNullable(this.playersByUUID.get(uuid)).map(this::checkOnline).orElse(null);
}
public @NotNull List<FakePlayer> getByCreator(@NotNull String creator) {
/**
* 获取创建者创建的所有假人
*
* @param creator 创建者
* @return 假人
*/
public @NotNull @Unmodifiable List<FakePlayer> getByCreator(@NotNull String creator) {
return Optional.ofNullable(this.playersByCreator.get(creator)).map(Collections::unmodifiableList).orElse(Collections.emptyList());
}
/**
* 移除一个假人
*
* @param player 假人
*/
public void remove(@NotNull FakePlayer player) {
this.playersByName.remove(player.getName());
this.playersByUUID.remove(player.getUUID());
Optional.ofNullable(this.playersByCreator.get(player.getCreator())).map(players -> players.remove(player));
}
public @Nullable FakePlayer removeByUUID(@NotNull UUID uniqueId) {
var player = getByUUID(uniqueId);
/**
* 通过 UUID 移除假人
*
* @param uuid UUID
* @return 被移除的假人
*/
public @Nullable FakePlayer removeByUUID(@NotNull UUID uuid) {
var player = getByUUID(uuid);
if (player == null) {
return null;
}
remove(player);
this.remove(player);
return player;
}
public List<FakePlayer> getAll() {
/**
* 获取所有假人
*
* @return 假人
*/
public @NotNull @Unmodifiable List<FakePlayer> getAll() {
return List.copyOf(this.playersByUUID.values());
}
/**
* 检测假人是否在线, 如果不在线了则移除并返回 {@code null}
*
* @param player 假人
* @return 假人
*/
private @Nullable FakePlayer checkOnline(@NotNull FakePlayer player) {
if (!player.isOnline()) {
this.playersByName.remove(player.getName());
this.playersByUUID.remove(player.getUUID());
this.remove(player);
return null;
}

View File

@ -1,5 +1,6 @@
package io.github.hello09x.fakeplayer.manager;
import io.github.hello09x.bedrock.task.Tasks;
import io.github.hello09x.fakeplayer.Main;
import io.github.hello09x.fakeplayer.config.FakeplayerConfig;
import io.github.hello09x.fakeplayer.entity.FakePlayer;
@ -13,12 +14,10 @@ import io.github.hello09x.fakeplayer.repository.UserConfigRepository;
import io.github.hello09x.fakeplayer.repository.model.Configs;
import io.github.hello09x.fakeplayer.util.AddressUtils;
import io.github.hello09x.fakeplayer.util.Commands;
import io.github.hello09x.fakeplayer.util.Tasker;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.bukkit.scheduler.BukkitRunnable;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@ -57,14 +56,11 @@ public class FakeplayerManager {
private FakeplayerManager() {
timer.scheduleAtFixedRate(() -> {
if (Bukkit.getServer().getTPS()[1] < config.getKaleTps()) {
new BukkitRunnable() {
@Override
public void run() {
if (removeAll() > 0) {
Tasks.runNextTick(Main.getInstance(), () -> {
if (removeAll("low tps") > 0) {
Bukkit.broadcast(text("[服务器过于卡顿, 已移除所有假人]", RED, ITALIC));
}
}
}.runTask(Main.getInstance());
});
}
}, 0, 60, TimeUnit.SECONDS
);
@ -103,19 +99,19 @@ public class FakeplayerManager {
try {
sn = name.isBlank() ? nameManager.register(creator) : nameManager.custom(creator, name);
} catch (IllegalCustomNameException e) {
creator.sendMessage(e.getMsg());
creator.sendMessage(e.getText());
return null;
}
var player = new FakePlayer(
var fake = new FakePlayer(
creator.getName(),
AddressUtils.getAddress(creator),
sn,
removeAt
);
var bukkitPlayer = player.getBukkitPlayer();
bukkitPlayer.playerListName(text(bukkitPlayer.getName(), GRAY, ITALIC));
var player = fake.getBukkitPlayer();
player.playerListName(text(player.getName(), GRAY, ITALIC));
boolean invulnerable = true, lookAtEntity = true, collidable = true, pickupItems = true;
if (creator instanceof Player p) {
@ -125,7 +121,7 @@ public class FakeplayerManager {
collidable = userConfigRepository.selectOrDefault(creatorId, Configs.collidable);
pickupItems = userConfigRepository.selectOrDefault(creatorId, Configs.pickup_items);
}
player.spawn(new SpawnOption(
fake.spawn(new SpawnOption(
spawnAt,
invulnerable,
collidable,
@ -133,15 +129,15 @@ public class FakeplayerManager {
pickupItems
));
playerList.add(player);
usedIdRepository.add(bukkitPlayer.getUniqueId());
playerList.add(fake);
usedIdRepository.add(player.getUniqueId());
Tasker.later(() -> {
dispatchCommands(bukkitPlayer, config.getPreparingCommands());
performCommands(bukkitPlayer, config.getSelfCommands());
}, 20);
Tasks.runLater(Main.getInstance(), 20, () -> {
dispatchCommands(player, config.getPreparingCommands());
performCommands(player, config.getSelfCommands());
});
return bukkitPlayer;
return player;
}
/**
@ -172,22 +168,6 @@ public class FakeplayerManager {
.orElse(null);
}
/**
* 根据名称删除假人
*
* @param name 名称
* @return 名称对应的玩家不在线或者不是假人
*/
public boolean remove(@NotNull String name) {
var player = get(name);
if (player == null) {
return false;
}
player.kick(text("[fakeplayer] removed"));
return true;
}
/**
* 获取一个假人的创建者, 如果这个玩家不是假人, 则为 {@code null}
*
@ -201,14 +181,32 @@ public class FakeplayerManager {
.orElse(null);
}
/**
* 根据名称删除假人
*
* @param name 名称
* @return 名称对应的玩家不在线或者不是假人
*/
public boolean remove(@NotNull String name, @Nullable String reason) {
var player = this.get(name);
if (player == null) {
return false;
}
player.kick(text("[fakeplayer] " + (reason == null ? "removed" : reason)));
return true;
}
/**
* 移除所有假人
*
* @return 移除的假人数量
*/
public int removeAll() {
public int removeAll(@Nullable String reason) {
var fakers = getAll();
fakers.forEach(Player::kick);
for (var f : fakers) {
f.kick(text("[fakeplayer] " + (reason == null ? "removed" : reason)));
}
return fakers.size();
}

View File

@ -79,7 +79,7 @@ public class WildFakeplayerManager implements PluginMessageListener {
@Override
public void run() {
for (var target : targets) {
manager.remove(target.getName());
manager.remove(target.getName(), "creator offline");
}
log.info(String.format("玩家 %s 已不在线, 移除他创建的 %d 个假人", entry.getKey(), entry.getValue().size()));
}

View File

@ -11,6 +11,7 @@ import net.minecraft.world.phys.BlockHitResult;
import net.minecraft.world.phys.EntityHitResult;
import net.minecraft.world.phys.HitResult;
import org.bukkit.Bukkit;
import org.bukkit.entity.Damageable;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@ -234,6 +235,7 @@ public enum Action {
},
LOOK_AT_NEAREST_ENTITY("目视实体") {
@Override
@SuppressWarnings("UnstableApiUsage")
public boolean tick(@NotNull ActionPack ap, @NotNull ActionSetting setting) {
@ -243,14 +245,21 @@ public enum Action {
return true;
}
var entities = bukkitPlayer.getNearbyEntities(4.5, 4.5, 4.5);
if (entities.isEmpty()) {
var target = bukkitPlayer
.getNearbyEntities(4.5, 4.5, 4.5)
.stream()
.filter(e -> e instanceof Damageable)
.findAny()
.orElse(null);
if (target == null) {
return false;
}
bukkitPlayer.lookAt(entities.get(0), LookAnchor.EYES, LookAnchor.EYES);
bukkitPlayer.lookAt(target, LookAnchor.EYES, LookAnchor.EYES);
return true;
}
},
DROP_ITEM("丢弃手上物品") {
@ -283,6 +292,7 @@ public enum Action {
};
@NotNull
public final String name;
static @Nullable HitResult getTarget(@NotNull ServerPlayer player) {
@ -300,11 +310,12 @@ public enum Action {
public abstract boolean tick(@NotNull ActionPack ap, @NotNull ActionSetting setting);
public void stop(@NotNull ActionPack ap, @NotNull ActionSetting setting) {}
public void inactiveTick(@NotNull ActionPack ap, @NotNull ActionSetting setting) {
this.stop(ap, setting);
}
public void stop(@NotNull ActionPack ap, @NotNull ActionSetting setting) {
}
}

View File

@ -16,7 +16,7 @@ public class ActionManager {
public final static ActionManager instance = new ActionManager();
private final ConcurrentMap<UUID, Map<Action, ActionTicker>> MANAGERS = new ConcurrentHashMap<>();
private final ConcurrentMap<UUID, Map<Action, ActionTicker>> managers = new ConcurrentHashMap<>();
public ActionManager() {
new BukkitRunnable() {
@ -33,7 +33,7 @@ public class ActionManager {
@NotNull Action action,
@NotNull ActionSetting setting
) {
var managers = MANAGERS.computeIfAbsent(player.getUniqueId(), key -> new HashMap<>());
var managers = this.managers.computeIfAbsent(player.getUniqueId(), key -> new HashMap<>());
var ticker = managers.computeIfAbsent(action, key -> new ActionTicker(
Main.getNms().getServerPlayer(player),
action,
@ -43,11 +43,13 @@ public class ActionManager {
}
public void tick() {
var itr = MANAGERS.entrySet().iterator();
var itr = managers.entrySet().iterator();
while (itr.hasNext()) {
var entry = itr.next();
var player = Bukkit.getPlayer(entry.getKey());
if (player == null) {
// 假人下线
itr.remove();
for (var ticker : entry.getValue().values()) {
ticker.stop();
@ -55,6 +57,7 @@ public class ActionManager {
continue;
}
// do tick
for (var ticker : entry.getValue().values()) {
ticker.tick();
}

View File

@ -2,26 +2,57 @@ package io.github.hello09x.fakeplayer.manager.action;
import net.minecraft.core.BlockPos;
import net.minecraft.server.level.ServerPlayer;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public class ActionPack {
/**
* 假人玩家
*/
@NotNull
public final ServerPlayer player;
/**
* 左键数据
*/
@NotNull
public final AttackActionPack attack = new AttackActionPack();
/**
* 右键相关数据
*/
@NotNull
public final UseActionPack use = new UseActionPack();
public ActionPack(ServerPlayer player) {
public ActionPack(@NotNull ServerPlayer player) {
this.player = player;
}
public final static class AttackActionPack {
/**
* 当前左键的目标位置
*/
@Nullable
public BlockPos pos;
/**
* 破坏方块的进度
*/
public float progress;
/**
* 冷却, 单位: tick
*/
public int freeze;
}
public final static class UseActionPack {
/**
* 冷却, 单位: tick
*/
public int freeze;
}

View File

@ -2,14 +2,29 @@ package io.github.hello09x.fakeplayer.manager.action;
import net.minecraft.server.level.ServerPlayer;
import org.jetbrains.annotations.NotNull;
public class ActionTicker {
/**
* 行为类型
*/
@NotNull
public final Action action;
/**
* 行为数据
*/
@NotNull
public final ActionPack actionPack;
/**
* 行为设置
*/
@NotNull
public ActionSetting setting;
public ActionTicker(ServerPlayer player, Action action, ActionSetting setting) {
public ActionTicker(@NotNull ServerPlayer player, @NotNull Action action, @NotNull ActionSetting setting) {
this.action = action;
this.setting = setting;
this.actionPack = new ActionPack(player);

View File

@ -4,11 +4,14 @@ import io.github.hello09x.fakeplayer.Main;
import io.github.hello09x.fakeplayer.config.FakeplayerConfig;
import io.github.hello09x.fakeplayer.manager.naming.exception.IllegalCustomNameException;
import io.github.hello09x.fakeplayer.repository.UsedIdRepository;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.RandomStringUtils;
import org.bukkit.Bukkit;
import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
@ -33,6 +36,36 @@ public class NameManager {
private final FakeplayerConfig config = FakeplayerConfig.instance;
private final Map<String, NameSource> nameSources = new HashMap<>();
private final String serverId;
public NameManager() {
var file = new File(Main.getInstance().getDataFolder(), "serverid");
serverId = Optional.ofNullable(readServerId(file)).orElseGet(() -> {
var uuid = UUID.randomUUID().toString();
try (var out = new FileWriter(file, StandardCharsets.UTF_8)) {
IOUtils.write(uuid, out);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
return uuid;
});
}
private static @Nullable String readServerId(@NotNull File file) {
if (!file.exists()) {
return null;
}
String serverId;
try(var in = new FileReader(file, StandardCharsets.UTF_8)) {
serverId = IOUtils.toString(in);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
return serverId.isBlank() ? null : serverId;
}
/**
* 通过名称生成 UUID
*
@ -40,7 +73,8 @@ public class NameManager {
* @return UUID
*/
private @NotNull UUID uuidFromName(@NotNull String name) {
var uuid = UUID.nameUUIDFromBytes(name.getBytes(StandardCharsets.UTF_8));
var base = serverId + ":" + name;
var uuid = UUID.nameUUIDFromBytes(base.getBytes(StandardCharsets.UTF_8));
if (!usedIdRepository.exists(uuid) && Bukkit.getOfflinePlayer(uuid).hasPlayedBefore()) {
uuid = UUID.randomUUID();
log.warning(String.format("Could not generate a UUID bound with name '%s' which is never played at this server, using random UUID as fallback: %s", name, uuid));
@ -134,17 +168,17 @@ public class NameManager {
* @param group 分组
* @param sequence 序列
*/
public void unregister(@NotNull String group, @NotNull Integer sequence) {
public void unregister(@NotNull String group, int sequence) {
Optional.ofNullable(nameSources.get(group)).ifPresent(ns -> ns.push(sequence));
}
/**
* 归还序列名
*
* @param sequenceName 序列名
* @param sn 序列名
*/
public void unregister(@NotNull SequenceName sequenceName) {
this.unregister(sequenceName.group(), sequenceName.sequence());
public void unregister(@NotNull SequenceName sn) {
this.unregister(sn.group(), sn.sequence());
}

View File

@ -4,8 +4,14 @@ import java.util.LinkedList;
public class NameSource {
/**
* 接下来可以使用的名称序号
*/
private final LinkedList<Integer> names;
/**
* 容量
*/
private volatile int capacity;
public NameSource(int initializeCapacity) {
@ -20,6 +26,11 @@ public class NameSource {
this(0);
}
/**
* 获取一个可使用的名称序号
*
* @return 名称序号
*/
public synchronized int pop() {
if (names.isEmpty()) {
var newCapacity = capacity * 2;
@ -31,6 +42,11 @@ public class NameSource {
return names.pop();
}
/**
* 归还一个名称序号
*
* @param i 名称序号
*/
public synchronized void push(int i) {
if (i >= capacity) {
return;

View File

@ -1,11 +1,29 @@
package io.github.hello09x.fakeplayer.manager.naming;
import org.jetbrains.annotations.NotNull;
import java.util.UUID;
/**
* 序列名
*
* @param group 分组
* @param sequence 序列
* @param uuid UUID
* @param name 名称
*/
public record SequenceName(
@NotNull
String group,
Integer sequence,
int sequence,
@NotNull
UUID uuid,
@NotNull
String name
) {
}

View File

@ -8,11 +8,11 @@ import org.jetbrains.annotations.NotNull;
public class IllegalCustomNameException extends IllegalArgumentException {
@Getter
private final Component msg;
private final Component text;
public IllegalCustomNameException(@NotNull TextComponent message) {
super(message.content());
this.msg = message;
this.text = message;
}
}

View File

@ -38,6 +38,9 @@ public class UsedIdRepository {
return UUIDS.contains(uuid);
}
/**
* 从文件里读取使用过的 UUIDs
*/
public void load() {
var file = new File(Main.getInstance().getDataFolder(), "used-uuids.txt");
if (!file.exists() || !file.isFile()) {
@ -60,6 +63,9 @@ public class UsedIdRepository {
}
}
/**
* 将使用过的 UUIDs 写入文件
*/
public void saveAll() {
var folder = Main.getInstance().getDataFolder();
if (!folder.exists() && !folder.mkdirs()) {

View File

@ -1,129 +0,0 @@
package io.github.hello09x.fakeplayer.util;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
public class Experience {
private Experience() {
}
/**
* Calculate a player's total experience based on level and progress to next.
*
* @param player the Player
* @return the amount of experience the Player has
* @see <a href=http://minecraft.gamepedia.com/Experience#Leveling_up>Experience#Leveling_up</a>
*/
public static int getExp(@NotNull Player player) {
return getExpFromLevel(player.getLevel())
+ Math.round(getExpToNext(player.getLevel()) * player.getExp());
}
/**
* Calculate total experience based on level.
*
* @param level the level
* @return the total experience calculated
* @see <a href=http://minecraft.gamepedia.com/Experience#Leveling_up>Experience#Leveling_up</a>
*/
public static int getExpFromLevel(int level) {
if (level > 30) {
return (int) (4.5 * level * level - 162.5 * level + 2220);
}
if (level > 15) {
return (int) (2.5 * level * level - 40.5 * level + 360);
}
return level * level + 6 * level;
}
/**
* Calculate level (including progress to next level) based on total experience.
*
* @param exp the total experience
* @return the level calculated
*/
public static double getLevelFromExp(long exp) {
int level = getIntLevelFromExp(exp);
// Get remaining exp progressing towards next level. Cast to float for next bit of math.
float remainder = exp - (float) getExpFromLevel(level);
// Get level progress with float precision.
float progress = remainder / getExpToNext(level);
// Slap both numbers together and call it a day. While it shouldn't be possible for progress
// to be an invalid value (value < 0 || 1 <= value)
return ((double) level) + progress;
}
/**
* Calculate level based on total experience.
*
* @param exp the total experience
* @return the level calculated
*/
public static int getIntLevelFromExp(long exp) {
if (exp > 1395) {
return (int) ((Math.sqrt(72 * exp - 54215D) + 325) / 18);
}
if (exp > 315) {
return (int) (Math.sqrt(40 * exp - 7839D) / 10 + 8.1);
}
if (exp > 0) {
return (int) (Math.sqrt(exp + 9D) - 3);
}
return 0;
}
/**
* Get the total amount of experience required to progress to the next level.
*
* @param level the current level
* @see <a href=http://minecraft.gamepedia.com/Experience#Leveling_up>Experience#Leveling_up</a>
*/
private static int getExpToNext(int level) {
if (level >= 30) {
// Simplified formula. Internal: 112 + (level - 30) * 9
return level * 9 - 158;
}
if (level >= 15) {
// Simplified formula. Internal: 37 + (level - 15) * 5
return level * 5 - 38;
}
// Internal: 7 + level * 2
return level * 2 + 7;
}
/**
* Change a Player's experience.
*
* <p>This method is preferred over {@link Player#giveExp(int)}.
* <br>In older versions the method does not take differences in exp per level into account.
* This leads to overlevelling when granting players large amounts of experience.
* <br>In modern versions, while differing amounts of experience per level are accounted for, the
* approach used is loop-heavy and requires an excessive number of calculations, which makes it
* quite slow.
*
* @param player the Player affected
* @param exp the amount of experience to add or remove
*/
public static void changeExp(@NotNull Player player, int exp) {
exp += getExp(player);
if (exp < 0) {
exp = 0;
}
double levelAndExp = getLevelFromExp(exp);
int level = (int) levelAndExp;
player.setLevel(level);
player.setExp((float) (levelAndExp - level));
}
public static void clean(@NotNull Player player) {
player.setLevel(0);
player.setExp(0);
}
}

View File

@ -2,13 +2,32 @@ package io.github.hello09x.fakeplayer.util;
public class Mth {
public static double round(double num, double base) {
/**
* num base 向下取整
* <ul>
* <li>3.0, 0.5 -> 3.0</li>
* <li>3.1, 0.5 -> 3.0</li>
* <li>3.6, 0.5 -> 3.5</li>
* </ul>
*
* @param num
* @param base 基数
* @return 取整后的数
*/
public static double floor(double num, double base) {
if (num % base == 0) {
return num;
}
return Math.floor(num / base) * base;
}
/**
* 将一个数约束在范围以内
* @param value
* @param min 最小值
* @param max 最大值
* @return 约束后的数
*/
public static float clamp(float value, float min, float max) {
return value < min ? min : Math.min(value, max);
}

View File

@ -1,5 +1,6 @@
package io.github.hello09x.fakeplayer.util;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.lang.reflect.Field;
@ -7,7 +8,11 @@ import java.lang.reflect.Modifier;
public class Reflections {
public static @Nullable Field getFirstFieldByType(Class<?> clazz, Class<?> fieldType, boolean includeStatic) {
public static @Nullable Field getFirstFieldByType(
@NotNull Class<?> clazz,
@NotNull Class<?> fieldType,
boolean includeStatic
) {
for (var field : clazz.getDeclaredFields()) {
if (includeStatic ^ Modifier.isStatic(field.getModifiers())) {
continue;
@ -20,7 +25,11 @@ public class Reflections {
return null;
}
public static @Nullable Field getFirstFieldByAssignFromType(Class<?> clazz, Class<?> fieldType, boolean includeStatic) {
public static @Nullable Field getFirstFieldByAssignFromType(
@NotNull Class<?> clazz,
@NotNull Class<?> fieldType,
boolean includeStatic
) {
for (var field : clazz.getDeclaredFields()) {
if (includeStatic ^ Modifier.isStatic(field.getModifiers())) {
continue;

View File

@ -1,28 +0,0 @@
package io.github.hello09x.fakeplayer.util;
import io.github.hello09x.fakeplayer.Main;
import org.bukkit.scheduler.BukkitRunnable;
import org.bukkit.scheduler.BukkitTask;
import org.jetbrains.annotations.NotNull;
public class Tasker {
public static @NotNull BukkitTask nextTick(@NotNull Runnable runnable) {
return new BukkitRunnable() {
@Override
public void run() {
runnable.run();
}
}.runTask(Main.getInstance());
}
public static @NotNull BukkitTask later(@NotNull Runnable runnable, long delay) {
return new BukkitRunnable() {
@Override
public void run() {
runnable.run();
}
}.runTaskLater(Main.getInstance(), delay);
}
}

View File

@ -8,6 +8,9 @@ import org.jetbrains.annotations.Nullable;
import java.util.function.Predicate;
/**
* copy from fabric carpet mod
*/
public class Tracer {
public static @Nullable HitResult rayTrace(
@NotNull Entity source,

View File

@ -5,7 +5,7 @@ import org.bukkit.World;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public class BK {
public class Worlds {
private final static String WORLD_OVERWORLD = "world";

View File

@ -23,6 +23,35 @@ public class UpdateChecker {
this.repository = repository;
}
public static boolean isNew(@NotNull String current, @NotNull String other) {
var split1 = current.split("\\.");
var split2 = other.split("\\.");
if (split2.length > split1.length) {
// 如果 other 的版本号位数更多, 则认为是新版本号
return true;
}
if (split2.length < split1.length) {
// 如果 other 的版本号位数更少, 则认为是旧版本
return false;
}
// split2.length == split1.length
var length = split1.length;
for (int i = 0; i < length; i++) {
var v1 = Integer.parseInt(split1[i]);
var v2 = Integer.parseInt(split2[i]);
if (v1 == v2) {
continue;
}
return v1 < v2;
}
return false;
}
public @NotNull Release getLastRelease() throws IOException, InterruptedException {
var url = String.format("https://api.github.com/repos/%s/%s/releases/latest", this.author, this.repository);
var client = HttpClient.newHttpClient();
@ -31,7 +60,12 @@ public class UpdateChecker {
.GET()
.build();
var body = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)).body();
var response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
if (response.statusCode() != 200) {
throw new IllegalStateException("Not 200 response: " + response.statusCode() + ": " + response.body());
}
var body = response.body();
return gson.fromJson(body, Release.class);
}

View File

@ -37,12 +37,16 @@ name-template: ''
name-pattern: '^[a-zA-Z0-9_]+$'
# 跟随下线
# 假人创建者玩家下线时是否自动下线
# 假人创建者下线时是否也跟着下线
# 如果玩家只是切换服务器, 那么不会触发跟随下线
follow-quiting: true
# 退出时是否丢弃背包物品
# 有跨服背包同步的谨慎开启, 需验证是否会导致物品复制
# 验证过程:
# 1. 创建假人
# 2. 通过 /kill 之类的命令杀死这个假人,假人死前会将背包物品丢弃出来
# 3. 重新召唤这个假人, 查看他的背包数据有没有被复制了一份
drop-inventory-on-quiting: true
# 如果启用, 则一个 IP 只能创建 `maximum` 个假人
@ -59,7 +63,7 @@ kale-tps: 0
# 有一些需要在 "登陆" 时生成玩家档案的插件发生异常比如 LuckPerms
# 如果服务器没有出现严重的错误不需要理会这些异常, 只是这些插件无法对假人进行操作而已
# 开启也不一定能解决所有问题, 也可能因为一些 "限制新加入玩家" 的插件而导致假人出现问题, 并且会创建更多的第三方插件数据
simulate-login: false
simulate-login: true
# 预准备命令
# 假人诞生时会以控制台的身份按顺序执行以下命令

View File

@ -3,6 +3,7 @@ version: '${project.version}'
main: io.github.hello09x.fakeplayer.Main
api-version: '1.20'
author: hello09x
website: 'https://github.com/tanyaofei/minecraft-fakeplayer'
permissions:
fakeplayer.all: