mirror of
https://github.com/tanyaofei/minecraft-fakeplayer.git
synced 2025-09-14 19:36:45 +08:00
更多功能
This commit is contained in:
parent
0651bb0171
commit
4f5c731d02
2
.gitignore
vendored
2
.gitignore
vendored
@ -124,4 +124,6 @@ fabric.properties
|
||||
|
||||
server/
|
||||
|
||||
server2/
|
||||
|
||||
lib/
|
69
README.md
Normal file
69
README.md
Normal file
@ -0,0 +1,69 @@
|
||||
# FakePlayer - 假人插件
|
||||
|
||||
这个插件模拟出真实的玩家来保证区块的加载以及怪物的刷新
|
||||
|
||||
### 支持版本
|
||||
|
||||
1.20.1 的 paper, purpur(建议)
|
||||
|
||||
## 命令
|
||||
|
||||
+ /fp create - 创建一个假人
|
||||
+ /fp remove - 删除假人
|
||||
+ /fp tp - 传送到假人身边
|
||||
+ /fp tphere - 将假人传送到自己身边
|
||||
+ /fp tps - 与假人交换位置
|
||||
|
||||
此外,假人是一个模拟玩家,因此可以被任何指令所识别比如 `kick`, `tp`, `ban` 等等
|
||||
|
||||
## 权限
|
||||
|
||||
+ fakeplayer.spawn - 创建、删除假人
|
||||
+ fakeplayer.tp - 假人 tp 权限
|
||||
+ fakeplayer.admin - 管理员权限
|
||||
|
||||
## 配置项
|
||||
|
||||
这个不定时更新内容,具体以插件的 `config.yml 为准`
|
||||
|
||||
```yml
|
||||
# 服务器最多存在多少个假人
|
||||
# 默认: 1000
|
||||
server-limit: 1000
|
||||
|
||||
# 每个玩家最多创建多少个假人
|
||||
# 默认: 1
|
||||
player-limit: 1
|
||||
|
||||
# 每多少个 ticks 触发一次假人动作更新
|
||||
# 这个值调的越大, 假人的动作更新越慢
|
||||
# 单位 tick
|
||||
tick-period: 1
|
||||
|
||||
# 假人创建者玩家下线时是否自动下线
|
||||
follow-quiting: true
|
||||
|
||||
# 是否检测 IP
|
||||
# 如果启用, 则一个 IP 只能创建 `player-limit` 个假人
|
||||
# 能够避免玩家开小号疯狂创建假人
|
||||
detect-ip: false
|
||||
|
||||
# 服务器最近 5 分钟平均 TPS 低于这个值清除所有假人
|
||||
# 每 60 秒检测一次
|
||||
# 默认: 0, 即不开启, 因为移除假人可能导致玩家炸机器, 按需开启吧
|
||||
kale-tps: 0
|
||||
|
||||
# 假人不定时胡言乱语
|
||||
# 每 6000 ticks(大约 5 分钟) 有 1/3 的几率胡言乱语
|
||||
nonsense:
|
||||
- 当个假人好累啊
|
||||
- 能不能给个凳子坐坐?
|
||||
- 靠,这里蚊子有点多
|
||||
- 腐竹,救我!我动不了了
|
||||
- 能不能让我玩会生存?
|
||||
- 你好,我是学生,v我50可以吗
|
||||
- 服务器将在 5 秒钟清除垃圾,请小心手上的物品 (
|
||||
- 你们卡吗,我怎么动不了了
|
||||
- 能不能在我这里建个挂机池?
|
||||
- 汤姆哥有点小帅
|
||||
```
|
2
pom.xml
2
pom.xml
@ -6,7 +6,7 @@
|
||||
|
||||
<groupId>io.github.hello09x</groupId>
|
||||
<artifactId>fakeplayer</artifactId>
|
||||
<version>0.0.1</version>
|
||||
<version>1_20_R1-0.0.2</version>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>fakeplayer</name>
|
||||
|
@ -4,6 +4,7 @@ import io.github.hello09x.fakeplayer.command.FakePlayerCommand;
|
||||
import io.github.hello09x.fakeplayer.listener.PlayerDeathListener;
|
||||
import io.github.hello09x.fakeplayer.listener.PlayerQuitListener;
|
||||
import io.github.hello09x.fakeplayer.manager.FakePlayerManager;
|
||||
import io.github.hello09x.fakeplayer.properties.FakeplayerProperties;
|
||||
import lombok.Getter;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
|
||||
@ -21,15 +22,21 @@ public final class Main extends JavaPlugin {
|
||||
getServer().getPluginCommand("fakeplayer").setExecutor(FakePlayerCommand.instance);
|
||||
}
|
||||
|
||||
{
|
||||
getServer().getPluginManager().registerEvents(PlayerQuitListener.instance, Main.getInstance());
|
||||
getServer().getPluginManager().registerEvents(PlayerDeathListener.instance, Main.getInstance());
|
||||
}
|
||||
|
||||
registerListeners();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisable() {
|
||||
FakePlayerManager.instance.removeFakePlayers();
|
||||
}
|
||||
|
||||
|
||||
private void registerListeners() {
|
||||
if (FakeplayerProperties.instance.isFollowQuiting()) {
|
||||
getServer().getPluginManager().registerEvents(PlayerQuitListener.instance, getInstance());
|
||||
}
|
||||
|
||||
getServer().getPluginManager().registerEvents(PlayerDeathListener.instance, getInstance());
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,11 +1,7 @@
|
||||
package io.github.hello09x.fakeplayer.command;
|
||||
|
||||
import io.github.hello09x.fakeplayer.command.admin.ReloadCommand;
|
||||
import io.github.hello09x.fakeplayer.command.admin.RemoveAllCommand;
|
||||
import io.github.hello09x.fakeplayer.command.player.CreateCommand;
|
||||
import io.github.hello09x.fakeplayer.command.player.RemoveCommand;
|
||||
import io.github.hello09x.fakeplayer.command.player.TpCommand;
|
||||
import io.github.hello09x.fakeplayer.command.player.TpHereCommand;
|
||||
import io.github.hello09x.fakeplayer.command.player.*;
|
||||
import io.github.tanyaofei.plugin.toolkit.command.ParentCommand;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
@ -14,15 +10,15 @@ public class FakePlayerCommand extends ParentCommand {
|
||||
|
||||
public final static FakePlayerCommand instance = new FakePlayerCommand(
|
||||
"假人相关命令",
|
||||
"fakeplayer"
|
||||
null
|
||||
);
|
||||
|
||||
static {
|
||||
instance.register("create", CreateCommand.instance);
|
||||
instance.register("remove", RemoveCommand.instance);
|
||||
instance.register("tp", TpCommand.instance);
|
||||
instance.register("tp", TpToCommand.instance);
|
||||
instance.register("tphere", TpHereCommand.instance);
|
||||
instance.register("removeall", RemoveAllCommand.instance);
|
||||
instance.register("tps", TpSwapCommand.instance);
|
||||
instance.register("reload", ReloadCommand.instance);
|
||||
}
|
||||
|
||||
|
@ -1,13 +1,11 @@
|
||||
package io.github.hello09x.fakeplayer.command.admin;
|
||||
|
||||
import io.github.hello09x.fakeplayer.manager.FakePlayerManager;
|
||||
import io.github.hello09x.fakeplayer.properties.FakeplayerProperties;
|
||||
import io.github.tanyaofei.plugin.toolkit.command.ExecutableCommand;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import org.bukkit.command.Command;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import properties.FakeplayerProperties;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
|
@ -1,56 +0,0 @@
|
||||
package io.github.hello09x.fakeplayer.command.admin;
|
||||
|
||||
import io.github.hello09x.fakeplayer.manager.FakePlayerManager;
|
||||
import io.github.tanyaofei.plugin.toolkit.command.ExecutableCommand;
|
||||
import org.bukkit.command.Command;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import static net.kyori.adventure.text.Component.text;
|
||||
import static net.kyori.adventure.text.format.NamedTextColor.GRAY;
|
||||
|
||||
public class RemoveAllCommand extends ExecutableCommand {
|
||||
|
||||
private final FakePlayerManager manager = FakePlayerManager.instance;
|
||||
|
||||
public final static RemoveAllCommand instance = new RemoveAllCommand(
|
||||
"移除所有假人",
|
||||
"/fp removeall",
|
||||
"fakeplayer.admin"
|
||||
);
|
||||
|
||||
public RemoveAllCommand(@NotNull String description, @NotNull String usage, @Nullable String permission) {
|
||||
super(description, usage, permission);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean execute(
|
||||
@NotNull CommandSender sender,
|
||||
@NotNull Command command,
|
||||
@NotNull String label,
|
||||
@NotNull String[] args
|
||||
) {
|
||||
if (args.length == 0) {
|
||||
var count = manager.removeFakePlayers();
|
||||
sender.sendMessage(text(String.format("已移除 %d 个假人", count), GRAY));
|
||||
return true;
|
||||
}
|
||||
|
||||
// TODO
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable List<String> onTabComplete(
|
||||
@NotNull CommandSender sender,
|
||||
@NotNull Command command,
|
||||
@NotNull String label,
|
||||
@NotNull String[] args
|
||||
) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
@ -3,7 +3,6 @@ package io.github.hello09x.fakeplayer.command.player;
|
||||
import io.github.hello09x.fakeplayer.command.MessageException;
|
||||
import io.github.hello09x.fakeplayer.manager.FakePlayerManager;
|
||||
import io.github.tanyaofei.plugin.toolkit.command.ExecutableCommand;
|
||||
import net.kyori.adventure.sound.Sound;
|
||||
import org.bukkit.command.Command;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.entity.Player;
|
||||
|
@ -14,14 +14,13 @@ import java.util.List;
|
||||
|
||||
import static net.kyori.adventure.text.Component.text;
|
||||
import static net.kyori.adventure.text.format.NamedTextColor.GRAY;
|
||||
import static net.kyori.adventure.text.format.NamedTextColor.RED;
|
||||
|
||||
public class CreateCommand extends ExecutableCommand {
|
||||
|
||||
public final static CreateCommand instance = new CreateCommand(
|
||||
"创建假人",
|
||||
"/fp create",
|
||||
"fakeplayer"
|
||||
"/fp create [名称]",
|
||||
"fakeplayer.spawn"
|
||||
);
|
||||
|
||||
private final FakePlayerManager manager = FakePlayerManager.instance;
|
||||
@ -60,7 +59,7 @@ public class CreateCommand extends ExecutableCommand {
|
||||
manager.spawnFakePlayer(sender, new Location(world, x, y, z));
|
||||
}
|
||||
|
||||
sender.sendMessage(text("创建成功", GRAY));
|
||||
sender.sendMessage(text("创建假人成功", GRAY));
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -2,14 +2,17 @@ package io.github.hello09x.fakeplayer.command.player;
|
||||
|
||||
import io.github.hello09x.fakeplayer.manager.FakePlayerManager;
|
||||
import io.github.tanyaofei.plugin.toolkit.command.ExecutableCommand;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.bukkit.command.Command;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static net.kyori.adventure.text.Component.text;
|
||||
import static net.kyori.adventure.text.format.NamedTextColor.GRAY;
|
||||
@ -20,7 +23,7 @@ public class RemoveCommand extends ExecutableCommand {
|
||||
public final static RemoveCommand instance = new RemoveCommand(
|
||||
"移除假人",
|
||||
"/fp remove",
|
||||
"fakeplayer"
|
||||
"fakeplayer.spawn"
|
||||
);
|
||||
|
||||
private final FakePlayerManager manager = FakePlayerManager.instance;
|
||||
@ -40,33 +43,35 @@ public class RemoveCommand extends ExecutableCommand {
|
||||
@NotNull String label,
|
||||
@NotNull String[] args
|
||||
) {
|
||||
if (args.length == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!(sender instanceof Player creator)) {
|
||||
sender.sendMessage(text("你不是玩家...", RED));
|
||||
var removed = new ArrayList<>(args.length);
|
||||
if (args.length < 1) {
|
||||
sender.sendMessage(text("请指定要移除的假人名称...", RED));
|
||||
return true;
|
||||
}
|
||||
|
||||
var name = args[0];
|
||||
if (name.equals("@all") || name.equals("@a")) {
|
||||
int count = manager.removeFakePlayers(creator);
|
||||
for (var name : args) {
|
||||
if (name.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (sender.isOp() && (name.equals("@all") || name.equals("@a"))) {
|
||||
var count = manager.removeFakePlayers();
|
||||
sender.sendMessage(text(String.format("已移除 %d 个假人", count), GRAY));
|
||||
return true;
|
||||
}
|
||||
|
||||
var fake = creator.isOp()
|
||||
? manager.getFakePlayer(name)
|
||||
: manager.getFakePlayer(creator, name);
|
||||
if (manager.removeFakePlayer(name)) {
|
||||
removed.add(name);
|
||||
}
|
||||
|
||||
if (fake == null) {
|
||||
sender.sendMessage(text("假人不存在", RED));
|
||||
}
|
||||
|
||||
if (removed.isEmpty()) {
|
||||
sender.sendMessage(text("找不到对应名称的假人...", RED));
|
||||
return true;
|
||||
}
|
||||
|
||||
fake.kick();
|
||||
sender.sendMessage(text("成功移除假人", GRAY));
|
||||
sender.sendMessage(text("已移除这些假人: " + StringUtils.join(" ", removed), GRAY));
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -78,20 +83,19 @@ public class RemoveCommand extends ExecutableCommand {
|
||||
@NotNull String[] args
|
||||
) {
|
||||
if (args.length != 1) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
if (!(sender instanceof Player creator)) {
|
||||
return Collections.emptyList();
|
||||
return sender.isOp() ? Collections.singletonList("@all") : Collections.emptyList();
|
||||
}
|
||||
|
||||
var fakes = creator.isOp()
|
||||
var fakers = sender.isOp()
|
||||
? manager.getFakePlayers()
|
||||
: manager.getFakePlayers(creator);
|
||||
: manager.getFakePlayers(sender);
|
||||
|
||||
return fakes
|
||||
.stream()
|
||||
.map(Player::getName)
|
||||
.filter(name -> args[0].isBlank() || name.toLowerCase().contains(args[0].toLowerCase()))
|
||||
var names = sender.isOp()
|
||||
? Stream.concat(fakers.stream().map(Player::getName), Stream.of("@all"))
|
||||
: fakers.stream().map(Player::getName);
|
||||
|
||||
return names
|
||||
.filter(name -> name.equals("@all") || args[0].isBlank() || name.toLowerCase().contains(args[0].toLowerCase()))
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
|
@ -15,7 +15,7 @@ public class TpHereCommand extends AbstractTeleportCommand {
|
||||
public final static TpHereCommand instance = new TpHereCommand(
|
||||
"传送假人到身边",
|
||||
"/fp tphere <名称>",
|
||||
"fakeplayer"
|
||||
"fakeplayer.tp"
|
||||
);
|
||||
|
||||
public TpHereCommand(
|
||||
|
@ -0,0 +1,58 @@
|
||||
package io.github.hello09x.fakeplayer.command.player;
|
||||
|
||||
import io.github.hello09x.fakeplayer.command.MessageException;
|
||||
import org.bukkit.Sound;
|
||||
import org.bukkit.SoundCategory;
|
||||
import org.bukkit.command.Command;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.player.PlayerTeleportEvent;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import static net.kyori.adventure.text.Component.text;
|
||||
import static net.kyori.adventure.text.format.NamedTextColor.RED;
|
||||
|
||||
public class TpSwapCommand extends AbstractTeleportCommand {
|
||||
|
||||
public final static TpSwapCommand instance = new TpSwapCommand(
|
||||
"与假人交换位置",
|
||||
"/fp tps <名称>",
|
||||
"fakeplayer.tp"
|
||||
);
|
||||
|
||||
public TpSwapCommand(@NotNull String description, @NotNull String usage, @Nullable String permission) {
|
||||
super(description, usage, permission);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean execute(
|
||||
@NotNull CommandSender sender,
|
||||
@NotNull Command command,
|
||||
@NotNull String label,
|
||||
@NotNull String[] args
|
||||
) {
|
||||
if (!(sender instanceof Player creator)) {
|
||||
sender.sendMessage(text("你不是玩家...", RED));
|
||||
return true;
|
||||
}
|
||||
|
||||
Player fake;
|
||||
try {
|
||||
fake = getFakePlayer(creator, args);
|
||||
} catch (MessageException e) {
|
||||
sender.sendMessage(e.getText());
|
||||
return true;
|
||||
}
|
||||
|
||||
var l1 = creator.getLocation();
|
||||
var l2 = fake.getLocation();
|
||||
|
||||
fake.teleport(l1, PlayerTeleportEvent.TeleportCause.PLUGIN);
|
||||
l1.getWorld().playSound(l1, Sound.ENTITY_ENDERMAN_TELEPORT, SoundCategory.PLAYERS, 1.0F, 1.0F);
|
||||
|
||||
creator.teleport(l2, PlayerTeleportEvent.TeleportCause.PLUGIN);
|
||||
l2.getWorld().playSound(l2, Sound.ENTITY_ENDERMAN_TELEPORT, SoundCategory.PLAYERS, 1.0F, 1.0F);
|
||||
return true;
|
||||
}
|
||||
}
|
@ -11,20 +11,19 @@ import org.jetbrains.annotations.Nullable;
|
||||
import static net.kyori.adventure.text.Component.text;
|
||||
import static net.kyori.adventure.text.format.NamedTextColor.RED;
|
||||
|
||||
public class TpCommand extends AbstractTeleportCommand {
|
||||
|
||||
public class TpToCommand extends AbstractTeleportCommand {
|
||||
|
||||
private final FakePlayerManager manager = FakePlayerManager.instance;
|
||||
|
||||
|
||||
public final static TpCommand instance = new TpCommand(
|
||||
public final static TpToCommand instance = new TpToCommand(
|
||||
"传送到假人身边",
|
||||
"/fp tp <名称>",
|
||||
"fakeplayer"
|
||||
"fakeplayer.tp"
|
||||
);
|
||||
|
||||
|
||||
public TpCommand(@NotNull String description, @NotNull String usage, @Nullable String permission) {
|
||||
public TpToCommand(@NotNull String description, @NotNull String usage, @Nullable String permission) {
|
||||
super(description, usage, permission);
|
||||
}
|
||||
|
@ -20,7 +20,7 @@ public class EmptyNetworkManager extends Connection {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void send(Packet packet, PacketSendListener genericfuturelistener) {
|
||||
public void send(Packet packet, PacketSendListener listener) {
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -5,22 +5,26 @@ import io.github.hello09x.fakeplayer.Main;
|
||||
import io.github.hello09x.fakeplayer.core.EmptyAdvancements;
|
||||
import io.github.hello09x.fakeplayer.core.EmptyConnection;
|
||||
import io.github.hello09x.fakeplayer.core.EmptyNetworkManager;
|
||||
import io.github.hello09x.fakeplayer.properties.FakeplayerProperties;
|
||||
import io.github.hello09x.fakeplayer.util.ReflectionUtils;
|
||||
import io.papermc.paper.entity.LookAnchor;
|
||||
import lombok.Getter;
|
||||
import lombok.SneakyThrows;
|
||||
import net.minecraft.network.protocol.PacketFlow;
|
||||
import net.minecraft.network.protocol.game.ClientboundPlayerInfoUpdatePacket;
|
||||
import net.minecraft.network.protocol.game.ServerboundClientInformationPacket;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.PlayerAdvancements;
|
||||
import net.minecraft.server.level.ChunkMap;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.server.level.*;
|
||||
import net.minecraft.world.entity.HumanoidArm;
|
||||
import net.minecraft.world.entity.player.ChatVisiblity;
|
||||
import org.apache.commons.lang3.RandomUtils;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Chunk;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Sound;
|
||||
import org.bukkit.craftbukkit.v1_20_R1.CraftServer;
|
||||
import org.bukkit.craftbukkit.v1_20_R1.CraftWorld;
|
||||
import org.bukkit.craftbukkit.v1_20_R1.entity.CraftEntity;
|
||||
import org.bukkit.craftbukkit.v1_20_R1.entity.CraftPlayer;
|
||||
import org.bukkit.entity.Player;
|
||||
@ -32,21 +36,29 @@ import org.jetbrains.annotations.NotNull;
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.bukkit.Sound.ENTITY_ENDERMAN_TELEPORT;
|
||||
|
||||
public class FakePlayer extends ServerPlayer {
|
||||
|
||||
public final static Field advancements = ReflectionUtils.getFirstFieldByType(ServerPlayer.class, PlayerAdvancements.class, false);
|
||||
private final static Field advancements = ReflectionUtils.getFirstFieldByType(ServerPlayer.class, PlayerAdvancements.class, false);
|
||||
private final static Field distanceManager = ReflectionUtils.getFirstFieldByAssignFromType(ChunkMap.class, DistanceManager.class, false);
|
||||
|
||||
@Getter
|
||||
private @NotNull
|
||||
final Location spawnLocation;
|
||||
|
||||
@Getter
|
||||
private final String creator;
|
||||
|
||||
private Player bukkitPlayer;
|
||||
|
||||
private volatile DistanceManager dm;
|
||||
|
||||
public FakePlayer(
|
||||
@NotNull String creator,
|
||||
@NotNull MinecraftServer server,
|
||||
@NotNull ServerLevel world,
|
||||
@NotNull UUID uniqueId,
|
||||
@ -54,6 +66,7 @@ public class FakePlayer extends ServerPlayer {
|
||||
@NotNull Location at
|
||||
) {
|
||||
super(server, world, new GameProfile(uniqueId, name));
|
||||
this.creator = creator;
|
||||
this.spawnLocation = at;
|
||||
|
||||
try {
|
||||
@ -90,33 +103,37 @@ public class FakePlayer extends ServerPlayer {
|
||||
return at.getWorld().isChunkLoaded(x, z);
|
||||
}
|
||||
|
||||
public @NotNull Player spawn() {
|
||||
public @NotNull Player spawn(long tickPeriod) {
|
||||
this.boardcast();
|
||||
this.addEntityToWorld();
|
||||
|
||||
var p = Objects.requireNonNull(Bukkit.getPlayer(this.uuid));
|
||||
p.setSleepingIgnored(true);
|
||||
p.setPersistent(false);
|
||||
p.setInvulnerable(true);
|
||||
bukkitPlayer = Objects.requireNonNull(Bukkit.getPlayer(this.uuid));
|
||||
bukkitPlayer.setSleepingIgnored(true);
|
||||
bukkitPlayer.setPersistent(false);
|
||||
bukkitPlayer.setInvulnerable(true);
|
||||
|
||||
new BukkitRunnable() {
|
||||
@Override
|
||||
@SneakyThrows
|
||||
public void run() {
|
||||
if (!p.isOnline()) {
|
||||
if (!bukkitPlayer.isOnline()) {
|
||||
cancel();
|
||||
}
|
||||
doTick();
|
||||
tickCount++;
|
||||
}
|
||||
}.runTaskTimer(Main.getInstance(), 0, 1);
|
||||
return p;
|
||||
}.runTaskTimer(Main.getInstance(), 0, tickPeriod);
|
||||
return bukkitPlayer;
|
||||
}
|
||||
|
||||
/**
|
||||
* 通知其他玩家加入假人
|
||||
*/
|
||||
@SuppressWarnings("all")
|
||||
public void boardcast() {
|
||||
this.updateOptions(new ServerboundClientInformationPacket(
|
||||
"en_us",
|
||||
10,
|
||||
Bukkit.getServer().getViewDistance(),
|
||||
ChatVisiblity.FULL,
|
||||
false,
|
||||
0,
|
||||
@ -159,6 +176,9 @@ public class FakePlayer extends ServerPlayer {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 将实体加入到世界
|
||||
*/
|
||||
public void addEntityToWorld() {
|
||||
var entity = this.getBukkitEntity();
|
||||
var handle = (ServerPlayer) ((CraftEntity) entity).getHandle();
|
||||
@ -167,6 +187,7 @@ public class FakePlayer extends ServerPlayer {
|
||||
spawnLocation.getChunk().load();
|
||||
}
|
||||
|
||||
|
||||
handle.level().addFreshEntity(handle, CreatureSpawnEvent.SpawnReason.CUSTOM);
|
||||
((CraftServer) Bukkit.getServer()).getHandle().respawn(
|
||||
this,
|
||||
@ -179,5 +200,90 @@ public class FakePlayer extends ServerPlayer {
|
||||
spawnLocation.getWorld().playSound(spawnLocation, Sound.ENTITY_ENDERMAN_TELEPORT, 1.0F, 1.0F);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void doTick() {
|
||||
super.doTick();
|
||||
super.baseTick();
|
||||
this.tickChunks();
|
||||
this.tickLookAt();
|
||||
this.tickNonsense();
|
||||
}
|
||||
|
||||
/**
|
||||
* 看向最近的实体
|
||||
*/
|
||||
public void tickLookAt() {
|
||||
if (this.tickCount % 20 != 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
var nearby = this.bukkitPlayer.getNearbyEntities(3, 3, 3);
|
||||
if (nearby.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
bukkitPlayer.lookAt(nearby.get(0), LookAnchor.EYES, LookAnchor.EYES);
|
||||
}
|
||||
|
||||
/**
|
||||
* 胡言乱语
|
||||
*/
|
||||
public void tickNonsense() {
|
||||
if (this.tickCount == 0 || this.tickCount % 6_000 != 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (RandomUtils.nextInt(0, 3) != 1) {
|
||||
// 1/3 的几率
|
||||
return;
|
||||
}
|
||||
|
||||
var nonsense = FakeplayerProperties.instance.getNonsense();
|
||||
if (nonsense.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
bukkitPlayer.chat(nonsense.get(RandomUtils.nextInt(0, nonsense.size())));
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新区块
|
||||
*/
|
||||
@SneakyThrows
|
||||
public void tickChunks() {
|
||||
if (this.dm == null) {
|
||||
var chunkMap = ((CraftWorld) bukkitPlayer.getWorld()).getHandle().getChunkSource().chunkMap;
|
||||
this.dm = (DistanceManager) distanceManager.get(chunkMap);
|
||||
}
|
||||
|
||||
var pos = ((CraftPlayer) bukkitPlayer).getHandle().chunkPosition();
|
||||
dm.addRegionTicketAtDistance(TicketType.PLAYER, pos, Bukkit.getServer().getSimulationDistance(), pos);
|
||||
}
|
||||
|
||||
public @NotNull Player getBukkitPlayer() {
|
||||
if (this.bukkitPlayer == null) {
|
||||
throw new IllegalStateException("fake player never spawn");
|
||||
}
|
||||
return this.bukkitPlayer;
|
||||
}
|
||||
|
||||
public List<Chunk> getNearbyChunks() {
|
||||
var distance = Math.min(Bukkit.getSimulationDistance(), this.bukkitPlayer.getSimulationDistance());
|
||||
|
||||
var center = bukkitPlayer.getChunk();
|
||||
var minX = center.getX() - distance;
|
||||
var maxX = center.getX() + distance;
|
||||
var minZ = center.getZ() - distance;
|
||||
var maxZ = center.getZ() + distance;
|
||||
|
||||
var world = this.bukkitPlayer.getWorld();
|
||||
var ret = new ArrayList<Chunk>(((distance * 2) + 1) * (distance * 2) + 1);
|
||||
for (int x = minX; x <= maxX; x++) {
|
||||
for (int z = minZ; z <= maxZ; z++) {
|
||||
ret.add(world.getChunkAt(x, z));
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -2,6 +2,9 @@ package io.github.hello09x.fakeplayer.manager;
|
||||
|
||||
import io.github.hello09x.fakeplayer.Main;
|
||||
import io.github.hello09x.fakeplayer.entity.FakePlayer;
|
||||
import io.github.hello09x.fakeplayer.properties.FakeplayerProperties;
|
||||
import io.github.hello09x.fakeplayer.util.AddressUtils;
|
||||
import net.kyori.adventure.text.format.Style;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.command.CommandSender;
|
||||
@ -9,16 +12,19 @@ import org.bukkit.craftbukkit.v1_20_R1.CraftServer;
|
||||
import org.bukkit.craftbukkit.v1_20_R1.CraftWorld;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.metadata.FixedMetadataValue;
|
||||
import org.bukkit.scheduler.BukkitRunnable;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import properties.FakeplayerProperties;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Timer;
|
||||
import java.util.TimerTask;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static net.kyori.adventure.text.Component.text;
|
||||
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.ITALIC;
|
||||
|
||||
public class FakePlayerManager {
|
||||
|
||||
@ -26,8 +32,31 @@ public class FakePlayerManager {
|
||||
|
||||
private final static String META_KEY_CREATOR = "fakeplayer:creator";
|
||||
|
||||
private final static String META_KEY_CREATOR_IP = "fakeplayer:creator-ip";
|
||||
|
||||
private final FakeplayerProperties properties = FakeplayerProperties.instance;
|
||||
|
||||
private volatile int count = 1;
|
||||
|
||||
public FakePlayerManager() {
|
||||
// 服务器 tps 过低删除所有假人
|
||||
new Timer().schedule(new TimerTask() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (Bukkit.getServer().getTPS()[1] < properties.getKaleTps()) {
|
||||
new BukkitRunnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (removeFakePlayers() > 0) {
|
||||
Bukkit.getServer().broadcast(text("[服务器过于卡顿, 已删除所有假人]").style(Style.style(RED, ITALIC)));
|
||||
}
|
||||
}
|
||||
}.runTask(Main.getInstance());
|
||||
}
|
||||
}
|
||||
}, 60_000, 60_000);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建一个假人
|
||||
*
|
||||
@ -38,27 +67,42 @@ public class FakePlayerManager {
|
||||
@NotNull CommandSender creator,
|
||||
@NotNull Location at
|
||||
) {
|
||||
var existed = getFakePlayers(creator).size();
|
||||
if (!creator.isOp() && existed >= properties.getMaximum()) {
|
||||
var playerLimit = properties.getPlayerLimit();
|
||||
if (!creator.isOp() && playerLimit != Integer.MAX_VALUE && getFakePlayers(creator).size() >= playerLimit) {
|
||||
creator.sendMessage(text("你创建的假人数量已达到上限...", RED));
|
||||
return;
|
||||
}
|
||||
|
||||
var serverLimit = properties.getServerLimit();
|
||||
if (!creator.isOp() && serverLimit != Integer.MAX_VALUE && getFakePlayers().size() >= serverLimit) {
|
||||
creator.sendMessage(text("服务器假人数量已达到上限...", RED));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!creator.isOp() && properties.isDetectIp() && countByAddress(AddressUtils.getAddress(creator)) >= 1) {
|
||||
creator.sendMessage(text("你所在 IP 创建的假人数量已达到上限...", RED));
|
||||
return;
|
||||
}
|
||||
|
||||
var name = creator.getName();
|
||||
var suffix = "_" + (existed + 1);
|
||||
var suffix = "_" + count++;
|
||||
if (name.length() + suffix.length() > 16) {
|
||||
name = name.substring(0, (16 - suffix.length()));
|
||||
}
|
||||
name = name + suffix;
|
||||
|
||||
var player = new FakePlayer(
|
||||
var faker = new FakePlayer(
|
||||
creator.getName(),
|
||||
((CraftServer) Bukkit.getServer()).getServer(),
|
||||
((CraftWorld) at.getWorld()).getHandle(),
|
||||
UUID.randomUUID(),
|
||||
name,
|
||||
at
|
||||
).spawn();
|
||||
player.setMetadata(META_KEY_CREATOR, new FixedMetadataValue(Main.getInstance(), creator.getName()));
|
||||
).spawn(properties.getTickPeriod());
|
||||
|
||||
faker.setMetadata(META_KEY_CREATOR, new FixedMetadataValue(Main.getInstance(), creator.getName()));
|
||||
faker.setMetadata(META_KEY_CREATOR_IP, new FixedMetadataValue(Main.getInstance(), AddressUtils.getAddress(creator)));
|
||||
faker.playerListName(text(creator.getName() + "的假人").style(Style.style(GRAY, ITALIC)));
|
||||
}
|
||||
|
||||
public @Nullable Player getFakePlayer(@NotNull CommandSender creator, @NotNull String name) {
|
||||
@ -100,7 +144,7 @@ public class FakePlayerManager {
|
||||
* @param creator 创建者
|
||||
* @return 移除假人的数量
|
||||
*/
|
||||
public int removeFakePlayers(@NotNull Player creator) {
|
||||
public int removeFakePlayers(@NotNull CommandSender creator) {
|
||||
var fakes = getFakePlayers(creator);
|
||||
for (var f : fakes) {
|
||||
f.kick();
|
||||
@ -108,6 +152,18 @@ public class FakePlayerManager {
|
||||
return fakes.size();
|
||||
}
|
||||
|
||||
public boolean removeFakePlayer(@NotNull String name) {
|
||||
var faker = getFakePlayer(name);
|
||||
if (faker == null) {
|
||||
return false;
|
||||
}
|
||||
if (!isFakePlayer(faker)) {
|
||||
return false;
|
||||
}
|
||||
faker.kick();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取一个假人的创建者, 如果这个玩家不是假人, 则为 {@code null}
|
||||
*
|
||||
@ -176,5 +232,13 @@ public class FakePlayerManager {
|
||||
return !player.getMetadata(META_KEY_CREATOR).isEmpty();
|
||||
}
|
||||
|
||||
public long countByAddress(@NotNull String address) {
|
||||
return Bukkit.getServer()
|
||||
.getOnlinePlayers()
|
||||
.stream()
|
||||
.filter(p -> p.getMetadata(META_KEY_CREATOR_IP).stream().anyMatch(meta -> meta.asString().equals(address)))
|
||||
.count();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,77 @@
|
||||
package io.github.hello09x.fakeplayer.properties;
|
||||
|
||||
|
||||
import io.github.hello09x.fakeplayer.Main;
|
||||
import io.github.tanyaofei.plugin.toolkit.properties.AbstractProperties;
|
||||
import lombok.Getter;
|
||||
import lombok.ToString;
|
||||
import org.bukkit.configuration.file.FileConfiguration;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
public class FakeplayerProperties extends AbstractProperties {
|
||||
|
||||
public final static FakeplayerProperties instance = new FakeplayerProperties(
|
||||
Main.getInstance(),
|
||||
"2"
|
||||
);
|
||||
|
||||
/**
|
||||
* 每位玩家最多多少个假人
|
||||
*/
|
||||
private int playerLimit;
|
||||
|
||||
/**
|
||||
* 服务器最多多少个假人
|
||||
*/
|
||||
private int serverLimit;
|
||||
|
||||
/**
|
||||
* 动作更新间隔
|
||||
*/
|
||||
private long tickPeriod;
|
||||
|
||||
/**
|
||||
* 创建者玩家下线时是否跟随下线
|
||||
*/
|
||||
private boolean followQuiting;
|
||||
|
||||
/**
|
||||
* 是否探测 IP
|
||||
*/
|
||||
private boolean detectIp;
|
||||
|
||||
/**
|
||||
* 服务器 tps 低于这个值移除所有假人
|
||||
*/
|
||||
private int kaleTps;
|
||||
|
||||
/**
|
||||
* 胡言乱语
|
||||
*/
|
||||
private List<String> nonsense;
|
||||
|
||||
public FakeplayerProperties(@NotNull JavaPlugin plugin, @NotNull String version) {
|
||||
super(plugin, version);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void reload(@NotNull FileConfiguration file) {
|
||||
this.playerLimit = maxIfZero(file.getInt("player-limit", 1));
|
||||
this.serverLimit = maxIfZero(file.getInt("server-limit", 1000));
|
||||
this.tickPeriod = file.getLong("tick-period", 1);
|
||||
this.followQuiting = file.getBoolean("follow-quiting", true);
|
||||
this.detectIp = file.getBoolean("detect-ip", false);
|
||||
this.kaleTps = file.getInt("kale-tps", 10);
|
||||
this.nonsense = file.getStringList("nonsense");
|
||||
}
|
||||
|
||||
private static int maxIfZero(int value) {
|
||||
return value == 0 ? Integer.MAX_VALUE : value;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
package io.github.hello09x.fakeplayer.util;
|
||||
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
import java.util.Optional;
|
||||
|
||||
public class AddressUtils {
|
||||
|
||||
public static String getAddress(@NotNull CommandSender sender) {
|
||||
if (sender instanceof Player p) {
|
||||
return Optional.ofNullable(p.getAddress()).map(InetSocketAddress::getHostString).orElse("<unknown>");
|
||||
} else {
|
||||
return "0.0.0.0";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -23,4 +23,17 @@ public class ReflectionUtils {
|
||||
return null;
|
||||
}
|
||||
|
||||
public static @Nullable Field getFirstFieldByAssignFromType(Class<?> clazz, Class<?> fieldType, boolean includeStatic) {
|
||||
for (var field : clazz.getDeclaredFields()) {
|
||||
if (includeStatic ^ Modifier.isStatic(field.getModifiers())) {
|
||||
continue;
|
||||
}
|
||||
if (fieldType.isAssignableFrom(field.getType())) {
|
||||
field.setAccessible(true);
|
||||
return field;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,32 +0,0 @@
|
||||
package properties;
|
||||
|
||||
|
||||
import io.github.hello09x.fakeplayer.Main;
|
||||
import io.github.tanyaofei.plugin.toolkit.properties.AbstractProperties;
|
||||
import lombok.Getter;
|
||||
import lombok.ToString;
|
||||
import org.bukkit.configuration.file.FileConfiguration;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
public class FakeplayerProperties extends AbstractProperties {
|
||||
|
||||
public final static FakeplayerProperties instance = new FakeplayerProperties(
|
||||
Main.getInstance(),
|
||||
"1"
|
||||
);
|
||||
|
||||
private int maximum;
|
||||
|
||||
public FakeplayerProperties(@NotNull JavaPlugin plugin, @NotNull String version) {
|
||||
super(plugin, version);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void reload(@NotNull FileConfiguration file) {
|
||||
this.maximum = file.getInt("maximum", 1);
|
||||
}
|
||||
|
||||
}
|
@ -1,5 +1,41 @@
|
||||
version: 1
|
||||
version: 2
|
||||
|
||||
# 服务器最多存在多少个假人
|
||||
# 默认: 1000
|
||||
server-limit: 1000
|
||||
|
||||
# 每个玩家最多创建多少个假人
|
||||
# 默认: 1
|
||||
maximum: 1
|
||||
player-limit: 1
|
||||
|
||||
# 每多少个 ticks 触发一次假人动作更新
|
||||
# 这个值调的越大, 假人的动作更新越慢
|
||||
# 单位 tick
|
||||
tick-period: 1
|
||||
|
||||
# 假人创建者玩家下线时是否自动下线
|
||||
follow-quiting: true
|
||||
|
||||
# 是否检测 IP
|
||||
# 如果启用, 则一个 IP 只能创建 `maximum` 个假人
|
||||
# 能够避免玩家开小号疯狂创建假人
|
||||
detect-ip: false
|
||||
|
||||
# 服务器最近 5 分钟平均 TPS 低于这个值清除所有假人
|
||||
# 每 60 秒检测一次
|
||||
# 默认: 0, 即不开启, 因为移除假人可能导致玩家炸机器, 按需开启吧
|
||||
kale-tps: 0
|
||||
|
||||
# 假人不定时胡言乱语
|
||||
# 每 6000 ticks(大约 5 分钟) 有 1/3 的几率胡言乱语
|
||||
nonsense:
|
||||
- 当个假人好累啊
|
||||
- 能不能给个凳子坐坐?
|
||||
- 靠,这里蚊子有点多
|
||||
- 腐竹,救我!我动不了了
|
||||
- 能不能让我玩会生存?
|
||||
- 你好,我是学生,v我50可以吗
|
||||
- 服务器将在 5 秒钟清除垃圾,请小心手上的物品 (
|
||||
- 你们卡吗,我怎么动不了了
|
||||
- 能不能在我这里建个挂机池?
|
||||
- 汤姆哥有点小帅
|
||||
|
@ -2,6 +2,7 @@ name: fakeplayer
|
||||
version: '${project.version}'
|
||||
main: io.github.hello09x.fakeplayer.Main
|
||||
api-version: '1.20'
|
||||
author: hello09x
|
||||
|
||||
commands:
|
||||
fakeplayer:
|
||||
@ -11,9 +12,19 @@ commands:
|
||||
- fp
|
||||
|
||||
permissions:
|
||||
fakeplayer:
|
||||
description: '假人基础权限'
|
||||
default: op
|
||||
fakeplayer.all:
|
||||
description: '假人所有基础权限'
|
||||
children:
|
||||
- fakeplayer.spawn
|
||||
- fakeplayer.tp
|
||||
|
||||
fakeplayer.spawn:
|
||||
description: '假人创建权限'
|
||||
default: true
|
||||
|
||||
fakeplayer.tp:
|
||||
description: '假人传送权限'
|
||||
default: true
|
||||
|
||||
fakeplayer.admin:
|
||||
description: '假人管理员权限'
|
||||
|
Loading…
Reference in New Issue
Block a user