更多功能

This commit is contained in:
tanyaofei 2023-07-18 15:59:32 +08:00
parent 0651bb0171
commit 4f5c731d02
22 changed files with 548 additions and 178 deletions

2
.gitignore vendored
View File

@ -124,4 +124,6 @@ fabric.properties
server/
server2/
lib/

69
README.md Normal file
View 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 秒钟清除垃圾,请小心手上的物品 (
- 你们卡吗,我怎么动不了了
- 能不能在我这里建个挂机池?
- 汤姆哥有点小帅
```

View File

@ -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>

View File

@ -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());
}
}

View File

@ -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);
}

View File

@ -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;

View File

@ -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();
}
}

View File

@ -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;

View File

@ -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;
}

View File

@ -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);
sender.sendMessage(text(String.format("已移除 %d 个假人", count), GRAY));
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;
}
if (manager.removeFakePlayer(name)) {
removed.add(name);
}
}
if (removed.isEmpty()) {
sender.sendMessage(text("找不到对应名称的假人...", RED));
return true;
}
var fake = creator.isOp()
? manager.getFakePlayer(name)
: manager.getFakePlayer(creator, name);
if (fake == null) {
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();
}
}

View File

@ -15,7 +15,7 @@ public class TpHereCommand extends AbstractTeleportCommand {
public final static TpHereCommand instance = new TpHereCommand(
"传送假人到身边",
"/fp tphere <名称>",
"fakeplayer"
"fakeplayer.tp"
);
public TpHereCommand(

View File

@ -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;
}
}

View File

@ -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);
}

View File

@ -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

View File

@ -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;
}
}

View File

@ -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();
}
}

View File

@ -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;
}
}

View File

@ -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";
}
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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 秒钟清除垃圾,请小心手上的物品 (
- 你们卡吗,我怎么动不了了
- 能不能在我这里建个挂机池?
- 汤姆哥有点小帅

View File

@ -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: '假人管理员权限'