mirror of
https://github.com/LeavesMC/Leaves.git
synced 2025-12-19 14:59:32 +00:00
1380 lines
57 KiB
Diff
1380 lines
57 KiB
Diff
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
|
From: violetc <58360096+s-yh-china@users.noreply.github.com>
|
|
Date: Thu, 3 Aug 2023 20:36:38 +0800
|
|
Subject: [PATCH] Replay Mod API
|
|
|
|
This patch is Powered by ReplayMod(https://github.com/ReplayMod)
|
|
|
|
diff --git a/src/main/java/net/minecraft/commands/arguments/EntityArgument.java b/src/main/java/net/minecraft/commands/arguments/EntityArgument.java
|
|
index 150daf6bf4b27a6ff984d872a28002f19beef51c..a9bbb0894a122d03cffc74b574936064981aedb9 100644
|
|
--- a/src/main/java/net/minecraft/commands/arguments/EntityArgument.java
|
|
+++ b/src/main/java/net/minecraft/commands/arguments/EntityArgument.java
|
|
@@ -147,6 +147,7 @@ public class EntityArgument implements ArgumentType<EntitySelector> {
|
|
if (icompletionprovider instanceof CommandSourceStack commandSourceStack && commandSourceStack.getEntity() instanceof ServerPlayer sourcePlayer) {
|
|
collection = new java.util.ArrayList<>();
|
|
for (final ServerPlayer player : commandSourceStack.getServer().getPlayerList().getPlayers()) {
|
|
+ if (player instanceof top.leavesmc.leaves.replay.ServerPhotographer) continue; // Leaves - skip photographer
|
|
if (sourcePlayer.getBukkitEntity().canSee(player.getBukkitEntity())) {
|
|
collection.add(player.getGameProfile().getName());
|
|
}
|
|
diff --git a/src/main/java/net/minecraft/commands/arguments/selector/EntitySelector.java b/src/main/java/net/minecraft/commands/arguments/selector/EntitySelector.java
|
|
index f25b9330e068c7d9e12cb57a7761cfef9ebaf7bc..7891fa8e60ed74dfcc85c33d5b9f9d516fc40b99 100644
|
|
--- a/src/main/java/net/minecraft/commands/arguments/selector/EntitySelector.java
|
|
+++ b/src/main/java/net/minecraft/commands/arguments/selector/EntitySelector.java
|
|
@@ -122,6 +122,7 @@ public class EntitySelector {
|
|
return this.findPlayers(source);
|
|
} else if (this.playerName != null) {
|
|
ServerPlayer entityplayer = source.getServer().getPlayerList().getPlayerByName(this.playerName);
|
|
+ entityplayer = entityplayer instanceof top.leavesmc.leaves.replay.ServerPhotographer ? null : entityplayer; // Leaves - skip photographer
|
|
|
|
return (List) (entityplayer == null ? Collections.emptyList() : Lists.newArrayList(new ServerPlayer[]{entityplayer}));
|
|
} else if (this.entityUUID != null) {
|
|
@@ -137,6 +138,7 @@ public class EntitySelector {
|
|
ServerLevel worldserver = (ServerLevel) iterator.next();
|
|
|
|
entity = worldserver.getEntity(this.entityUUID);
|
|
+ entity = entity instanceof top.leavesmc.leaves.replay.ServerPhotographer ? null : entity; // Leaves - skip photographer
|
|
} while (entity == null);
|
|
|
|
return Lists.newArrayList(new Entity[]{entity});
|
|
@@ -145,7 +147,7 @@ public class EntitySelector {
|
|
Predicate<Entity> predicate = this.getPredicate(vec3d);
|
|
|
|
if (this.currentEntity) {
|
|
- return (List) (source.getEntity() != null && predicate.test(source.getEntity()) ? Lists.newArrayList(new Entity[]{source.getEntity()}) : Collections.emptyList());
|
|
+ return (List) (source.getEntity() != null && !(source.getEntity() instanceof top.leavesmc.leaves.replay.ServerPhotographer) && predicate.test(source.getEntity()) ? Lists.newArrayList(new Entity[]{source.getEntity()}) : Collections.emptyList()); // Leaves - skip photographer
|
|
} else {
|
|
List<Entity> list = Lists.newArrayList();
|
|
|
|
@@ -160,6 +162,7 @@ public class EntitySelector {
|
|
this.addEntities(list, worldserver1, vec3d, predicate);
|
|
}
|
|
}
|
|
+ list.removeIf(entity -> entity instanceof top.leavesmc.leaves.replay.ServerPhotographer); // Leaves - skip photographer
|
|
|
|
return this.sortAndLimit(vec3d, list);
|
|
}
|
|
@@ -200,9 +203,11 @@ public class EntitySelector {
|
|
|
|
if (this.playerName != null) {
|
|
entityplayer = source.getServer().getPlayerList().getPlayerByName(this.playerName);
|
|
+ entityplayer = entityplayer instanceof top.leavesmc.leaves.replay.ServerPhotographer ? null : entityplayer; // Leaves - skip photographer
|
|
return (List) (entityplayer == null ? Collections.emptyList() : Lists.newArrayList(new ServerPlayer[]{entityplayer}));
|
|
} else if (this.entityUUID != null) {
|
|
entityplayer = source.getServer().getPlayerList().getPlayer(this.entityUUID);
|
|
+ entityplayer = entityplayer instanceof top.leavesmc.leaves.replay.ServerPhotographer ? null : entityplayer; // Leaves - skip photographer
|
|
return (List) (entityplayer == null ? Collections.emptyList() : Lists.newArrayList(new ServerPlayer[]{entityplayer}));
|
|
} else {
|
|
Vec3 vec3d = (Vec3) this.position.apply(source.getPosition());
|
|
@@ -212,7 +217,7 @@ public class EntitySelector {
|
|
if (source.getEntity() instanceof ServerPlayer) {
|
|
ServerPlayer entityplayer1 = (ServerPlayer) source.getEntity();
|
|
|
|
- if (predicate.test(entityplayer1)) {
|
|
+ if (predicate.test(entityplayer1) && !(entityplayer1 instanceof top.leavesmc.leaves.replay.ServerPhotographer)) { // Leaves - skip photographer
|
|
return Lists.newArrayList(new ServerPlayer[]{entityplayer1});
|
|
}
|
|
}
|
|
@@ -223,7 +228,7 @@ public class EntitySelector {
|
|
Object object;
|
|
|
|
if (this.isWorldLimited()) {
|
|
- object = source.getLevel().getPlayers(predicate, i);
|
|
+ object = source.getLevel().getPlayers((entityplayer3 -> !(entityplayer3 instanceof top.leavesmc.leaves.replay.ServerPhotographer) && predicate.test(entityplayer3)), i); // Leaves - skip photographer
|
|
} else {
|
|
object = Lists.newArrayList();
|
|
Iterator iterator = source.getServer().getPlayerList().getPlayers().iterator();
|
|
@@ -231,7 +236,7 @@ public class EntitySelector {
|
|
while (iterator.hasNext()) {
|
|
ServerPlayer entityplayer2 = (ServerPlayer) iterator.next();
|
|
|
|
- if (predicate.test(entityplayer2)) {
|
|
+ if (predicate.test(entityplayer2) && !(entityplayer2 instanceof top.leavesmc.leaves.replay.ServerPhotographer)) { // Leaves - skip photographer
|
|
((List) object).add(entityplayer2);
|
|
if (((List) object).size() >= i) {
|
|
return (List) object;
|
|
diff --git a/src/main/java/net/minecraft/server/PlayerAdvancements.java b/src/main/java/net/minecraft/server/PlayerAdvancements.java
|
|
index fd9e85dab7c511873824cac56a270ff435792292..257e51570ed08660613895f5a1eccbee785707f3 100644
|
|
--- a/src/main/java/net/minecraft/server/PlayerAdvancements.java
|
|
+++ b/src/main/java/net/minecraft/server/PlayerAdvancements.java
|
|
@@ -46,6 +46,7 @@ import net.minecraft.world.level.GameRules;
|
|
import net.minecraft.world.level.GameType;
|
|
import org.slf4j.Logger;
|
|
import top.leavesmc.leaves.bot.ServerBot;
|
|
+import top.leavesmc.leaves.replay.ServerPhotographer;
|
|
|
|
public class PlayerAdvancements {
|
|
|
|
@@ -227,7 +228,7 @@ public class PlayerAdvancements {
|
|
|
|
public boolean award(Advancement advancement, String criterionName) {
|
|
// Leaves start - bot can't get advancement
|
|
- if (player instanceof ServerBot) {
|
|
+ if (player instanceof ServerBot || player instanceof ServerPhotographer) { // Leaves - and photographer
|
|
return false;
|
|
}
|
|
// Leaves end - bot can't get advancement
|
|
diff --git a/src/main/java/net/minecraft/server/players/PlayerList.java b/src/main/java/net/minecraft/server/players/PlayerList.java
|
|
index 63149cfdfbc129fa02070e06d44a9417dbde7c5f..6f609ec7a9814a44afc82aa504010519d614b796 100644
|
|
--- a/src/main/java/net/minecraft/server/players/PlayerList.java
|
|
+++ b/src/main/java/net/minecraft/server/players/PlayerList.java
|
|
@@ -133,6 +133,7 @@ import org.bukkit.event.player.PlayerRespawnEvent.RespawnReason;
|
|
|
|
import top.leavesmc.leaves.util.ArrayConstants;
|
|
import top.leavesmc.leaves.util.ReturnPortalManager; // Leaves - return portal fix
|
|
+import top.leavesmc.leaves.replay.ServerPhotographer;
|
|
|
|
public abstract class PlayerList {
|
|
|
|
@@ -165,6 +166,7 @@ public abstract class PlayerList {
|
|
private boolean allowCheatsForAllPlayers;
|
|
private static final boolean ALLOW_LOGOUTIVATOR = false;
|
|
private int sendAllPlayerInfoIn;
|
|
+ public final List<ServerPlayer> realPlayers = new java.util.concurrent.CopyOnWriteArrayList(); // Leaves - replay api
|
|
|
|
// CraftBukkit start
|
|
private CraftServer cserver;
|
|
@@ -192,6 +194,110 @@ public abstract class PlayerList {
|
|
}
|
|
abstract public void loadAndSaveFiles(); // Paper - moved from DedicatedPlayerList constructor
|
|
|
|
+ // Leaves start - replay api
|
|
+ public void placeNewPhotographer(Connection connection, ServerPhotographer player, ServerLevel worldserver, Location location) {
|
|
+ player.isRealPlayer = true;
|
|
+ player.loginTime = System.currentTimeMillis();
|
|
+
|
|
+ player.setServerLevel(worldserver);
|
|
+
|
|
+ player.spawnIn(worldserver);
|
|
+ player.gameMode.setLevel((ServerLevel) player.level());
|
|
+ player.setPosRaw(location.getX(), location.getY(), location.getZ());
|
|
+ player.setRot(location.getYaw(), location.getPitch());
|
|
+
|
|
+ LevelData worlddata = worldserver.getLevelData();
|
|
+
|
|
+ ServerGamePacketListenerImpl playerconnection = new ServerGamePacketListenerImpl(this.server, connection, player);
|
|
+ GameRules gamerules = worldserver.getGameRules();
|
|
+ boolean flag = gamerules.getBoolean(GameRules.RULE_DO_IMMEDIATE_RESPAWN);
|
|
+ boolean flag1 = gamerules.getBoolean(GameRules.RULE_REDUCEDDEBUGINFO);
|
|
+
|
|
+ playerconnection.send(new ClientboundLoginPacket(player.getId(), worlddata.isHardcore(), player.gameMode.getGameModeForPlayer(), player.gameMode.getPreviousGameModeForPlayer(), this.server.levelKeys(), this.synchronizedRegistries, worldserver.dimensionTypeId(), worldserver.dimension(), BiomeManager.obfuscateSeed(worldserver.getSeed()), this.getMaxPlayers(), worldserver.getWorld().getSendViewDistance(), worldserver.getWorld().getSimulationDistance(), flag1, !flag, worldserver.isDebug(), worldserver.isFlat(), player.getLastDeathLocation(), player.getPortalCooldown()));
|
|
+ player.getBukkitEntity().sendSupportedChannels();
|
|
+ playerconnection.send(new ClientboundUpdateEnabledFeaturesPacket(FeatureFlags.REGISTRY.toNames(worldserver.enabledFeatures())));
|
|
+ playerconnection.send(new ClientboundCustomPayloadPacket(ClientboundCustomPayloadPacket.BRAND, (new FriendlyByteBuf(Unpooled.buffer())).writeUtf(this.getServer().getServerModName())));
|
|
+ playerconnection.send(new ClientboundChangeDifficultyPacket(worlddata.getDifficulty(), worlddata.isDifficultyLocked()));
|
|
+ playerconnection.send(new ClientboundPlayerAbilitiesPacket(player.getAbilities()));
|
|
+ playerconnection.send(new ClientboundSetCarriedItemPacket(player.getInventory().selected));
|
|
+ playerconnection.send(new ClientboundUpdateRecipesPacket(this.server.getRecipeManager().getRecipes()));
|
|
+ playerconnection.send(new ClientboundUpdateTagsPacket(TagNetworkSerialization.serializeTagsToNetwork(this.registries)));
|
|
+ this.sendPlayerPermissionLevel(player);
|
|
+ player.getStats().markAllDirty();
|
|
+ player.getRecipeBook().sendInitialRecipeBook(player);
|
|
+ this.updateEntireScoreboard(worldserver.getScoreboard(), player);
|
|
+ this.server.invalidateStatus();
|
|
+
|
|
+ playerconnection.teleport(player.getX(), player.getY(), player.getZ(), player.getYRot(), player.getXRot());
|
|
+ ServerStatus serverping = this.server.getStatus();
|
|
+
|
|
+ if (serverping != null) {
|
|
+ player.sendServerStatus(serverping);
|
|
+ }
|
|
+
|
|
+ this.players.add(player);
|
|
+ this.playersByName.put(player.getScoreboardName().toLowerCase(java.util.Locale.ROOT), player);
|
|
+ this.playersByUUID.put(player.getUUID(), player);
|
|
+
|
|
+ player.supressTrackerForLogin = true;
|
|
+ worldserver.addNewPlayer(player);
|
|
+ this.server.getCustomBossEvents().onPlayerConnect(player);
|
|
+ CraftPlayer bukkitPlayer = player.getBukkitEntity();
|
|
+
|
|
+ // Leaves start - bot support
|
|
+ if (top.leavesmc.leaves.LeavesConfig.fakeplayerSupport) {
|
|
+ ServerBot bot = ServerBot.getBot(player.getScoreboardName().toLowerCase(java.util.Locale.ROOT));
|
|
+ if (bot != null) {
|
|
+ bot.die(bot.damageSources().fellOutOfWorld()); // Leaves - remove bot with the same name
|
|
+ this.playersByName.put(player.getScoreboardName().toLowerCase(java.util.Locale.ROOT), player);
|
|
+ this.playersByUUID.put(player.getUUID(), player);
|
|
+ }
|
|
+ ServerBot.getBots().forEach(bot1 -> {
|
|
+ bot1.sendPlayerInfo(player);
|
|
+ bot1.sendFakeDataIfNeed(player, true);
|
|
+ }); // Leaves - render bot
|
|
+ }
|
|
+ // Leaves end - bot support
|
|
+
|
|
+ final List<ServerPlayer> onlinePlayers = Lists.newArrayListWithExpectedSize(this.players.size() - 1);
|
|
+ for (int i = 0; i < this.players.size(); ++i) {
|
|
+ ServerPlayer entityplayer1 = this.players.get(i);
|
|
+
|
|
+ if (entityplayer1 == player || !bukkitPlayer.canSee(entityplayer1.getBukkitEntity())) {
|
|
+ continue;
|
|
+ }
|
|
+
|
|
+ onlinePlayers.add(entityplayer1);
|
|
+ }
|
|
+
|
|
+ if (!onlinePlayers.isEmpty()) {
|
|
+ player.connection.send(ClientboundPlayerInfoUpdatePacket.createPlayerInitializing(onlinePlayers));
|
|
+ }
|
|
+
|
|
+ player.sentListPacket = true;
|
|
+ player.supressTrackerForLogin = false;
|
|
+ ((ServerLevel)player.level()).getChunkSource().chunkMap.addEntity(player);
|
|
+
|
|
+ this.sendLevelInfo(player, worldserver);
|
|
+
|
|
+ if (player.level() == worldserver && !worldserver.players().contains(player)) {
|
|
+ worldserver.addNewPlayer(player);
|
|
+ this.server.getCustomBossEvents().onPlayerConnect(player);
|
|
+ }
|
|
+
|
|
+ worldserver = player.serverLevel();
|
|
+ this.server.getServerResourcePack().ifPresent((minecraftserver_serverresourcepackinfo) -> {
|
|
+ player.sendTexturePack(minecraftserver_serverresourcepackinfo.url(), minecraftserver_serverresourcepackinfo.hash(), minecraftserver_serverresourcepackinfo.isRequired(), minecraftserver_serverresourcepackinfo.prompt());
|
|
+ });
|
|
+
|
|
+ Iterator<MobEffectInstance> iterator = player.getActiveEffects().iterator();
|
|
+ while (iterator.hasNext()) {
|
|
+ MobEffectInstance mobeffect = iterator.next();
|
|
+ playerconnection.send(new ClientboundUpdateMobEffectPacket(player.getId(), mobeffect));
|
|
+ }
|
|
+ }
|
|
+ // Leaves end - replay api
|
|
+
|
|
public void placeNewPlayer(Connection connection, ServerPlayer player) {
|
|
player.isRealPlayer = true; // Paper
|
|
player.loginTime = System.currentTimeMillis(); // Paper
|
|
@@ -323,6 +429,7 @@ public abstract class PlayerList {
|
|
|
|
// entityplayer.connection.send(ClientboundPlayerInfoUpdatePacket.createPlayerInitializing(this.players)); // CraftBukkit - replaced with loop below
|
|
this.players.add(player);
|
|
+ this.realPlayers.add(player); // Leaves - replay api
|
|
this.playersByName.put(player.getScoreboardName().toLowerCase(java.util.Locale.ROOT), player); // Spigot
|
|
this.playersByUUID.put(player.getUUID(), player);
|
|
// this.broadcastAll(ClientboundPlayerInfoUpdatePacket.createPlayerInitializing(List.of(entityplayer))); // CraftBukkit - replaced with loop below
|
|
@@ -398,6 +505,12 @@ public abstract class PlayerList {
|
|
continue;
|
|
}
|
|
|
|
+ // Leaves start - skip photographer
|
|
+ if (entityplayer1 instanceof ServerPhotographer) {
|
|
+ continue;
|
|
+ }
|
|
+ // Leaves end - skip photographer
|
|
+
|
|
onlinePlayers.add(entityplayer1); // Paper - use single player info update packet
|
|
}
|
|
// Paper start - use single player info update packet
|
|
@@ -612,6 +725,43 @@ public abstract class PlayerList {
|
|
|
|
}
|
|
|
|
+ // Leaevs start - replay mod api
|
|
+ public void removePhotographer(ServerPhotographer entityplayer) {
|
|
+ ServerLevel worldserver = entityplayer.serverLevel();
|
|
+
|
|
+ entityplayer.awardStat(Stats.LEAVE_GAME);
|
|
+
|
|
+ if (entityplayer.containerMenu != entityplayer.inventoryMenu) {
|
|
+ entityplayer.closeContainer(org.bukkit.event.inventory.InventoryCloseEvent.Reason.DISCONNECT);
|
|
+ }
|
|
+
|
|
+ if (server.isSameThread()) entityplayer.doTick();
|
|
+
|
|
+ if (this.collideRuleTeamName != null) {
|
|
+ final Scoreboard scoreBoard = this.server.getLevel(Level.OVERWORLD).getScoreboard();
|
|
+ final PlayerTeam team = scoreBoard.getPlayersTeam(this.collideRuleTeamName);
|
|
+ if (entityplayer.getTeam() == team && team != null) {
|
|
+ scoreBoard.removePlayerFromTeam(entityplayer.getScoreboardName(), team);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ worldserver.removePlayerImmediately(entityplayer, Entity.RemovalReason.UNLOADED_WITH_PLAYER);
|
|
+ entityplayer.retireScheduler();
|
|
+ entityplayer.getAdvancements().stopListening();
|
|
+ this.players.remove(entityplayer);
|
|
+ this.playersByName.remove(entityplayer.getScoreboardName().toLowerCase(java.util.Locale.ROOT));
|
|
+ this.server.getCustomBossEvents().onPlayerDisconnect(entityplayer);
|
|
+ UUID uuid = entityplayer.getUUID();
|
|
+ ServerPlayer entityplayer1 = this.playersByUUID.get(uuid);
|
|
+
|
|
+ if (entityplayer1 == entityplayer) {
|
|
+ this.playersByUUID.remove(uuid);
|
|
+ }
|
|
+
|
|
+ this.cserver.getScoreboardManager().removePlayer(entityplayer.getBukkitEntity());
|
|
+ }
|
|
+ // Leaves stop - replay mod api
|
|
+
|
|
public net.kyori.adventure.text.Component remove(ServerPlayer entityplayer) { // CraftBukkit - return string // Paper - return Component
|
|
// Paper start
|
|
return this.remove(entityplayer, net.kyori.adventure.text.Component.translatable("multiplayer.player.left", net.kyori.adventure.text.format.NamedTextColor.YELLOW, io.papermc.paper.configuration.GlobalConfiguration.get().messages.useDisplayNameInQuitMessage ? entityplayer.getBukkitEntity().displayName() : PaperAdventure.asAdventure(entityplayer.getDisplayName())));
|
|
@@ -682,6 +832,7 @@ public abstract class PlayerList {
|
|
entityplayer.retireScheduler(); // Paper - Folia schedulers
|
|
entityplayer.getAdvancements().stopListening();
|
|
this.players.remove(entityplayer);
|
|
+ this.realPlayers.remove(entityplayer); // Leaves - replay api
|
|
this.playersByName.remove(entityplayer.getScoreboardName().toLowerCase(java.util.Locale.ROOT)); // Spigot
|
|
this.server.getCustomBossEvents().onPlayerDisconnect(entityplayer);
|
|
UUID uuid = entityplayer.getUUID();
|
|
diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java
|
|
index c04aa86e53633e4def6f6a9c4b5e667e82756672..1106be0a96e65618db47c52d9d13b0a0ee673a27 100644
|
|
--- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java
|
|
+++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java
|
|
@@ -261,6 +261,7 @@ import org.yaml.snakeyaml.error.MarkedYAMLException;
|
|
|
|
import net.md_5.bungee.api.chat.BaseComponent; // Spigot
|
|
import top.leavesmc.leaves.entity.CraftBotManager;
|
|
+import top.leavesmc.leaves.entity.CraftPhotographerManager;
|
|
|
|
import javax.annotation.Nullable; // Paper
|
|
import javax.annotation.Nonnull; // Paper
|
|
@@ -307,6 +308,7 @@ public final class CraftServer implements Server {
|
|
private final io.papermc.paper.logging.SysoutCatcher sysoutCatcher = new io.papermc.paper.logging.SysoutCatcher(); // Paper
|
|
private final CraftPotionBrewer potionBrewer = new CraftPotionBrewer(); // Paper
|
|
private final CraftBotManager botManager = new CraftBotManager();
|
|
+ private final CraftPhotographerManager photographerManager = new CraftPhotographerManager();
|
|
|
|
// Paper start - Folia region threading API
|
|
private final io.papermc.paper.threadedregions.scheduler.FallbackRegionScheduler regionizedScheduler = new io.papermc.paper.threadedregions.scheduler.FallbackRegionScheduler();
|
|
@@ -388,7 +390,7 @@ public final class CraftServer implements Server {
|
|
public CraftServer(DedicatedServer console, PlayerList playerList) {
|
|
this.console = console;
|
|
this.playerList = (DedicatedPlayerList) playerList;
|
|
- this.playerView = Collections.unmodifiableList(Lists.transform(playerList.players, new Function<ServerPlayer, CraftPlayer>() {
|
|
+ this.playerView = Collections.unmodifiableList(Lists.transform(playerList.realPlayers, new Function<ServerPlayer, CraftPlayer>() { // Leaves - replay api
|
|
@Override
|
|
public CraftPlayer apply(ServerPlayer player) {
|
|
return player.getBukkitEntity();
|
|
@@ -3174,4 +3176,11 @@ public final class CraftServer implements Server {
|
|
return botManager;
|
|
}
|
|
// Leaves end - Bot API
|
|
+
|
|
+ // Leaves start - replay mod api
|
|
+ @Override
|
|
+ public CraftPhotographerManager getPhotographerManager() {
|
|
+ return photographerManager;
|
|
+ }
|
|
+ // Leaves end - replay mod api
|
|
}
|
|
diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java
|
|
index 8388c2a56cbe531cf5f60a18866c85fad1e23c54..bdd36d58c55a66a9538540fa5c88a2943f1c7b05 100644
|
|
--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java
|
|
+++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java
|
|
@@ -198,6 +198,8 @@ import top.leavesmc.leaves.bot.ServerBot;
|
|
import top.leavesmc.leaves.entity.CraftBot;
|
|
|
|
import net.md_5.bungee.api.chat.BaseComponent; // Spigot
|
|
+import top.leavesmc.leaves.entity.CraftPhotographer;
|
|
+import top.leavesmc.leaves.replay.ServerPhotographer;
|
|
|
|
public abstract class CraftEntity implements org.bukkit.entity.Entity {
|
|
private static PermissibleBase perm;
|
|
@@ -237,6 +239,7 @@ public abstract class CraftEntity implements org.bukkit.entity.Entity {
|
|
// Leaves start - add CraftBot
|
|
if (entity instanceof ServerPlayer) {
|
|
if (entity instanceof ServerBot) { return new CraftBot(server, (ServerBot) entity); }
|
|
+ if (entity instanceof ServerPhotographer) { return new CraftPhotographer(server, (ServerPhotographer) entity); }
|
|
else { return new CraftPlayer(server, (ServerPlayer) entity); }
|
|
}
|
|
// Leaves end - add CraftBot
|
|
diff --git a/src/main/java/top/leavesmc/leaves/entity/CraftPhotographer.java b/src/main/java/top/leavesmc/leaves/entity/CraftPhotographer.java
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..65fd6fd9e6af4e43268f1f1507a37e1bd95d41b8
|
|
--- /dev/null
|
|
+++ b/src/main/java/top/leavesmc/leaves/entity/CraftPhotographer.java
|
|
@@ -0,0 +1,68 @@
|
|
+package top.leavesmc.leaves.entity;
|
|
+
|
|
+import net.minecraft.server.level.ServerPlayer;
|
|
+import org.bukkit.craftbukkit.CraftServer;
|
|
+import org.bukkit.craftbukkit.entity.CraftPlayer;
|
|
+import org.bukkit.entity.Player;
|
|
+import org.jetbrains.annotations.NotNull;
|
|
+import org.jetbrains.annotations.Nullable;
|
|
+import top.leavesmc.leaves.replay.ServerPhotographer;
|
|
+
|
|
+import java.io.File;
|
|
+
|
|
+public class CraftPhotographer extends CraftPlayer implements Photographer {
|
|
+
|
|
+ public CraftPhotographer(CraftServer server, ServerPhotographer entity) {
|
|
+ super(server, entity);
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public void stopRecording() {
|
|
+ this.stopRecording(true);
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public void stopRecording(boolean async) {
|
|
+ this.getHandle().remove(async);
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public void pauseRecording() {
|
|
+ this.getHandle().pauseRecording();
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public void resumeRecording() {
|
|
+ this.getHandle().resumeRecording();
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public void setRecordFile(@NotNull File file) {
|
|
+ this.getHandle().setSaveFile(file);
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public void setFollowPlayer(@Nullable Player player) {
|
|
+ ServerPlayer serverPlayer = player != null ? ((CraftPlayer) player).getHandle() : null;
|
|
+ this.getHandle().setFollowPlayer(serverPlayer);
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public @NotNull String getId() {
|
|
+ return this.getHandle().createState.id;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public ServerPhotographer getHandle() {
|
|
+ return (ServerPhotographer) entity;
|
|
+ }
|
|
+
|
|
+ public void setHandle(final ServerPhotographer entity) {
|
|
+ super.setHandle(entity);
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public String toString() {
|
|
+ return "CraftPhotographer{" + "name=" + getName() + '}';
|
|
+ }
|
|
+}
|
|
diff --git a/src/main/java/top/leavesmc/leaves/entity/CraftPhotographerManager.java b/src/main/java/top/leavesmc/leaves/entity/CraftPhotographerManager.java
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..56b41cb3401ea597b48ec98098fe31f4dfb4933d
|
|
--- /dev/null
|
|
+++ b/src/main/java/top/leavesmc/leaves/entity/CraftPhotographerManager.java
|
|
@@ -0,0 +1,84 @@
|
|
+package top.leavesmc.leaves.entity;
|
|
+
|
|
+import com.google.common.collect.Lists;
|
|
+import org.bukkit.Location;
|
|
+import org.bukkit.util.Consumer;
|
|
+import org.jetbrains.annotations.NotNull;
|
|
+import org.jetbrains.annotations.Nullable;
|
|
+import top.leavesmc.leaves.bot.ServerBot;
|
|
+import top.leavesmc.leaves.replay.BukkitRecorderOption;
|
|
+import top.leavesmc.leaves.replay.RecorderOption;
|
|
+import top.leavesmc.leaves.replay.ServerPhotographer;
|
|
+
|
|
+import java.util.Collection;
|
|
+import java.util.Collections;
|
|
+import java.util.UUID;
|
|
+
|
|
+public class CraftPhotographerManager implements PhotographerManager {
|
|
+
|
|
+ private final Collection<Photographer> photographerViews = Collections.unmodifiableList(Lists.transform(ServerPhotographer.getPhotographers(), ServerPhotographer::getBukkitPlayer));
|
|
+
|
|
+ @Override
|
|
+ public @Nullable Photographer getPhotographer(@NotNull UUID uuid) {
|
|
+ ServerPhotographer photographer = ServerPhotographer.getPhotographer(uuid);
|
|
+ if (photographer != null) {
|
|
+ return photographer.getBukkitPlayer();
|
|
+ }
|
|
+ return null;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public @Nullable Photographer getPhotographer(@NotNull String id) {
|
|
+ ServerPhotographer photographer = ServerPhotographer.getPhotographer(id);
|
|
+ if (photographer != null) {
|
|
+ return photographer.getBukkitPlayer();
|
|
+ }
|
|
+ return null;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public @Nullable Photographer createPhotographer(@NotNull String id, @NotNull Location location) {
|
|
+ ServerPhotographer photographer = new ServerPhotographer.PhotographerCreateState(location, id, RecorderOption.createDefaultOption()).createSync();
|
|
+ if (photographer != null) {
|
|
+ return photographer.getBukkitPlayer();
|
|
+ }
|
|
+ return null;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public @Nullable Photographer createPhotographer(@NotNull String id, @NotNull Location location, @NotNull BukkitRecorderOption recorderOption) {
|
|
+ ServerPhotographer photographer = new ServerPhotographer.PhotographerCreateState(location, id, RecorderOption.createFromBukkit(recorderOption)).createSync();
|
|
+ if (photographer != null) {
|
|
+ return photographer.getBukkitPlayer();
|
|
+ }
|
|
+ return null;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public void removePhotographer(@NotNull String id) {
|
|
+ ServerPhotographer photographer = ServerPhotographer.getPhotographer(id);
|
|
+ if (photographer != null) {
|
|
+ photographer.remove(true);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public void removePhotographer(@NotNull UUID uuid) {
|
|
+ ServerPhotographer photographer = ServerPhotographer.getPhotographer(uuid);
|
|
+ if (photographer != null) {
|
|
+ photographer.remove(true);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public void removeAllPhotographers() {
|
|
+ for (ServerPhotographer photographer : ServerPhotographer.getPhotographers()) {
|
|
+ photographer.remove(true);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public Collection<Photographer> getPhotographers() {
|
|
+ return photographerViews;
|
|
+ }
|
|
+}
|
|
diff --git a/src/main/java/top/leavesmc/leaves/replay/DigestOutputStream.java b/src/main/java/top/leavesmc/leaves/replay/DigestOutputStream.java
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..92a6478b337c4371e61b7e7878b242619398ecdd
|
|
--- /dev/null
|
|
+++ b/src/main/java/top/leavesmc/leaves/replay/DigestOutputStream.java
|
|
@@ -0,0 +1,46 @@
|
|
+package top.leavesmc.leaves.replay;
|
|
+
|
|
+import org.jetbrains.annotations.NotNull;
|
|
+
|
|
+import java.io.IOException;
|
|
+import java.io.OutputStream;
|
|
+import java.util.zip.Checksum;
|
|
+
|
|
+public class DigestOutputStream extends OutputStream {
|
|
+
|
|
+ private final Checksum sum;
|
|
+ private final OutputStream out;
|
|
+
|
|
+ public DigestOutputStream(OutputStream out, Checksum sum) {
|
|
+ this.out = out;
|
|
+ this.sum = sum;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public void close() throws IOException {
|
|
+ out.close();
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public void flush() throws IOException {
|
|
+ out.flush();
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public void write(int b) throws IOException {
|
|
+ sum.update(b);
|
|
+ out.write(b);
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public void write(byte @NotNull [] b) throws IOException {
|
|
+ sum.update(b);
|
|
+ out.write(b);
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public void write(byte @NotNull [] b, int off, int len) throws IOException {
|
|
+ sum.update(b, off, len);
|
|
+ out.write(b, off, len);
|
|
+ }
|
|
+}
|
|
diff --git a/src/main/java/top/leavesmc/leaves/replay/RecordMetaData.java b/src/main/java/top/leavesmc/leaves/replay/RecordMetaData.java
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..46a86cfce4aa859b8de7c126c22f64a999a4fe7a
|
|
--- /dev/null
|
|
+++ b/src/main/java/top/leavesmc/leaves/replay/RecordMetaData.java
|
|
@@ -0,0 +1,23 @@
|
|
+package top.leavesmc.leaves.replay;
|
|
+
|
|
+import java.util.HashSet;
|
|
+import java.util.Set;
|
|
+import java.util.UUID;
|
|
+
|
|
+public class RecordMetaData {
|
|
+
|
|
+ public static final int CURRENT_FILE_FORMAT_VERSION = 14;
|
|
+
|
|
+ public boolean singleplayer = false;
|
|
+ public String serverName = "Leaves";
|
|
+ public int duration = 0;
|
|
+ public long date;
|
|
+ public String mcversion;
|
|
+ public String fileFormat = "MCPR";
|
|
+ public int fileFormatVersion;
|
|
+ public int protocol;
|
|
+ public String generator;
|
|
+ public int selfId = -1;
|
|
+
|
|
+ public Set<UUID> players = new HashSet<>();
|
|
+}
|
|
diff --git a/src/main/java/top/leavesmc/leaves/replay/Recorder.java b/src/main/java/top/leavesmc/leaves/replay/Recorder.java
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..95f7b4cd7cdfbbc67a21ac07df8ab67463ac78f1
|
|
--- /dev/null
|
|
+++ b/src/main/java/top/leavesmc/leaves/replay/Recorder.java
|
|
@@ -0,0 +1,224 @@
|
|
+package top.leavesmc.leaves.replay;
|
|
+
|
|
+import net.minecraft.SharedConstants;
|
|
+import net.minecraft.network.Connection;
|
|
+import net.minecraft.network.ConnectionProtocol;
|
|
+import net.minecraft.network.PacketSendListener;
|
|
+import net.minecraft.network.protocol.Packet;
|
|
+import net.minecraft.network.protocol.PacketFlow;
|
|
+import net.minecraft.network.protocol.game.ClientboundAddEntityPacket;
|
|
+import net.minecraft.network.protocol.game.ClientboundAddPlayerPacket;
|
|
+import net.minecraft.network.protocol.game.ClientboundBundlePacket;
|
|
+import net.minecraft.network.protocol.game.ClientboundDisconnectPacket;
|
|
+import net.minecraft.network.protocol.game.ClientboundGameEventPacket;
|
|
+import net.minecraft.network.protocol.game.ClientboundPlayerChatPacket;
|
|
+import net.minecraft.network.protocol.game.ClientboundSetTimePacket;
|
|
+import net.minecraft.network.protocol.game.ClientboundSystemChatPacket;
|
|
+import net.minecraft.network.protocol.login.ClientboundGameProfilePacket;
|
|
+import org.jetbrains.annotations.NotNull;
|
|
+import org.jetbrains.annotations.Nullable;
|
|
+import top.leavesmc.leaves.LeavesLogger;
|
|
+
|
|
+import java.io.File;
|
|
+import java.io.IOException;
|
|
+import java.util.concurrent.CompletableFuture;
|
|
+import java.util.concurrent.CompletionException;
|
|
+import java.util.concurrent.ExecutorService;
|
|
+import java.util.concurrent.Executors;
|
|
+import java.util.concurrent.TimeUnit;
|
|
+
|
|
+public class Recorder extends Connection {
|
|
+
|
|
+ private static final LeavesLogger LOGGER = LeavesLogger.LOGGER;
|
|
+
|
|
+ private final ReplayFile replayFile;
|
|
+ private final ServerPhotographer photographer;
|
|
+ private final RecorderOption recorderOption;
|
|
+ private final RecordMetaData metaData;
|
|
+
|
|
+ private final ExecutorService saveService = Executors.newSingleThreadExecutor();
|
|
+
|
|
+ private boolean stopped = false;
|
|
+ private boolean paused = false;
|
|
+ private boolean resumeOnNextPacket = true;
|
|
+
|
|
+ private long startTime;
|
|
+ private long lastPacket;
|
|
+ private long timeShift = 0;
|
|
+
|
|
+ private boolean isSaved;
|
|
+ private boolean isSaving;
|
|
+
|
|
+ private ConnectionProtocol state = ConnectionProtocol.LOGIN;
|
|
+
|
|
+ public Recorder(ServerPhotographer photographer, RecorderOption recorderOption, File replayFile) throws IOException {
|
|
+ super(PacketFlow.CLIENTBOUND);
|
|
+
|
|
+ this.photographer = photographer;
|
|
+ this.recorderOption = recorderOption;
|
|
+ this.metaData = new RecordMetaData();
|
|
+ this.replayFile = new ReplayFile(replayFile);
|
|
+ }
|
|
+
|
|
+ public void start() {
|
|
+ startTime = System.currentTimeMillis();
|
|
+
|
|
+ metaData.singleplayer = false;
|
|
+ metaData.serverName = recorderOption.serverName;
|
|
+ metaData.generator = "leaves";
|
|
+ metaData.date = startTime;
|
|
+ metaData.mcversion = SharedConstants.getCurrentVersion().getName();
|
|
+
|
|
+ // TODO start event
|
|
+ savePacket(new ClientboundGameProfilePacket(photographer.getGameProfile()));
|
|
+ state = ConnectionProtocol.PLAY;
|
|
+
|
|
+ if (recorderOption.forceWeather != null) {
|
|
+ setWeather(recorderOption.forceWeather);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ public void stop() {
|
|
+ stopped = true;
|
|
+ }
|
|
+
|
|
+ public void pauseRecording() {
|
|
+ resumeOnNextPacket = false;
|
|
+ paused = true;
|
|
+ }
|
|
+
|
|
+ public void resumeRecording() {
|
|
+ resumeOnNextPacket = true;
|
|
+ }
|
|
+
|
|
+ public void setWeather(RecorderOption.RecordWeather weather) {
|
|
+ weather.getPackets().forEach(this::savePacket);
|
|
+ }
|
|
+
|
|
+ public long getRecordedTime() {
|
|
+ final long base = System.currentTimeMillis() - startTime;
|
|
+ return base - timeShift;
|
|
+ }
|
|
+
|
|
+ private synchronized long getCurrentTimeAndUpdate() {
|
|
+ long now = getRecordedTime();
|
|
+ if (paused) {
|
|
+ if (resumeOnNextPacket) {
|
|
+ paused = false;
|
|
+ }
|
|
+ timeShift += now - lastPacket;
|
|
+ return lastPacket;
|
|
+ }
|
|
+ return lastPacket = now;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public boolean isConnected() {
|
|
+ return true;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public void send(@NotNull Packet<?> packet, @Nullable PacketSendListener callbacks) {
|
|
+ if (!stopped) {
|
|
+ if (packet instanceof ClientboundBundlePacket packet1) {
|
|
+ packet1.subPackets().forEach(subPacket -> {
|
|
+ send(subPacket, null);
|
|
+ });
|
|
+ }
|
|
+
|
|
+ if (packet instanceof ClientboundAddPlayerPacket packet1) {
|
|
+ metaData.players.add(packet1.getPlayerId());
|
|
+ saveMetadata();
|
|
+ }
|
|
+
|
|
+ if (packet instanceof ClientboundDisconnectPacket) {
|
|
+ return;
|
|
+ }
|
|
+
|
|
+ if (recorderOption.forceDayTime != -1 && packet instanceof ClientboundSetTimePacket packet1) {
|
|
+ packet = new ClientboundSetTimePacket(packet1.getDayTime(), recorderOption.forceDayTime, false);
|
|
+ }
|
|
+
|
|
+ if (recorderOption.forceWeather != null && packet instanceof ClientboundGameEventPacket packet1) {
|
|
+ ClientboundGameEventPacket.Type type = packet1.getEvent();
|
|
+ if (type == ClientboundGameEventPacket.START_RAINING || type == ClientboundGameEventPacket.STOP_RAINING || type == ClientboundGameEventPacket.RAIN_LEVEL_CHANGE || type == ClientboundGameEventPacket.THUNDER_LEVEL_CHANGE) {
|
|
+ return;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ if (recorderOption.ignoreChat && (packet instanceof ClientboundSystemChatPacket || packet instanceof ClientboundPlayerChatPacket)) {
|
|
+ return;
|
|
+ }
|
|
+
|
|
+ savePacket(packet);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ private void saveMetadata() {
|
|
+ saveService.submit(() -> {
|
|
+ try {
|
|
+ replayFile.saveMetaData(metaData);
|
|
+ } catch (IOException e) {
|
|
+ e.printStackTrace();
|
|
+ }
|
|
+ });
|
|
+ }
|
|
+
|
|
+ private void savePacket(Packet<?> packet) {
|
|
+ try {
|
|
+ final long timestamp = getCurrentTimeAndUpdate();
|
|
+ final boolean login = state == ConnectionProtocol.LOGIN;
|
|
+ saveService.submit(() -> {
|
|
+ try {
|
|
+ replayFile.savePacket(timestamp, packet, login);
|
|
+ } catch (Exception e) {
|
|
+ LOGGER.severe("Error saving packet");
|
|
+ e.printStackTrace();
|
|
+ }
|
|
+ });
|
|
+ } catch (Exception e) {
|
|
+ LOGGER.severe("Error saving packet");
|
|
+ e.printStackTrace();
|
|
+ }
|
|
+ }
|
|
+
|
|
+ public boolean isSaved() {
|
|
+ return isSaved;
|
|
+ }
|
|
+
|
|
+ public CompletableFuture<Void> saveRecording(File dest) {
|
|
+ isSaved = true;
|
|
+ if (!isSaving) {
|
|
+ isSaving = true;
|
|
+ metaData.duration = (int) lastPacket;
|
|
+ return CompletableFuture.runAsync(() -> {
|
|
+ saveMetadata();
|
|
+ saveService.shutdown();
|
|
+ boolean interrupted = false;
|
|
+ try {
|
|
+ saveService.awaitTermination(10, TimeUnit.SECONDS);
|
|
+ } catch (InterruptedException e) {
|
|
+ interrupted = true;
|
|
+ }
|
|
+ try {
|
|
+ replayFile.closeAndSave(dest);
|
|
+ } catch (IOException e) {
|
|
+ e.printStackTrace();
|
|
+ throw new CompletionException(e);
|
|
+ } finally {
|
|
+ if (interrupted) {
|
|
+ Thread.currentThread().interrupt();
|
|
+ }
|
|
+ }
|
|
+ }, runnable -> {
|
|
+ final Thread thread = new Thread(runnable, "Recording file save thread");
|
|
+ thread.start();
|
|
+ });
|
|
+ } else {
|
|
+ LOGGER.warning("saveRecording() called twice");
|
|
+ return CompletableFuture.supplyAsync(() -> {
|
|
+ throw new IllegalStateException("saveRecording() called twice");
|
|
+ });
|
|
+ }
|
|
+ }
|
|
+}
|
|
diff --git a/src/main/java/top/leavesmc/leaves/replay/RecorderOption.java b/src/main/java/top/leavesmc/leaves/replay/RecorderOption.java
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..06e7166336d621e1a8edb4a2ad88e2cb8a52abb1
|
|
--- /dev/null
|
|
+++ b/src/main/java/top/leavesmc/leaves/replay/RecorderOption.java
|
|
@@ -0,0 +1,57 @@
|
|
+package top.leavesmc.leaves.replay;
|
|
+
|
|
+import net.minecraft.network.protocol.Packet;
|
|
+import net.minecraft.network.protocol.game.ClientboundGameEventPacket;
|
|
+import org.jetbrains.annotations.Contract;
|
|
+import org.jetbrains.annotations.NotNull;
|
|
+
|
|
+import java.util.List;
|
|
+
|
|
+public class RecorderOption {
|
|
+
|
|
+ public int recordDistance = -1;
|
|
+ public String serverName = "Leaves";
|
|
+ public RecordWeather forceWeather = null;
|
|
+ public int forceDayTime = -1;
|
|
+ public boolean ignoreChat = false;
|
|
+ public boolean ignoreItem = false;
|
|
+
|
|
+ @NotNull
|
|
+ @Contract(" -> new")
|
|
+ public static RecorderOption createDefaultOption() {
|
|
+ return new RecorderOption();
|
|
+ }
|
|
+
|
|
+ @NotNull
|
|
+ public static RecorderOption createFromBukkit(@NotNull BukkitRecorderOption bukkitRecorderOption) {
|
|
+ RecorderOption recorderOption = new RecorderOption();
|
|
+ // recorderOption.recordDistance = bukkitRecorderOption.recordDistance;
|
|
+ // recorderOption.ignoreItem = bukkitRecorderOption.ignoreItem;
|
|
+ recorderOption.serverName = bukkitRecorderOption.serverName;
|
|
+ recorderOption.ignoreChat = bukkitRecorderOption.ignoreChat;
|
|
+ recorderOption.forceDayTime = bukkitRecorderOption.forceDayTime;
|
|
+ recorderOption.forceWeather = switch (bukkitRecorderOption.forceWeather) {
|
|
+ case RAIN -> RecordWeather.RAIN;
|
|
+ case CLEAR -> RecordWeather.CLEAR;
|
|
+ case THUNDER -> RecordWeather.THUNDER;
|
|
+ case NULL -> null;
|
|
+ };
|
|
+ return recorderOption;
|
|
+ }
|
|
+
|
|
+ public enum RecordWeather {
|
|
+ CLEAR(new ClientboundGameEventPacket(ClientboundGameEventPacket.STOP_RAINING, 0), new ClientboundGameEventPacket(ClientboundGameEventPacket.RAIN_LEVEL_CHANGE, 0), new ClientboundGameEventPacket(ClientboundGameEventPacket.THUNDER_LEVEL_CHANGE, 0)),
|
|
+ RAIN(new ClientboundGameEventPacket(ClientboundGameEventPacket.START_RAINING, 0), new ClientboundGameEventPacket(ClientboundGameEventPacket.RAIN_LEVEL_CHANGE, 1), new ClientboundGameEventPacket(ClientboundGameEventPacket.THUNDER_LEVEL_CHANGE, 0)),
|
|
+ THUNDER(new ClientboundGameEventPacket(ClientboundGameEventPacket.START_RAINING, 0), new ClientboundGameEventPacket(ClientboundGameEventPacket.RAIN_LEVEL_CHANGE, 1), new ClientboundGameEventPacket(ClientboundGameEventPacket.THUNDER_LEVEL_CHANGE, 1));
|
|
+
|
|
+ private final List<Packet<?>> packets;
|
|
+
|
|
+ private RecordWeather(Packet<?>... packets) {
|
|
+ this.packets = List.of(packets);
|
|
+ }
|
|
+
|
|
+ public List<Packet<?>> getPackets() {
|
|
+ return packets;
|
|
+ }
|
|
+ }
|
|
+}
|
|
diff --git a/src/main/java/top/leavesmc/leaves/replay/ReplayFile.java b/src/main/java/top/leavesmc/leaves/replay/ReplayFile.java
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..5dbd28ab5fa45653dc98dc00c28b936531896258
|
|
--- /dev/null
|
|
+++ b/src/main/java/top/leavesmc/leaves/replay/ReplayFile.java
|
|
@@ -0,0 +1,164 @@
|
|
+package top.leavesmc.leaves.replay;
|
|
+
|
|
+import com.google.gson.Gson;
|
|
+import com.google.gson.GsonBuilder;
|
|
+import io.netty.buffer.ByteBuf;
|
|
+import io.netty.buffer.Unpooled;
|
|
+import net.minecraft.SharedConstants;
|
|
+import net.minecraft.network.ConnectionProtocol;
|
|
+import net.minecraft.network.FriendlyByteBuf;
|
|
+import net.minecraft.network.protocol.Packet;
|
|
+import net.minecraft.network.protocol.PacketFlow;
|
|
+import org.jetbrains.annotations.NotNull;
|
|
+import top.leavesmc.leaves.util.UUIDSerializer;
|
|
+
|
|
+import java.io.BufferedOutputStream;
|
|
+import java.io.DataOutputStream;
|
|
+import java.io.File;
|
|
+import java.io.FileInputStream;
|
|
+import java.io.FileOutputStream;
|
|
+import java.io.IOException;
|
|
+import java.io.InputStream;
|
|
+import java.io.OutputStream;
|
|
+import java.io.OutputStreamWriter;
|
|
+import java.io.Writer;
|
|
+import java.nio.charset.StandardCharsets;
|
|
+import java.nio.file.Files;
|
|
+import java.util.List;
|
|
+import java.util.UUID;
|
|
+import java.util.zip.CRC32;
|
|
+import java.util.zip.ZipEntry;
|
|
+import java.util.zip.ZipOutputStream;
|
|
+
|
|
+public class ReplayFile {
|
|
+
|
|
+ private static final String RECORDING_FILE = "recording.tmcpr";
|
|
+ private static final String RECORDING_FILE_CRC32 = "recording.tmcpr.crc32";
|
|
+ private static final String MARKER_FILE = "markers.json";
|
|
+ private static final String META_FILE = "metaData.json";
|
|
+
|
|
+ private static final Gson MARKER_GSON = new GsonBuilder().registerTypeAdapter(ReplayMarker.class, new ReplayMarker.Serializer()).create();
|
|
+ private static final Gson META_GSON = new GsonBuilder().registerTypeAdapter(UUID.class, new UUIDSerializer()).create();
|
|
+
|
|
+ private final File tmpDir;
|
|
+ private final DataOutputStream packetStream;
|
|
+ private final CRC32 crc32 = new CRC32();
|
|
+
|
|
+ private final File markerFile;
|
|
+ private final File metaFile;
|
|
+
|
|
+ public ReplayFile(@NotNull File name) throws IOException {
|
|
+ this.tmpDir = new File(name.getParentFile(), name.getName() + ".tmp");
|
|
+ if (tmpDir.exists()) {
|
|
+ if (!ReplayFile.deleteDir(tmpDir)) {
|
|
+ throw new IOException("Recording file " + name + " already exists!");
|
|
+ }
|
|
+ }
|
|
+
|
|
+ if (!tmpDir.mkdirs()) {
|
|
+ throw new IOException("Failed to create temp directory for recording " + tmpDir);
|
|
+ }
|
|
+
|
|
+ File packetFile = new File(tmpDir, RECORDING_FILE);
|
|
+ metaFile = new File(tmpDir, META_FILE);
|
|
+ markerFile = new File(tmpDir, MARKER_FILE);
|
|
+
|
|
+ packetStream = new DataOutputStream(new DigestOutputStream(new BufferedOutputStream(new FileOutputStream(packetFile)), crc32));
|
|
+ }
|
|
+
|
|
+ private byte @NotNull [] getPacketBytes(Packet<?> packet, boolean isLogin) {
|
|
+ ConnectionProtocol state = isLogin ? ConnectionProtocol.LOGIN : ConnectionProtocol.PLAY;
|
|
+ int packetID = state.getPacketId(PacketFlow.CLIENTBOUND, packet);
|
|
+ ByteBuf buf = Unpooled.buffer();
|
|
+ FriendlyByteBuf packetBuf = new FriendlyByteBuf(buf);
|
|
+ packetBuf.writeVarInt(packetID);
|
|
+ packet.write(packetBuf);
|
|
+
|
|
+ buf.readerIndex(0);
|
|
+ byte[] ret = new byte[buf.readableBytes()];
|
|
+ buf.readBytes(ret);
|
|
+ buf.release();
|
|
+ return ret;
|
|
+ }
|
|
+
|
|
+ public void saveMarkers(List<ReplayMarker> markers) throws IOException {
|
|
+ try (Writer writer = new OutputStreamWriter(new FileOutputStream(markerFile), StandardCharsets.UTF_8)) {
|
|
+ writer.write(MARKER_GSON.toJson(markers));
|
|
+ }
|
|
+ }
|
|
+
|
|
+ public void saveMetaData(@NotNull RecordMetaData data) throws IOException {
|
|
+ data.fileFormat = "MCPR";
|
|
+ data.fileFormatVersion = RecordMetaData.CURRENT_FILE_FORMAT_VERSION;
|
|
+ data.protocol = SharedConstants.getCurrentVersion().getProtocolVersion();
|
|
+
|
|
+ try (Writer writer = new OutputStreamWriter(new FileOutputStream(metaFile), StandardCharsets.UTF_8)) {
|
|
+ writer.write(META_GSON.toJson(data));
|
|
+ }
|
|
+ }
|
|
+
|
|
+ public void savePacket(long timestamp, Packet<?> packet, boolean isLoginPhase) throws Exception {
|
|
+ byte[] data = getPacketBytes(packet, isLoginPhase);
|
|
+ packetStream.writeInt((int) timestamp);
|
|
+ packetStream.writeInt(data.length);
|
|
+ packetStream.write(data);
|
|
+ }
|
|
+
|
|
+ public synchronized void closeAndSave(File file) throws IOException {
|
|
+ packetStream.close();
|
|
+
|
|
+ String[] files = tmpDir.list();
|
|
+ if (files == null) {
|
|
+ return;
|
|
+ }
|
|
+
|
|
+ try (ZipOutputStream os = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(file)))) {
|
|
+ for (String fileName : files) {
|
|
+ os.putNextEntry(new ZipEntry(fileName));
|
|
+ File f = new File(tmpDir, fileName);
|
|
+ copy(new FileInputStream(f), os);
|
|
+ }
|
|
+
|
|
+ os.putNextEntry(new ZipEntry(RECORDING_FILE_CRC32));
|
|
+ Writer writer = new OutputStreamWriter(os);
|
|
+ writer.write(Long.toString(crc32.getValue()));
|
|
+ writer.flush();
|
|
+ }
|
|
+
|
|
+ for (String fileName : files) {
|
|
+ File f = new File(tmpDir, fileName);
|
|
+ Files.delete(f.toPath());
|
|
+ }
|
|
+ Files.delete(tmpDir.toPath());
|
|
+ }
|
|
+
|
|
+ private void copy(@NotNull InputStream in, OutputStream out) throws IOException {
|
|
+ byte[] buffer = new byte[8192];
|
|
+ int len;
|
|
+ while ((len = in.read(buffer)) > -1) {
|
|
+ out.write(buffer, 0, len);
|
|
+ }
|
|
+ in.close();
|
|
+ }
|
|
+
|
|
+ private static boolean deleteDir(File dir) {
|
|
+ if (dir == null || !dir.exists()) {
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ File[] files = dir.listFiles();
|
|
+ if (files != null) {
|
|
+ for (File file : files) {
|
|
+ if (file.isDirectory()) {
|
|
+ deleteDir(file);
|
|
+ } else {
|
|
+ if (!file.delete()) {
|
|
+ return false;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ return dir.delete();
|
|
+ }
|
|
+}
|
|
diff --git a/src/main/java/top/leavesmc/leaves/replay/ReplayMarker.java b/src/main/java/top/leavesmc/leaves/replay/ReplayMarker.java
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..852f2098d93d4437fe79af06e454d8494b6decf1
|
|
--- /dev/null
|
|
+++ b/src/main/java/top/leavesmc/leaves/replay/ReplayMarker.java
|
|
@@ -0,0 +1,43 @@
|
|
+package top.leavesmc.leaves.replay;
|
|
+
|
|
+import com.google.gson.JsonElement;
|
|
+import com.google.gson.JsonObject;
|
|
+import com.google.gson.JsonPrimitive;
|
|
+import com.google.gson.JsonSerializationContext;
|
|
+import com.google.gson.JsonSerializer;
|
|
+
|
|
+import java.lang.reflect.Type;
|
|
+
|
|
+public class ReplayMarker {
|
|
+
|
|
+ public int time;
|
|
+ public String name;
|
|
+ public double x = 0;
|
|
+ public double y = 0;
|
|
+ public double z = 0;
|
|
+ public float phi = 0;
|
|
+ public float theta = 0;
|
|
+ public float varphi = 0;
|
|
+
|
|
+ public static class Serializer implements JsonSerializer<ReplayMarker> {
|
|
+ @Override
|
|
+ public JsonElement serialize(ReplayMarker src, Type typeOfSrc, JsonSerializationContext context) {
|
|
+ JsonObject ret = new JsonObject();
|
|
+ JsonObject value = new JsonObject();
|
|
+ JsonObject position = new JsonObject();
|
|
+ ret.add("realTimestamp", new JsonPrimitive(src.time));
|
|
+ ret.add("value", value);
|
|
+
|
|
+ value.add("name", new JsonPrimitive(src.name));
|
|
+ value.add("position", position);
|
|
+
|
|
+ position.add("x", new JsonPrimitive(src.x));
|
|
+ position.add("y", new JsonPrimitive(src.y));
|
|
+ position.add("z", new JsonPrimitive(src.z));
|
|
+ position.add("yaw", new JsonPrimitive(src.phi));
|
|
+ position.add("pitch", new JsonPrimitive(src.theta));
|
|
+ position.add("roll", new JsonPrimitive(src.varphi));
|
|
+ return ret;
|
|
+ }
|
|
+ }
|
|
+}
|
|
diff --git a/src/main/java/top/leavesmc/leaves/replay/ServerPhotographer.java b/src/main/java/top/leavesmc/leaves/replay/ServerPhotographer.java
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..ceac26080c5378d0e0fdd1bbda1ca26ead55b4c6
|
|
--- /dev/null
|
|
+++ b/src/main/java/top/leavesmc/leaves/replay/ServerPhotographer.java
|
|
@@ -0,0 +1,211 @@
|
|
+package top.leavesmc.leaves.replay;
|
|
+
|
|
+import com.mojang.authlib.GameProfile;
|
|
+import net.minecraft.server.MinecraftServer;
|
|
+import net.minecraft.server.level.ServerLevel;
|
|
+import net.minecraft.server.level.ServerPlayer;
|
|
+import net.minecraft.stats.ServerStatsCounter;
|
|
+import net.minecraft.world.damagesource.DamageSource;
|
|
+import net.minecraft.world.level.GameType;
|
|
+import net.minecraft.world.phys.Vec3;
|
|
+import org.bukkit.Location;
|
|
+import org.bukkit.craftbukkit.CraftWorld;
|
|
+import org.jetbrains.annotations.NotNull;
|
|
+import top.leavesmc.leaves.LeavesLogger;
|
|
+import top.leavesmc.leaves.bot.BotStatsCounter;
|
|
+import top.leavesmc.leaves.entity.CraftPhotographer;
|
|
+import top.leavesmc.leaves.entity.Photographer;
|
|
+
|
|
+import java.io.File;
|
|
+import java.io.IOException;
|
|
+import java.util.List;
|
|
+import java.util.UUID;
|
|
+import java.util.concurrent.CompletableFuture;
|
|
+import java.util.concurrent.CopyOnWriteArrayList;
|
|
+
|
|
+import static top.leavesmc.leaves.bot.ServerBot.isCreateLegal;
|
|
+
|
|
+public class ServerPhotographer extends ServerPlayer {
|
|
+
|
|
+ private static final List<ServerPhotographer> photographers = new CopyOnWriteArrayList<>();
|
|
+
|
|
+ public PhotographerCreateState createState;
|
|
+ private ServerPlayer followPlayer;
|
|
+ private Recorder recorder;
|
|
+ private File saveFile;
|
|
+ private Vec3 lastPos;
|
|
+
|
|
+ private final ServerStatsCounter stats;
|
|
+
|
|
+ private ServerPhotographer(MinecraftServer server, ServerLevel world, GameProfile profile) {
|
|
+ super(server, world, profile);
|
|
+ this.followPlayer = null;
|
|
+ this.stats = new BotStatsCounter(server);
|
|
+ this.lastPos = this.position();
|
|
+ }
|
|
+
|
|
+ public static ServerPhotographer createPhotographer(@NotNull PhotographerCreateState state) throws IOException {
|
|
+ if (!isCreateLegal(state.id)) {
|
|
+ return null;
|
|
+ }
|
|
+
|
|
+ MinecraftServer server = MinecraftServer.getServer();
|
|
+
|
|
+ ServerLevel world = ((CraftWorld) state.loc.getWorld()).getHandle();
|
|
+ GameProfile profile = new GameProfile(UUID.randomUUID(), state.id);
|
|
+
|
|
+ ServerPhotographer photographer = new ServerPhotographer(server, world, profile);
|
|
+ photographer.recorder = new Recorder(photographer, state.option, new File("replay", state.id));
|
|
+ photographer.saveFile = new File("replay", state.id + ".mcpr");
|
|
+ photographer.createState = state;
|
|
+
|
|
+ photographer.recorder.start();
|
|
+ MinecraftServer.getServer().getPlayerList().placeNewPhotographer(photographer.recorder, photographer, world, state.loc);
|
|
+ photographer.setGameMode(GameType.SPECTATOR);
|
|
+ photographer.serverLevel().chunkSource.move(photographer);
|
|
+ photographer.setInvisible(true);
|
|
+ photographers.add(photographer);
|
|
+
|
|
+ LeavesLogger.LOGGER.info("Photographer " + state.id + " created");
|
|
+
|
|
+ // TODO record distance
|
|
+
|
|
+ return photographer;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public void tick() {
|
|
+ super.tick();
|
|
+ super.doTick();
|
|
+
|
|
+ if (this.server.getTickCount() % 10 == 0) {
|
|
+ connection.resetPosition();
|
|
+ this.serverLevel().chunkSource.move(this);
|
|
+ }
|
|
+
|
|
+ if (this.followPlayer != null) {
|
|
+ if (this.getCamera() == this || this.getCamera().level() != this.level()) {
|
|
+ this.getBukkitPlayer().teleport(this.getCamera().getBukkitEntity().getLocation());
|
|
+ this.setCamera(followPlayer);
|
|
+ }
|
|
+ if (lastPos.distanceToSqr(this.position()) > 1024D) {
|
|
+ this.getBukkitPlayer().teleport(this.getCamera().getBukkitEntity().getLocation());
|
|
+ }
|
|
+ }
|
|
+
|
|
+ lastPos = this.position();
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public void die(@NotNull DamageSource damageSource) {
|
|
+ super.die(damageSource);
|
|
+ remove(true);
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public boolean isInvulnerableTo(@NotNull DamageSource damageSource) {
|
|
+ return true;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public boolean hurt(@NotNull DamageSource source, float amount) {
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public void setHealth(float health) {
|
|
+ }
|
|
+
|
|
+ @NotNull
|
|
+ @Override
|
|
+ public ServerStatsCounter getStats() {
|
|
+ return stats;
|
|
+ }
|
|
+
|
|
+ public void remove(boolean async) {
|
|
+ super.remove(RemovalReason.KILLED);
|
|
+ photographers.remove(this);
|
|
+ this.recorder.stop();
|
|
+ this.server.getPlayerList().removePhotographer(this);
|
|
+
|
|
+ LeavesLogger.LOGGER.info("Photographer " + createState.id + " removed");
|
|
+
|
|
+ if (!recorder.isSaved()) {
|
|
+ CompletableFuture<Void> future = recorder.saveRecording(saveFile);
|
|
+ if (!async) {
|
|
+ future.join();
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ public void setFollowPlayer(ServerPlayer followPlayer) {
|
|
+ this.setCamera(followPlayer);
|
|
+ this.followPlayer = followPlayer;
|
|
+ }
|
|
+
|
|
+ public void setSaveFile(File saveFile) {
|
|
+ this.saveFile = saveFile;
|
|
+ }
|
|
+
|
|
+ public void pauseRecording() {
|
|
+ this.recorder.pauseRecording();
|
|
+ }
|
|
+
|
|
+ public void resumeRecording() {
|
|
+ this.recorder.resumeRecording();
|
|
+ }
|
|
+
|
|
+ public static ServerPhotographer getPhotographer(String id) {
|
|
+ for (ServerPhotographer photographer : photographers) {
|
|
+ if (photographer.createState.id.equals(id)) {
|
|
+ return photographer;
|
|
+ }
|
|
+ }
|
|
+ return null;
|
|
+ }
|
|
+
|
|
+ public static ServerPhotographer getPhotographer(UUID uuid) {
|
|
+ for (ServerPhotographer photographer : photographers) {
|
|
+ if (photographer.getUUID().equals(uuid)) {
|
|
+ return photographer;
|
|
+ }
|
|
+ }
|
|
+ return null;
|
|
+ }
|
|
+
|
|
+ public static List<ServerPhotographer> getPhotographers() {
|
|
+ return photographers;
|
|
+ }
|
|
+
|
|
+ public Photographer getBukkitPlayer() {
|
|
+ return getBukkitEntity();
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ @NotNull
|
|
+ public CraftPhotographer getBukkitEntity() {
|
|
+ return (CraftPhotographer) super.getBukkitEntity();
|
|
+ }
|
|
+
|
|
+ public static class PhotographerCreateState {
|
|
+
|
|
+ public RecorderOption option;
|
|
+ public Location loc;
|
|
+ public final String id;
|
|
+
|
|
+ public PhotographerCreateState(Location loc, String id, RecorderOption option) {
|
|
+ this.loc = loc;
|
|
+ this.id = id;
|
|
+ this.option = option;
|
|
+ }
|
|
+
|
|
+ public ServerPhotographer createSync() {
|
|
+ try {
|
|
+ return createPhotographer(this);
|
|
+ } catch (IOException e) {
|
|
+ e.printStackTrace();
|
|
+ }
|
|
+ return null;
|
|
+ }
|
|
+ }
|
|
+}
|
|
diff --git a/src/main/java/top/leavesmc/leaves/util/UUIDSerializer.java b/src/main/java/top/leavesmc/leaves/util/UUIDSerializer.java
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..1329a725a2bd03d3ef6d7131d8bc77f20bf2e566
|
|
--- /dev/null
|
|
+++ b/src/main/java/top/leavesmc/leaves/util/UUIDSerializer.java
|
|
@@ -0,0 +1,17 @@
|
|
+package top.leavesmc.leaves.util;
|
|
+
|
|
+import com.google.gson.JsonElement;
|
|
+import com.google.gson.JsonPrimitive;
|
|
+import com.google.gson.JsonSerializationContext;
|
|
+import com.google.gson.JsonSerializer;
|
|
+import org.jetbrains.annotations.NotNull;
|
|
+
|
|
+import java.lang.reflect.Type;
|
|
+import java.util.UUID;
|
|
+
|
|
+public class UUIDSerializer implements JsonSerializer<UUID> {
|
|
+ @Override
|
|
+ public JsonElement serialize(@NotNull UUID src, Type typeOfSrc, JsonSerializationContext context) {
|
|
+ return new JsonPrimitive(src.toString());
|
|
+ }
|
|
+}
|