Start Pre-release 26.1.2

some patches still not applied, need further update
This commit is contained in:
Helvetica Volubi
2026-05-17 05:28:47 +08:00
parent eff43b61ff
commit 3856196543
39 changed files with 338 additions and 387 deletions
@@ -36,3 +36,23 @@ index 0a20c6f03625391201e5ced167707e54ed27daa1..2872b22e44cba48442c526f0a53e1fc6
this.createConnection(player, transmitter);
}
diff --git a/src/main/java/org/leavesmc/leaves/plugin/ServerFeatureManager.java b/src/main/java/org/leavesmc/leaves/plugin/ServerFeatureManager.java
index 8f0548908a9e43ce29c4476f2707f6898e540aec..8b7c66785a06eb4782de0b0a9b542221d6258220 100644
--- a/src/main/java/org/leavesmc/leaves/plugin/ServerFeatureManager.java
+++ b/src/main/java/org/leavesmc/leaves/plugin/ServerFeatureManager.java
@@ -11,14 +11,7 @@ public class ServerFeatureManager implements FeatureManager {
private final Set<String> availableFeatures = new HashSet<>();
private ServerFeatureManager() {
- if (false) { // Luminol not provide this
- availableFeatures.addAll(Set.of(
- FAKEPLAYER,
- PHOTOGRAPHER,
- BYTEBUF,
- UPDATE_SUPPRESSION_EVENT
- ));
- }
+ availableFeatures.add(FAKEPLAYER); // Lophine - fakeplayer support
if (Boolean.getBoolean("leavesclip.enable.mixin")) {
availableFeatures.add(MIXIN);
}
@@ -1,26 +0,0 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Helvetica Volubi <suisuroru@blue-millennium.fun>
Date: Sun, 17 May 2026 03:43:54 +0800
Subject: [PATCH] Leaves: Fakeplayer
diff --git a/src/main/java/org/leavesmc/leaves/plugin/ServerFeatureManager.java b/src/main/java/org/leavesmc/leaves/plugin/ServerFeatureManager.java
index 8f0548908a9e43ce29c4476f2707f6898e540aec..009f05ac58b9d1cf809e997e30158eb5d667e9f0 100644
--- a/src/main/java/org/leavesmc/leaves/plugin/ServerFeatureManager.java
+++ b/src/main/java/org/leavesmc/leaves/plugin/ServerFeatureManager.java
@@ -11,14 +11,7 @@ public class ServerFeatureManager implements FeatureManager {
private final Set<String> availableFeatures = new HashSet<>();
private ServerFeatureManager() {
- if (false) { // Luminol not provide this
- availableFeatures.addAll(Set.of(
- FAKEPLAYER,
- PHOTOGRAPHER,
- BYTEBUF,
- UPDATE_SUPPRESSION_EVENT
- ));
- }
+ availableFeatures.add(FAKEPLAYER);
if (Boolean.getBoolean("leavesclip.enable.mixin")) {
availableFeatures.add(MIXIN);
}
@@ -0,0 +1,18 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Helvetica Volubi <suisuroru@blue-millennium.fun>
Date: Sun, 17 May 2026 05:33:21 +0800
Subject: [PATCH] Leaves: Replay Mod API
diff --git a/src/main/java/org/leavesmc/leaves/plugin/ServerFeatureManager.java b/src/main/java/org/leavesmc/leaves/plugin/ServerFeatureManager.java
index 8b7c66785a06eb4782de0b0a9b542221d6258220..1f3fe7f786bb8f7c59e3a0235dcc572f3171ee6b 100644
--- a/src/main/java/org/leavesmc/leaves/plugin/ServerFeatureManager.java
+++ b/src/main/java/org/leavesmc/leaves/plugin/ServerFeatureManager.java
@@ -12,6 +12,7 @@ public class ServerFeatureManager implements FeatureManager {
private ServerFeatureManager() {
availableFeatures.add(FAKEPLAYER); // Lophine - fakeplayer support
+ availableFeatures.add(PHOTOGRAPHER); // Lophine - Replay Mod API support
if (Boolean.getBoolean("leavesclip.enable.mixin")) {
availableFeatures.add(MIXIN);
}
@@ -48,7 +48,7 @@ index 6cedc129f31289eaf4e3580219fc1988c0f3c5a1..5f1fad59a26e3fe29f9000a92be5d6c4
AABB aabb = new AABB(this.blockPosition()).inflate(32.0, 10.0, 32.0);
this.level()
diff --git a/net/minecraft/world/entity/LivingEntity.java b/net/minecraft/world/entity/LivingEntity.java
index 17483ffff6cb2a9e86db7079e7c3a9a800654c62..fde9fad11e130cf93205d23da09ad369b35db160 100644
index 17483ffff6cb2a9e86db7079e7c3a9a800654c62..29919b8f3c05c8ffaf66f86e441778eeefb293b1 100644
--- a/net/minecraft/world/entity/LivingEntity.java
+++ b/net/minecraft/world/entity/LivingEntity.java
@@ -1961,6 +1961,13 @@ public abstract class LivingEntity extends Entity implements Attackable, Waypoin
@@ -65,7 +65,7 @@ index 17483ffff6cb2a9e86db7079e7c3a9a800654c62..fde9fad11e130cf93205d23da09ad369
}
}); // Paper end
this.postDeathDropItems(deathEvent); // Paper
@@ -1971,6 +1978,18 @@ public abstract class LivingEntity extends Entity implements Attackable, Waypoin
@@ -1971,6 +1978,19 @@ public abstract class LivingEntity extends Entity implements Attackable, Waypoin
return deathEvent; // Paper
}
@@ -73,6 +73,7 @@ index 17483ffff6cb2a9e86db7079e7c3a9a800654c62..fde9fad11e130cf93205d23da09ad369
+ private void damageTransferToAsync(LivingEntity entity, DamageSource damageSource) {
+ entity.getBukkitEntity().taskScheduler.schedule((LivingEntity nmsEntity) -> {
+ try {
+ if (nmsEntity.isRemoved()) return;
+ nmsEntity.awardKillScore(this, damageSource);
+ } catch (Throwable ex) {
+ LOGGER.error(ex.getMessage(), ex);
@@ -84,7 +85,7 @@ index 17483ffff6cb2a9e86db7079e7c3a9a800654c62..fde9fad11e130cf93205d23da09ad369
protected void dropEquipment(final ServerLevel level) {
}
protected void postDeathDropItems(org.bukkit.event.entity.EntityDeathEvent event) {} // Paper - method for post death logic that cannot be ran before the event is potentially cancelled
@@ -2629,6 +2648,18 @@ public abstract class LivingEntity extends Entity implements Attackable, Waypoin
@@ -2629,6 +2649,18 @@ public abstract class LivingEntity extends Entity implements Attackable, Waypoin
}
}
@@ -6,10 +6,10 @@ Subject: [PATCH] Compatibility fix for Raid Revert and Cross Region Damage
diff --git a/net/minecraft/world/entity/LivingEntity.java b/net/minecraft/world/entity/LivingEntity.java
index fde9fad11e130cf93205d23da09ad369b35db160..44956582516ac508681d7ca3e4e04e4270859bd8 100644
index 29919b8f3c05c8ffaf66f86e441778eeefb293b1..1e820f34636f415ef78cdfbed06c6952a3cf1c0b 100644
--- a/net/minecraft/world/entity/LivingEntity.java
+++ b/net/minecraft/world/entity/LivingEntity.java
@@ -1234,6 +1234,29 @@ public abstract class LivingEntity extends Entity implements Attackable, Waypoin
@@ -1234,6 +1234,30 @@ public abstract class LivingEntity extends Entity implements Attackable, Waypoin
}
}
@@ -28,11 +28,12 @@ index fde9fad11e130cf93205d23da09ad369b35db160..44956582516ac508681d7ca3e4e04e42
+ if (entity != null)
+ entity.getBukkitEntity().taskScheduler.schedule((Entity nmsEntity) -> {
+ try {
+ if (nmsEntity.isRemoved()) return;
+ addEffect(effectInstance, nmsEntity, cause, fireEvent);
+ } catch (Throwable ex) {
+ LOGGER.error(ex.getMessage(), ex);
+ }
+ }, null, 1L );
+ }, null, 1L);
+ }
+ // Lophine End - raid revert adapt Cross Region Damage trace
+
@@ -544,10 +544,10 @@ index b69698f47ccb0f1ac3eeea05a02ab9d798319b44..b073a91fd67d092713c53db6f3df75c6
final boolean xZero = movement.x == 0.0;
final boolean yZero = movement.y == 0.0;
diff --git a/net/minecraft/world/entity/LivingEntity.java b/net/minecraft/world/entity/LivingEntity.java
index 44956582516ac508681d7ca3e4e04e4270859bd8..30f287141689b65ce61fdb22214847d0af1ff44e 100644
index 1e820f34636f415ef78cdfbed06c6952a3cf1c0b..8a8de7f4d62532709805ebde531dc71ad78fa5c9 100644
--- a/net/minecraft/world/entity/LivingEntity.java
+++ b/net/minecraft/world/entity/LivingEntity.java
@@ -3345,7 +3345,7 @@ public abstract class LivingEntity extends Entity implements Attackable, Waypoin
@@ -3347,7 +3347,7 @@ public abstract class LivingEntity extends Entity implements Attackable, Waypoin
private void travelRidden(final Player controller, final Vec3 selfInput) {
Vec3 riddenInput = this.getRiddenInput(controller, selfInput);
this.tickRidden(controller, riddenInput);
@@ -556,7 +556,7 @@ index 44956582516ac508681d7ca3e4e04e4270859bd8..30f287141689b65ce61fdb22214847d0
this.setSpeed(this.getRiddenSpeed(controller));
this.travel(riddenInput);
} else {
@@ -4268,7 +4268,7 @@ public abstract class LivingEntity extends Entity implements Attackable, Waypoin
@@ -4270,7 +4270,7 @@ public abstract class LivingEntity extends Entity implements Attackable, Waypoin
// Paper start - lag compensate eating
// we add 1 to the expected time to avoid lag compensating when we should not
final boolean shouldLagCompensate = this.useItem.has(DataComponents.FOOD) && this.eatStartTime != -1 && (System.nanoTime() - this.eatStartTime) > ((1L + this.totalEatTimeTicks) * 50L * (1000L * 1000L));
@@ -565,7 +565,7 @@ index 44956582516ac508681d7ca3e4e04e4270859bd8..30f287141689b65ce61fdb22214847d0
this.useItemRemaining = 0;
// Paper end - lag compensate eating
this.completeUsingItem();
@@ -4457,6 +4457,23 @@ public abstract class LivingEntity extends Entity implements Attackable, Waypoin
@@ -4459,6 +4459,23 @@ public abstract class LivingEntity extends Entity implements Attackable, Waypoin
this.stopUsingItem();
}
@@ -0,0 +1,560 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Helvetica Volubi <suisuroru@blue-millennium.fun>
Date: Sun, 16 Nov 2025 15:48:12 +0800
Subject: [PATCH] Leaves: Replay Mod API
Co-authored by: violetc <58360096+s-yh-china@users.noreply.github.com>
As a part of : Leaves (https://github.com/LeavesMC/Leaves) & ReplayMod (https://github.com/ReplayMod)
Licensed under: GPL-3.0 (https://www.gnu.org/licenses/gpl-3.0.html)
This patch is Powered by ReplayMod (https://github.com/ReplayMod)
diff --git a/net/minecraft/commands/CommandSourceStack.java b/net/minecraft/commands/CommandSourceStack.java
index 28776f3363810259f8c3bfdfd9f8c0455975be2a..0f562d7403aebf6c0907f2a33f758f4351ef282e 100644
--- a/net/minecraft/commands/CommandSourceStack.java
+++ b/net/minecraft/commands/CommandSourceStack.java
@@ -562,7 +562,7 @@ public class CommandSourceStack implements SharedSuggestionProvider, ExecutionCo
@Override
public Collection<String> getOnlinePlayerNames() {
- return this.entity instanceof ServerPlayer sourcePlayer && !sourcePlayer.getBukkitEntity().hasPermission("paper.bypass-visibility.tab-completion") ? this.getServer().getPlayerList().getPlayers().stream().filter(serverPlayer -> sourcePlayer.getBukkitEntity().canSee(serverPlayer.getBukkitEntity())).map(serverPlayer -> serverPlayer.getGameProfile().name()).toList() : Lists.newArrayList(this.server.getPlayerNames()); // Paper - Make CommandSourceStack respect hidden players
+ return this.entity instanceof ServerPlayer sourcePlayer && !(sourcePlayer instanceof org.leavesmc.leaves.replay.ServerPhotographer) && !sourcePlayer.getBukkitEntity().hasPermission("paper.bypass-visibility.tab-completion") ? this.getServer().getPlayerList().getPlayers().stream().filter(serverPlayer -> sourcePlayer.getBukkitEntity().canSee(serverPlayer.getBukkitEntity())).map(serverPlayer -> serverPlayer.getGameProfile().name()).toList() : Lists.newArrayList(this.server.getPlayerNames()); // Paper - Make CommandSourceStack respect hidden players // Leaves - only real player
}
@Override
diff --git a/net/minecraft/commands/arguments/selector/EntitySelector.java b/net/minecraft/commands/arguments/selector/EntitySelector.java
index 7e3912b8b29e40eb3347f1451b0fd3ebee2af4f1..0834e402e6af7028e08851378e15390ec6314380 100644
--- a/net/minecraft/commands/arguments/selector/EntitySelector.java
+++ b/net/minecraft/commands/arguments/selector/EntitySelector.java
@@ -142,11 +142,12 @@ public class EntitySelector {
return this.findPlayers(sender);
} else if (this.playerName != null) {
ServerPlayer result = sender.getServer().getPlayerList().getPlayerByName(this.playerName);
+ result = result instanceof org.leavesmc.leaves.replay.ServerPhotographer ? null : result; // Leaves - skip photographer
return result == null ? List.of() : List.of(result);
} else if (this.entityUUID != null) {
for (ServerLevel level : sender.getServer().getAllLevels()) {
Entity entity = level.getEntity(this.entityUUID);
- if (entity != null) {
+ if (entity != null && !(entity instanceof org.leavesmc.leaves.replay.ServerPhotographer)) {
if (entity.getType().isEnabled(sender.enabledFeatures())) {
return List.of(entity);
}
@@ -160,7 +161,7 @@ public class EntitySelector {
AABB absoluteAabb = this.getAbsoluteAabb(pos);
if (this.currentEntity) {
Predicate<Entity> predicate = this.getPredicate(pos, absoluteAabb, null);
- return sender.getEntity() != null && predicate.test(sender.getEntity()) ? List.of(sender.getEntity()) : List.of();
+ return sender.getEntity() != null && !(sender.getEntity() instanceof org.leavesmc.leaves.replay.ServerPhotographer) && predicate.test(sender.getEntity()) ? List.of(sender.getEntity()) : List.of(); // Leaves - skip photographer
} else {
Predicate<Entity> predicate = this.getPredicate(pos, absoluteAabb, sender.enabledFeatures());
List<Entity> result = new ObjectArrayList<>();
@@ -171,6 +172,7 @@ public class EntitySelector {
this.addEntities(result, levelx, absoluteAabb, predicate);
}
}
+ result.removeIf(entity -> entity instanceof org.leavesmc.leaves.replay.ServerPhotographer); // Leaves - skip photographer
return this.sortAndLimit(pos, result);
}
@@ -206,9 +208,11 @@ public class EntitySelector {
this.checkPermissions(sender);
if (this.playerName != null) {
ServerPlayer result = sender.getServer().getPlayerList().getPlayerByName(this.playerName);
+ result = result instanceof org.leavesmc.leaves.replay.ServerPhotographer ? null : result; // Leaves - skip photographer
return result == null ? List.of() : List.of(result);
} else if (this.entityUUID != null) {
ServerPlayer result = sender.getServer().getPlayerList().getPlayer(this.entityUUID);
+ result = result instanceof org.leavesmc.leaves.replay.ServerPhotographer ? null : result; // Leaves - skip photographer
return result == null ? List.of() : List.of(result);
} else {
Vec3 pos = this.position.apply(sender.getPosition());
@@ -220,11 +224,11 @@ public class EntitySelector {
int limit = this.getResultLimit();
List<ServerPlayer> result;
if (this.isWorldLimited()) {
- result = sender.getLevel().getPlayers(predicate, limit);
+ result = sender.getLevel().getPlayers((player -> !(player instanceof org.leavesmc.leaves.replay.ServerPhotographer) && predicate.test(player)), limit); // Leaves - skip photographer
} else {
result = new ObjectArrayList<>();
- for (ServerPlayer player : sender.getServer().getPlayerList().getPlayers()) {
+ for (ServerPlayer player : sender.getServer().getPlayerList().realPlayers) { // Leaves - only real players
if (predicate.test(player)) {
result.add(player);
if (result.size() >= limit) {
diff --git a/net/minecraft/server/MinecraftServer.java b/net/minecraft/server/MinecraftServer.java
index 86393793dd1b90f033c93c70933e1e4a7546eb15..48f7d57f6d3ef4250e6b3cf42055d29d94d3f6e3 100644
--- a/net/minecraft/server/MinecraftServer.java
+++ b/net/minecraft/server/MinecraftServer.java
@@ -1921,7 +1921,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
}
private ServerStatus.Players buildPlayerStatus() {
- List<ServerPlayer> players = new java.util.ArrayList<>(this.playerList.getPlayers()); // Folia - region threading
+ List<ServerPlayer> players = new java.util.ArrayList<>(this.playerList.realPlayers); // Folia - region threading // Leaves - only real player
int maxPlayers = this.getMaxPlayers();
if (this.hidesOnlinePlayers()) {
return new ServerStatus.Players(maxPlayers, players.size(), List.of());
@@ -2142,7 +2142,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
@Override
public int getPlayerCount() {
- return this.playerList.getPlayerCount();
+ return this.playerList.realPlayers.size(); // Leaves - only real player
}
public String[] getPlayerNames() {
@@ -2354,7 +2354,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
} else {
int count = 0;
- for (ServerPlayer player : this.getPlayerList().getPlayers()) {
+ for (ServerPlayer player : this.getPlayerList().realPlayers) { // Leaves - only real player
player.getBukkitEntity().taskScheduler.schedule((ServerPlayer updatedPlayer) -> { // Folia - region threading
// Paper start - Expand PlayerGameModeChangeEvent
org.bukkit.event.player.PlayerGameModeChangeEvent event = updatedPlayer.setGameMode(gameType, org.bukkit.event.player.PlayerGameModeChangeEvent.Cause.DEFAULT_GAMEMODE, null); // Folia - region threading
@@ -2541,7 +2541,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
if (Thread.currentThread() != this.serverThread) return; // Paper
// Paper start - we don't need to save everything, just advancements
// this.getPlayerList().saveAll();
- for (final ServerPlayer player : this.getPlayerList().getPlayers()) {
+ for (final ServerPlayer player : this.getPlayerList().realPlayers) { // Leaves - only real players
player.getAdvancements().save();
}
// Paper end - we don't need to save everything, just advancements
@@ -2677,7 +2677,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
PlayerList playerList = this.getPlayerList();
UserWhiteList whiteList = playerList.getWhiteList();
- for (ServerPlayer player : Lists.newArrayList(playerList.getPlayers())) {
+ for (ServerPlayer player : Lists.newArrayList(playerList.realPlayers)) { // Leaves - only real player
if (!whiteList.isWhiteListed(player.nameAndId()) && !this.getPlayerList().isOp(player.nameAndId())) { // Paper - Fix kicking ops when whitelist is reloaded (MC-171420)
player.connection.disconnect(net.kyori.adventure.text.Component.text(org.spigotmc.SpigotConfig.whitelistMessage), org.bukkit.event.player.PlayerKickEvent.Cause.WHITELIST); // Paper - use configurable message & kick event cause
}
diff --git a/net/minecraft/server/PlayerAdvancements.java b/net/minecraft/server/PlayerAdvancements.java
index 89448eee9f135de8b406bf5f4147b3298e33ba17..b286aefc7d328bc7a7f44ce93615392075d9fef7 100644
--- a/net/minecraft/server/PlayerAdvancements.java
+++ b/net/minecraft/server/PlayerAdvancements.java
@@ -169,7 +169,7 @@ public class PlayerAdvancements {
public boolean award(final AdvancementHolder holder, final String criterion) {
// Leaves start - bot can't get advancement
- if (player instanceof org.leavesmc.leaves.bot.ServerBot) {
+ if (player instanceof org.leavesmc.leaves.bot.ServerBot || player instanceof org.leavesmc.leaves.replay.ServerPhotographer) { // Leaves - and photographer
return false;
}
// Leaves end - bot can't get advancement
diff --git a/net/minecraft/server/ServerScoreboard.java b/net/minecraft/server/ServerScoreboard.java
index 9bf09b888425e491072864265c8de3885154a0ba..97a8fee8280061312a28013a5400e835db2eb474 100644
--- a/net/minecraft/server/ServerScoreboard.java
+++ b/net/minecraft/server/ServerScoreboard.java
@@ -245,7 +245,7 @@ public class ServerScoreboard extends Scoreboard {
public void startTrackingObjective(final Objective objective) {
List<Packet<?>> packets = this.getStartTrackingPackets(objective);
- for (ServerPlayer player : this.server.getPlayerList().getPlayers()) {
+ for (ServerPlayer player : this.server.getPlayerList().realPlayers) { // Leaves - only real players
if (player.getBukkitEntity().getScoreboard().getHandle() != this) continue; // CraftBukkit - Only players on this board
for (Packet<?> packet : packets) {
player.connection.send(packet);
@@ -271,7 +271,7 @@ public class ServerScoreboard extends Scoreboard {
public void stopTrackingObjective(final Objective objective) {
List<Packet<?>> packets = this.getStopTrackingPackets(objective);
- for (ServerPlayer player : this.server.getPlayerList().getPlayers()) {
+ for (ServerPlayer player : this.server.getPlayerList().realPlayers) { // Leaves - only real players
if (player.getBukkitEntity().getScoreboard().getHandle() != this) continue; // CraftBukkit - Only players on this board
for (Packet<?> packet : packets) {
player.connection.send(packet);
diff --git a/net/minecraft/server/commands/ListPlayersCommand.java b/net/minecraft/server/commands/ListPlayersCommand.java
index b850c908a594f839ce124c6bd01762d31cc3aa1e..b2240fd3f5ace03168fd679932675a72c09e81d7 100644
--- a/net/minecraft/server/commands/ListPlayersCommand.java
+++ b/net/minecraft/server/commands/ListPlayersCommand.java
@@ -33,7 +33,7 @@ public class ListPlayersCommand {
private static int format(final CommandSourceStack source, final Function<ServerPlayer, Component> formatter) {
PlayerList playerList = source.getServer().getPlayerList();
// CraftBukkit start
- List<ServerPlayer> playersTemp = playerList.getPlayers();
+ List<ServerPlayer> playersTemp = playerList.realPlayers;
if (source.getBukkitSender() instanceof org.bukkit.entity.Player) {
org.bukkit.entity.Player sender = (org.bukkit.entity.Player) source.getBukkitSender();
playersTemp = playersTemp.stream().filter((ep) -> sender.canSee(ep.getBukkitEntity())).collect(java.util.stream.Collectors.toList());
diff --git a/net/minecraft/server/commands/OpCommand.java b/net/minecraft/server/commands/OpCommand.java
index 20cbc0e65fea3fc2d0033259c2f35e158ea81926..6bcd09b8d39333bd5f2c0d7c0a9a91bb52f9314d 100644
--- a/net/minecraft/server/commands/OpCommand.java
+++ b/net/minecraft/server/commands/OpCommand.java
@@ -25,7 +25,8 @@ public class OpCommand {
(c, p) -> {
PlayerList list = c.getSource().getServer().getPlayerList();
return SharedSuggestionProvider.suggest(
- list.getPlayers().stream().filter(player -> !list.isOp(player.nameAndId())).map(pl -> pl.getGameProfile().name()), p
+ list.realPlayers // Leaves - only real player
+ .stream().filter(player -> !list.isOp(player.nameAndId())).map(pl -> pl.getGameProfile().name()), p
);
}
)
diff --git a/net/minecraft/server/commands/ParticleCommand.java b/net/minecraft/server/commands/ParticleCommand.java
index a14f82a1587e175030d4cd806726456831008dea..563d897a70f402d556cabd3e02131aaf22ff8d1e 100644
--- a/net/minecraft/server/commands/ParticleCommand.java
+++ b/net/minecraft/server/commands/ParticleCommand.java
@@ -36,7 +36,7 @@ public class ParticleCommand {
0.0F,
0,
false,
- c.getSource().getServer().getPlayerList().getPlayers()
+ c.getSource().getServer().getPlayerList().realPlayers // Leaves - only real player
)
)
.then(
@@ -50,7 +50,7 @@ public class ParticleCommand {
0.0F,
0,
false,
- c.getSource().getServer().getPlayerList().getPlayers()
+ c.getSource().getServer().getPlayerList().realPlayers // Leaves - only real player
)
)
.then(
@@ -68,7 +68,7 @@ public class ParticleCommand {
FloatArgumentType.getFloat(c, "speed"),
IntegerArgumentType.getInteger(c, "count"),
false,
- c.getSource().getServer().getPlayerList().getPlayers()
+ c.getSource().getServer().getPlayerList().realPlayers // Leaves - only real player
)
)
.then(
@@ -82,7 +82,7 @@ public class ParticleCommand {
FloatArgumentType.getFloat(c, "speed"),
IntegerArgumentType.getInteger(c, "count"),
true,
- c.getSource().getServer().getPlayerList().getPlayers()
+ c.getSource().getServer().getPlayerList().realPlayers // Leaves - only real player
)
)
.then(
@@ -112,7 +112,7 @@ public class ParticleCommand {
FloatArgumentType.getFloat(c, "speed"),
IntegerArgumentType.getInteger(c, "count"),
false,
- c.getSource().getServer().getPlayerList().getPlayers()
+ c.getSource().getServer().getPlayerList().realPlayers // Leaves - only real player
)
)
.then(
diff --git a/net/minecraft/server/commands/TeamMsgCommand.java b/net/minecraft/server/commands/TeamMsgCommand.java
index d04fd8ab5f8e3c1697af5448c320838a822dbf1f..f46abe413e9b30f219fa1f7fc485de4c8859be90 100644
--- a/net/minecraft/server/commands/TeamMsgCommand.java
+++ b/net/minecraft/server/commands/TeamMsgCommand.java
@@ -40,7 +40,7 @@ public class TeamMsgCommand {
} else {
List<ServerPlayer> receivers = source.getServer()
.getPlayerList()
- .getPlayers()
+ .realPlayers // Leaves - only real players
.stream()
.filter(receiver -> receiver == entity || receiver.getTeam() == team)
.toList();
diff --git a/net/minecraft/server/commands/WhitelistCommand.java b/net/minecraft/server/commands/WhitelistCommand.java
index 907d1304503a80c6d0fd064bf1aaf4e081e9f85b..e0c1ee55888261bca42bbadec8812da2a36dded8 100644
--- a/net/minecraft/server/commands/WhitelistCommand.java
+++ b/net/minecraft/server/commands/WhitelistCommand.java
@@ -44,7 +44,7 @@ public class WhitelistCommand {
(c, p) -> {
PlayerList list = c.getSource().getServer().getPlayerList();
return SharedSuggestionProvider.suggest(
- list.getPlayers()
+ list.realPlayers // Leaves - only real player
.stream()
.map(Player::nameAndId)
.filter(nameAndId -> !list.getWhiteList().isWhiteListed(nameAndId))
diff --git a/net/minecraft/server/gui/PlayerListComponent.java b/net/minecraft/server/gui/PlayerListComponent.java
index 37c9f983ae89c497f44a1b7967d741abf909e0a0..d7fb2505f2a86ca0d81f144624ef2e1a4b4bddbb 100644
--- a/net/minecraft/server/gui/PlayerListComponent.java
+++ b/net/minecraft/server/gui/PlayerListComponent.java
@@ -17,8 +17,8 @@ public class PlayerListComponent extends JList<String> {
if (this.tickCount++ % 20 == 0) {
Vector<String> players = new Vector<>();
- for (int i = 0; i < this.server.getPlayerList().getPlayers().size(); i++) {
- players.add(this.server.getPlayerList().getPlayers().get(i).getGameProfile().name());
+ for (int i = 0; i < this.server.getPlayerList().realPlayers.size(); i++) { // Leaves - only real players
+ players.add(this.server.getPlayerList().realPlayers.get(i).getGameProfile().name()); // Leaves - only real players
}
this.setListData(players);
diff --git a/net/minecraft/server/level/ServerLevel.java b/net/minecraft/server/level/ServerLevel.java
index e05705884a285143b8a3ebad00cb489efe47d532..c51b0075cd650085cd991bc89396f605cb520442 100644
--- a/net/minecraft/server/level/ServerLevel.java
+++ b/net/minecraft/server/level/ServerLevel.java
@@ -3100,7 +3100,7 @@ public class ServerLevel extends Level implements WorldGenLevel, ServerEntityGet
if (entity instanceof ServerPlayer player) {
ServerLevel.this.players.add(player);
// Leaves start - skip
- if (!(player instanceof org.leavesmc.leaves.bot.ServerBot)) {
+ if (!(player instanceof org.leavesmc.leaves.bot.ServerBot) && !(player instanceof org.leavesmc.leaves.replay.ServerPhotographer)) { // and photographer
ServerLevel.this.realPlayers.add(player);
}
// Leaves end - skip
@@ -3186,7 +3186,7 @@ public class ServerLevel extends Level implements WorldGenLevel, ServerEntityGet
if (entity instanceof ServerPlayer player) {
ServerLevel.this.players.remove(player);
// Leaves start - skip
- if (!(player instanceof org.leavesmc.leaves.bot.ServerBot)) {
+ if (!(player instanceof org.leavesmc.leaves.bot.ServerBot) && !(player instanceof org.leavesmc.leaves.replay.ServerPhotographer)) { // and photographer
ServerLevel.this.realPlayers.remove(player);
}
// Leaves end - skip
diff --git a/net/minecraft/server/players/PlayerList.java b/net/minecraft/server/players/PlayerList.java
index 4edc952087c3255ca488b140402621bbf6495770..a39480a7cd19074daf93accb1f9e7c41e33e775c 100644
--- a/net/minecraft/server/players/PlayerList.java
+++ b/net/minecraft/server/players/PlayerList.java
@@ -132,6 +132,7 @@ public abstract class PlayerList {
private int simulationDistance;
private boolean allowCommandsForAllPlayers;
private int sendAllPlayerInfoIn;
+ public final List<ServerPlayer> realPlayers = new java.util.concurrent.CopyOnWriteArrayList<>(); // Leaves - replay api
// CraftBukkit start
private org.bukkit.craftbukkit.CraftServer cserver;
@@ -193,6 +194,7 @@ public abstract class PlayerList {
private boolean countConnection(Connection conn, int limit) {
synchronized (this.connectionsStateLock) {
+ if (conn instanceof org.leavesmc.leaves.bot.ServerBotPacketListenerImpl.BotConnection) return true;
int count = this.usersCountedAgainstLimit.size();
if (count >= limit) {
return false;
@@ -223,6 +225,124 @@ public abstract class PlayerList {
abstract public void loadAndSaveFiles(); // Paper - fix converting txt to json file; moved from DedicatedPlayerList constructor
+ // Leaves start - replay mod api
+ public void placeNewPhotographer(Connection connection, org.leavesmc.leaves.replay.ServerPhotographer player, ServerLevel worldserver) {
+ player.isRealPlayer = true; // Paper
+ player.loginTime = System.currentTimeMillis(); // Paper
+
+ ServerLevel worldserver1 = worldserver;
+
+ player.setServerLevel(worldserver1);
+ player.spawnIn(worldserver1);
+ player.gameMode.setLevel(player.level());
+
+ LevelData worlddata = worldserver1.getLevelData();
+
+ ServerGamePacketListenerImpl playerconnection = new ServerGamePacketListenerImpl(this.server, connection, player, CommonListenerCookie.createInitial(player.gameProfile, false));
+ GameRules gamerules = worldserver1.getGameRules();
+ boolean flag = gamerules.get(GameRules.IMMEDIATE_RESPAWN);
+ boolean flag1 = gamerules.get(GameRules.REDUCED_DEBUG_INFO);
+ boolean flag2 = gamerules.get(GameRules.LIMITED_CRAFTING);
+
+ playerconnection.send(new ClientboundLoginPacket(player.getId(), worlddata.isHardcore(), this.server.levelKeys(), this.getMaxPlayers(), io.papermc.paper.FeatureHooks.getViewDistance(worldserver1), io.papermc.paper.FeatureHooks.getSimulationDistance(worldserver1), flag1, !flag, flag2, player.createCommonSpawnInfo(worldserver1), this.server.enforceSecureProfile()));
+ player.getBukkitEntity().sendSupportedChannels(); // CraftBukkit
+ playerconnection.send(new ClientboundChangeDifficultyPacket(worlddata.getDifficulty(), worlddata.isDifficultyLocked()));
+ playerconnection.send(new ClientboundPlayerAbilitiesPacket(player.getAbilities()));
+ playerconnection.send(new ClientboundSetHeldSlotPacket(player.getInventory().getSelectedSlot()));
+ RecipeManager craftingmanager = this.server.getRecipeManager();
+ playerconnection.send(new ClientboundUpdateRecipesPacket(craftingmanager.getSynchronizedItemProperties(), craftingmanager.getSynchronizedStonecutterRecipes()));
+
+ this.sendPlayerPermissionLevel(player);
+ player.getStats().markAllDirty();
+ player.getRecipeBook().sendInitialRecipeBook(player);
+ this.updateEntireScoreboard(worldserver1.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); // Spigot
+ this.playersByUUID.put(player.getUUID(), player);
+
+ player.suppressTrackerForLogin = true;
+ worldserver1.addNewPlayer(player);
+ this.server.getCustomBossEvents().onPlayerConnect(player);
+ org.bukkit.craftbukkit.entity.CraftPlayer bukkitPlayer = player.getBukkitEntity();
+
+ player.containerMenu.transferTo(player.containerMenu, bukkitPlayer);
+ if (!player.connection.isAcceptingMessages()) {
+ return;
+ }
+
+ // org.leavesmc.leaves.protocol.core.LeavesProtocolManager.handlePlayerJoin(player); // Leaves - protocol
+
+ // Leaves start - bot support
+ if (FakeplayerConfig.enable) {
+ org.leavesmc.leaves.bot.ServerBot bot = this.server.getBotList().getBotByName(player.getScoreboardName());
+ if (bot != null) {
+ this.server.getBotList().removeBot(bot, org.leavesmc.leaves.event.bot.BotRemoveEvent.RemoveReason.INTERNAL, player.getBukkitEntity(), false, false);
+ }
+ this.server.getBotList().bots.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;
+ }
+
+ // Leaves start - skip photographer
+ if (entityplayer1 instanceof org.leavesmc.leaves.replay.ServerPhotographer) {
+ continue;
+ }
+ // Leaves end - skip photographer
+
+ onlinePlayers.add(entityplayer1);
+ }
+ if (!onlinePlayers.isEmpty()) {
+ player.connection.send(ClientboundPlayerInfoUpdatePacket.createPlayerInitializing(onlinePlayers, player));
+ }
+
+ player.sentListPacket = true;
+ player.suppressTrackerForLogin = false;
+ ((ServerLevel)player.level()).getChunkSource().chunkMap.addEntity(player);
+
+ this.sendLevelInfo(player, worldserver1);
+
+ if (player.level() == worldserver1 && !worldserver1.players().contains(player)) {
+ worldserver1.addNewPlayer(player);
+ this.server.getCustomBossEvents().onPlayerConnect(player);
+ }
+
+ worldserver1 = player.level();
+ java.util.Iterator<net.minecraft.world.effect.MobEffectInstance> iterator = player.getActiveEffects().iterator();
+ while (iterator.hasNext()) {
+ MobEffectInstance mobeffect = iterator.next();
+ playerconnection.send(new ClientboundUpdateMobEffectPacket(player.getId(), mobeffect, false));
+ }
+
+ if (player.isDeadOrDying()) {
+ net.minecraft.core.Holder<net.minecraft.world.level.biome.Biome> plains = worldserver1.registryAccess().lookupOrThrow(net.minecraft.core.registries.Registries.BIOME)
+ .getOrThrow(net.minecraft.world.level.biome.Biomes.PLAINS);
+ player.connection.send(new net.minecraft.network.protocol.game.ClientboundLevelChunkWithLightPacket(
+ new net.minecraft.world.level.chunk.EmptyLevelChunk(worldserver1, player.chunkPosition(), plains),
+ worldserver1.getLightEngine(), null, null, false)
+ );
+ }
+ }
+ // Leaves end - replay mod api
+
public void placeNewPlayer(final Connection connection, final ServerPlayer player, final CommonListenerCookie cookie) {
player.isRealPlayer = true; // Paper
player.loginTime = System.currentTimeMillis(); // Paper - Replace OfflinePlayer#getLastPlayed
@@ -296,6 +416,7 @@ public abstract class PlayerList {
// player.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(player))); // CraftBukkit - replaced with loop below
@@ -515,6 +636,7 @@ public abstract class PlayerList {
}
protected void save(final ServerPlayer player) {
+ if (player instanceof org.leavesmc.leaves.replay.ServerPhotographer) return; // Leaves - skip photographer
if (!player.getBukkitEntity().isPersistent()) return; // CraftBukkit
player.lastSave = System.nanoTime(); // Folia - region threading - changed to nanoTime tracking
this.playerIo.save(player);
@@ -529,6 +651,47 @@ public abstract class PlayerList {
}
}
+ // Leaves start - replay mod api
+ public void removePhotographer(org.leavesmc.leaves.replay.ServerPhotographer entityplayer) {
+ ServerLevel worldserver = entityplayer.level();
+
+ 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 net.minecraft.world.scores.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);
+ // Leaves - guard name removal so a photographer sharing a real player's name won't evict the real player's mapping
+ final String nameKey = entityplayer.getScoreboardName().toLowerCase(java.util.Locale.ROOT);
+ if (this.playersByName.get(nameKey) == entityplayer) {
+ this.playersByName.remove(nameKey);
+ }
+ 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 end - replay mod api
+
public net.kyori.adventure.text.@Nullable Component remove(final ServerPlayer player) { // CraftBukkit - return string // Paper - return Component
// Paper start - Fix kick event leave message not being sent
return this.remove(player, net.kyori.adventure.text.Component.translatable("multiplayer.player.left", net.kyori.adventure.text.format.NamedTextColor.YELLOW, io.papermc.paper.configuration.GlobalConfiguration.get().messages.useDisplayNameInQuitMessage ? player.getBukkitEntity().displayName() : io.papermc.paper.adventure.PaperAdventure.asAdventure(player.getDisplayName())));
@@ -602,6 +765,7 @@ public abstract class PlayerList {
player.getBukkitEntity().packetProcessor.close(); // Folia - region threading
player.getAdvancements().stopListening();
this.players.remove(player);
+ this.realPlayers.remove(player); // Leaves - replay api
this.playersByName.remove(player.getScoreboardName().toLowerCase(java.util.Locale.ROOT)); // Spigot
this.server.getCustomBossEvents().onPlayerDisconnect(player);
UUID uuid = player.getUUID();
@@ -912,15 +1076,15 @@ public abstract class PlayerList {
}
public String[] getPlayerNamesArray() {
- List<ServerPlayer> players = new java.util.ArrayList<>(this.players); // Folia - region threading
- String[] names = new String[players.size() + this.server.getBotList().bots.size()]; // Leaves - fakeplayer support
+ List<ServerPlayer> players = new java.util.ArrayList<>(this.realPlayers); // Folia - region threading // Lophine - fakeplayer support
+ String[] names = new String[this.realPlayers.size() + this.server.getBotList().bots.size()]; // Leaves - fakeplayer support
- for (int i = 0; i < players.size(); i++) { // Folia - region threading
- names[i] = players.get(i).getGameProfile().name(); // Folia - region threading
+ for (int i = 0; i < this.realPlayers.size(); i++) { // Folia - region threading // Leaves - only real players
+ names[i] = this.realPlayers.get(i).getGameProfile().name(); // Folia - region threading // Leaves - only real players
}
// Leaves start - fakeplayer support
- for (int i = this.players.size(); i < names.length; ++i) {
- names[i] = this.server.getBotList().bots.get(i - this.players.size()).gameProfile.name();
+ for (int i = this.realPlayers.size(); i < names.length; ++i) { // Leaves - only real players
+ names[i] = this.server.getBotList().bots.get(i - this.realPlayers.size()).gameProfile.name(); // Leaves - only real players
}
// Leaves end - fakeplayer support
diff --git a/net/minecraft/world/entity/LivingEntity.java b/net/minecraft/world/entity/LivingEntity.java
index 8a8de7f4d62532709805ebde531dc71ad78fa5c9..167855ad90d0c168a6d49f078ee3409ab84d4bf3 100644
--- a/net/minecraft/world/entity/LivingEntity.java
+++ b/net/minecraft/world/entity/LivingEntity.java
@@ -261,7 +261,7 @@ public abstract class LivingEntity extends Entity implements Attackable, Waypoin
public int useItemRemaining;
protected int fallFlyTicks;
private long lastKineticHitFeedbackTime = -2147483648L;
- private BlockPos lastPos;
+ protected BlockPos lastPos;
private Optional<BlockPos> lastClimbablePos = Optional.empty();
private @Nullable DamageSource lastDamageSource;
private long lastDamageStamp = Long.MIN_VALUE; // Folia - region threading
@@ -0,0 +1,144 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Helvetica Volubi <suisuroru@blue-millennium.fun>
Date: Sun, 8 Mar 2026 23:56:06 +0800
Subject: [PATCH] Leaves: Creative fly no clip
Co-authored by: violetc <58360096+s-yh-china@users.noreply.github.com>
As a part of : Leaves (https://github.com/LeavesMC/Leaves/blob/da12a58e8d3bd3cd13b52a60676d5a1e03acd30a/leaves-server/minecraft-patches/features/0049-Creative-fly-no-clip.patch)
Licensed under: GPL-3.0 (https://www.gnu.org/licenses/gpl-3.0.html)
diff --git a/net/minecraft/world/entity/ExperienceOrb.java b/net/minecraft/world/entity/ExperienceOrb.java
index e302c6198d71782b524391329aa5c89d2b3e0ccf..2312803297321f66c9057bc54ce56dd17ecb6f1f 100644
--- a/net/minecraft/world/entity/ExperienceOrb.java
+++ b/net/minecraft/world/entity/ExperienceOrb.java
@@ -199,7 +199,7 @@ public class ExperienceOrb extends Entity {
Player prevTarget = this.followingPlayer; // CraftBukkit - store old target
if (this.followingPlayer == null || this.followingPlayer.isSpectator() || this.followingPlayer.distanceToSqr(this) > 64.0) {
Player nearestPlayer = this.level().getNearestPlayer(this, 8.0);
- if (nearestPlayer != null && !nearestPlayer.isSpectator() && !nearestPlayer.isDeadOrDying()) {
+ if (nearestPlayer != null && !nearestPlayer.isCreativeFlyOrSpectator() && !nearestPlayer.isDeadOrDying()) { // Leaves - creative no clip
this.followingPlayer = nearestPlayer;
} else {
this.followingPlayer = null;
diff --git a/net/minecraft/world/entity/player/Player.java b/net/minecraft/world/entity/player/Player.java
index 38689fa466a20c64f9916ee32a57a9fdb58d9403..7d514537ee38f9b9366692bc969ff503d883aa16 100644
--- a/net/minecraft/world/entity/player/Player.java
+++ b/net/minecraft/world/entity/player/Player.java
@@ -279,8 +279,8 @@ public abstract class Player extends Avatar implements ContainerUser {
if (this.cullTask != null) this.cullTask.requestCullSignal(); // Luminol - Ray tracing entity tracker
// Luminol end
- this.noPhysics = this.isSpectator();
- if (this.isSpectator() || this.isPassenger()) {
+ this.noPhysics = this.isCreativeFlyOrSpectator(); // Leaves - creative no clip
+ if (this.isCreativeFlyOrSpectator() || this.isPassenger()) { // Leaves - creative no clip
this.setOnGround(false);
}
@@ -406,7 +406,7 @@ public abstract class Player extends Avatar implements ContainerUser {
if (this.canPlayerFitWithinBlocksAndEntitiesWhen(Pose.SWIMMING)) {
Pose desiredPose = this.getDesiredPose();
Pose actualPose;
- if (this.isSpectator() || this.isPassenger() || this.canPlayerFitWithinBlocksAndEntitiesWhen(desiredPose)) {
+ if (this.isCreativeFlyOrSpectator() || this.isPassenger() || this.canPlayerFitWithinBlocksAndEntitiesWhen(desiredPose)) { // Leaves - creative no clip
actualPose = desiredPose;
} else if (this.canPlayerFitWithinBlocksAndEntitiesWhen(Pose.CROUCHING)) {
actualPose = Pose.CROUCHING;
@@ -535,7 +535,7 @@ public abstract class Player extends Avatar implements ContainerUser {
this.updateSwingTime();
this.yHeadRot = this.getYRot();
this.setSpeed((float)this.getAttributeValue(Attributes.MOVEMENT_SPEED));
- if (this.getHealth() > 0.0F && !this.isSpectator()) {
+ if (this.getHealth() > 0.0F && !this.isCreativeFlyOrSpectator()) { // Leaves - creative no clip
AABB pickupArea;
if (this.isPassenger() && !this.getVehicle().isRemoved()) {
pickupArea = this.getBoundingBox().minmax(this.getVehicle().getBoundingBox()).inflate(1.0, 0.0, 1.0);
@@ -1846,6 +1846,26 @@ public abstract class Player extends Avatar implements ContainerUser {
return this.gameMode() == GameType.SPECTATOR;
}
+ // Leaves start - creative no clip
+ public boolean isCreativeFlyOrSpectator() {
+ return isSpectator() || (fun.bm.lophine.config.modules.function.CreativeFlyNoClipConfig.enabled && isCreative() && getAbilities().flying);
+ }
+
+ public boolean canSpectatingPlace(final net.minecraft.world.level.LevelReader world, final BlockState state, final BlockPos pos, final net.minecraft.world.phys.shapes.CollisionContext context) {
+ if (this.isCreativeFlyOrSpectator()) {
+ net.minecraft.world.phys.shapes.VoxelShape voxelShape = state.getCollisionShape(world, pos, context);
+ return voxelShape.isEmpty() || world.isUnobstructed(this, voxelShape.move(pos.getX(), pos.getY(), pos.getZ()));
+ } else {
+ return world.isUnobstructed(state, pos, context);
+ }
+ }
+
+ @Override
+ public boolean isCollidable(final boolean ignoreClimbing) {
+ return !isCreativeFlyOrSpectator() && super.isCollidable(ignoreClimbing);
+ }
+ // Leaves end - creative no clip
+
@Override
public boolean isPickable() {
return !this.isSpectator() && super.isPickable();
diff --git a/net/minecraft/world/item/BlockItem.java b/net/minecraft/world/item/BlockItem.java
index 2e2a8af12f249ec5bd0935fd13f5acbd0a777b5f..06160c24f1ffbe1445284bc81598cabb3ad20477 100644
--- a/net/minecraft/world/item/BlockItem.java
+++ b/net/minecraft/world/item/BlockItem.java
@@ -158,8 +158,9 @@ public class BlockItem extends Item {
Player player = context.getPlayer();
// CraftBukkit start
Level world = context.getLevel(); // Paper - Cancel hit for vanished players
+ CollisionContext collisionContext = player == null ? CollisionContext.empty() : CollisionContext.placementContext(player); // Leaves - creative no clip
boolean canBuild = (!this.mustSurvive() || stateForPlacement.canSurvive(world, context.getClickedPos()))
- && world.checkEntityCollision(stateForPlacement, player, CollisionContext.placementContext(player), context.getClickedPos(), true); // Paper - Cancel hit for vanished players
+ && ((fun.bm.lophine.config.modules.function.CreativeFlyNoClipConfig.enabled && context.getPlayer() != null) ? context.getPlayer().canSpectatingPlace(world, stateForPlacement, context.getClickedPos(), collisionContext) : world.checkEntityCollision(stateForPlacement, player, collisionContext, context.getClickedPos(), true)); // Paper - Cancel hit for vanished players // Leaves - creative no clip
org.bukkit.entity.Player bukkitPlayer = (context.getPlayer() instanceof ServerPlayer) ? (org.bukkit.entity.Player) context.getPlayer().getBukkitEntity() : null;
org.bukkit.event.block.BlockCanBuildEvent event = new org.bukkit.event.block.BlockCanBuildEvent(
diff --git a/net/minecraft/world/item/StandingAndWallBlockItem.java b/net/minecraft/world/item/StandingAndWallBlockItem.java
index cb0a4a9865df69e5504a86de6c144bc4450bd19a..a40a98bb48bb14c49670ba55d2a2e8d7e9d0d178 100644
--- a/net/minecraft/world/item/StandingAndWallBlockItem.java
+++ b/net/minecraft/world/item/StandingAndWallBlockItem.java
@@ -44,7 +44,7 @@ public class StandingAndWallBlockItem extends BlockItem {
// return stateForPlacement != null && level.isUnobstructed(stateForPlacement, pos, CollisionContext.empty()) ? stateForPlacement : null;
// CraftBukkit start
if (stateForPlacement != null) {
- boolean defaultReturn = level.isUnobstructed(stateForPlacement, pos, CollisionContext.empty());
+ boolean defaultReturn = (fun.bm.lophine.config.modules.function.CreativeFlyNoClipConfig.enabled && context.getPlayer() != null) ? context.getPlayer().canSpectatingPlace(level, stateForPlacement, pos, CollisionContext.empty()) : level.isUnobstructed(stateForPlacement, pos, CollisionContext.empty()); // Leaves - creative no clip
org.bukkit.entity.Player player = (context.getPlayer() instanceof net.minecraft.server.level.ServerPlayer serverPlayer) ? serverPlayer.getBukkitEntity() : null;
org.bukkit.event.block.BlockCanBuildEvent event = new org.bukkit.event.block.BlockCanBuildEvent(org.bukkit.craftbukkit.block.CraftBlock.at(context.getLevel(), pos), player, stateForPlacement.asBlockData(), defaultReturn, org.bukkit.craftbukkit.CraftEquipmentSlot.getHand(context.getHand())); // Paper - Expose hand in BlockCanBuildEvent
diff --git a/net/minecraft/world/level/block/entity/ShulkerBoxBlockEntity.java b/net/minecraft/world/level/block/entity/ShulkerBoxBlockEntity.java
index 6205f52ecad24f80d21b924945c5ff06c6704aca..d84159df4d2a0379ffbb6bdb0807b4a02dbec71e 100644
--- a/net/minecraft/world/level/block/entity/ShulkerBoxBlockEntity.java
+++ b/net/minecraft/world/level/block/entity/ShulkerBoxBlockEntity.java
@@ -153,7 +153,7 @@ public class ShulkerBoxBlockEntity extends RandomizableContainerBlockEntity impl
List<Entity> entities = level.getEntities(null, aabb);
if (!entities.isEmpty()) {
for (Entity entity : entities) {
- if (entity.getPistonPushReaction() != PushReaction.IGNORE) {
+ if (entity.getPistonPushReaction() != PushReaction.IGNORE && !(entity instanceof net.minecraft.world.entity.player.Player player && player.isCreativeFlyOrSpectator())) { // Leaves - creative no clip
entity.move(
MoverType.SHULKER_BOX,
new Vec3(
diff --git a/net/minecraft/world/level/block/piston/PistonMovingBlockEntity.java b/net/minecraft/world/level/block/piston/PistonMovingBlockEntity.java
index 2ecb10fe97d15954e44c61247eabc8818eb16f34..22663ad55f669081b43644ad2cf1bd4aac48b745 100644
--- a/net/minecraft/world/level/block/piston/PistonMovingBlockEntity.java
+++ b/net/minecraft/world/level/block/piston/PistonMovingBlockEntity.java
@@ -168,7 +168,7 @@ public class PistonMovingBlockEntity extends BlockEntity {
dz = movement.getStepZ();
}
- entity.setDeltaMovement(dx, dy, dz);
+ if (!(entity instanceof net.minecraft.world.entity.player.Player player) || !player.isCreativeFlyOrSpectator()) entity.setDeltaMovement(dx, dy, dz); // Leaves - creative no clip
// Paper - EAR items stuck in slime pushed by a piston
entity.activatedTick = Math.max(entity.activatedTick, io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick() + 10); // Folia - region threading
entity.activatedImmunityTick = Math.max(entity.activatedImmunityTick, io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick() + 10); // Folia - region threading
@@ -204,6 +204,7 @@ public class PistonMovingBlockEntity extends BlockEntity {
}
private static void moveEntityByPiston(final Direction pistonDirection, final Entity entity, final double delta, final Direction movement) {
+ if (entity instanceof net.minecraft.world.entity.player.Player player && player.isCreativeFlyOrSpectator()) return; // Leaves - creative no clip
NOCLIP.set(pistonDirection);
Vec3 previousPos = entity.position();
entity.move(MoverType.PISTON, new Vec3(delta * movement.getStepX(), delta * movement.getStepY(), delta * movement.getStepZ()));
@@ -0,0 +1,95 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Helvetica Volubi <suisuroru@blue-millennium.fun>
Date: Mon, 27 Oct 2025 15:38:29 +0800
Subject: [PATCH] Leaves: Wool Hopper Counter
Co-authored by: violetc <58360096+s-yh-china@users.noreply.github.com>
As a part of : Leaves (https://github.com/LeavesMC/Leaves)
Licensed under: MIT
This patch is Powered by fabric-carpet(https://github.com/gnembon/fabric-carpet)
diff --git a/net/minecraft/world/level/block/entity/HopperBlockEntity.java b/net/minecraft/world/level/block/entity/HopperBlockEntity.java
index d036018ee64995ee754296014888ee70c0ea5ab3..aa782c6441cdce3eb39aefb28d40f3b1970d4e62 100644
--- a/net/minecraft/world/level/block/entity/HopperBlockEntity.java
+++ b/net/minecraft/world/level/block/entity/HopperBlockEntity.java
@@ -239,8 +239,30 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen
changed |= action.getAsBoolean(); // Paper - note: this is not a validator, it's what adds/sucks in items
}
+ // Leaves start - Wool hopper counter
+ if (fun.bm.lophine.config.modules.function.WoolHopperCounterConfig.unlimitedSpeed && org.leavesmc.leaves.util.HopperCounter.isEnabled()) {
+ net.minecraft.world.item.DyeColor woolColor = org.leavesmc.leaves.util.WoolUtils.getWoolColorAtPosition(level, entity.getBlockPos().relative(state.getValue(HopperBlock.FACING)));
+ if (woolColor != null) {
+ for (int i = 0; i < Short.MAX_VALUE; i++) {
+ changed |= suckInItems(level, entity);
+ if (!changed) {
+ break;
+ } else {
+ woolHopperCounter(level, pos, state, HopperBlockEntity.getContainerAt(level, pos));
+ }
+ }
+ }
+ }
+ // Leaves end - Wool hopper counter
+
if (changed) {
entity.setCooldown(level.spigotConfig.hopperTransfer); // Spigot
+ // Leaves start - Wool hopper counter
+ if (fun.bm.lophine.config.modules.function.WoolHopperCounterConfig.unlimitedSpeed && org.leavesmc.leaves.util.HopperCounter.isEnabled() && woolHopperCounter(level, pos, state, HopperBlockEntity.getContainerAt(level, pos))) {
+ entity.setCooldown(0);
+ return true;
+ }
+ // Leaves end - Wool hopper counter
setChanged(level, pos, state);
// Leaves start - Lithium Sleeping Block Entity
if (me.earthme.luminol.config.modules.optimizations.LeavesSleepingBlockEntityConfig.enabled
@@ -491,6 +513,13 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen
// Paper end - Perf: Optimize Hoppers
private static boolean ejectItems(final Level level, final BlockPos blockPos, final HopperBlockEntity self) {
+ // Leaves start - hopper counter
+ if (org.leavesmc.leaves.util.HopperCounter.isEnabled()) {
+ if (woolHopperCounter(level, blockPos, level.getBlockState(blockPos), HopperBlockEntity.getContainerAt(level, blockPos))) {
+ return true;
+ }
+ }
+ // Leaves end - hopper counter
Container container = me.earthme.luminol.config.modules.optimizations.LeavesSleepingBlockEntityConfig.enabled ? self.getInsertInventory(level) : getAttachedContainer(level, blockPos, self); // Leaves - Lithium Sleeping Block Entity
if (container == null) {
return false;
@@ -512,6 +541,26 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen
}
}
+ // Leaves start - hopper counter
+ private static boolean woolHopperCounter(Level level, BlockPos blockPos, BlockState state, @Nullable Container container) {
+ if (container == null) {
+ return false;
+ }
+ net.minecraft.world.item.DyeColor woolColor = org.leavesmc.leaves.util.WoolUtils.getWoolColorAtPosition(level, blockPos.relative(state.getValue(HopperBlock.FACING)));
+ if (woolColor != null) {
+ for (int i = 0; i < container.getContainerSize(); ++i) {
+ if (!container.getItem(i).isEmpty()) {
+ ItemStack itemstack = container.getItem(i);
+ org.leavesmc.leaves.util.HopperCounter.getCounter(woolColor).add(level.getServer(), itemstack);
+ container.setItem(i, ItemStack.EMPTY);
+ }
+ }
+ return true;
+ }
+ return false;
+ }
+ // Leaves end - hopper counter
+
private static int[] getSlots(final Container container, final Direction direction) {
if (container instanceof WorldlyContainer worldlyContainer) {
return worldlyContainer.getSlotsForFace(direction);
@@ -606,6 +655,7 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen
}
public static boolean addItem(final Container container, final ItemEntity entity) {
+ if (fun.bm.lophine.config.modules.function.WoolHopperCounterConfig.unlimitedSpeed && org.leavesmc.leaves.util.HopperCounter.isEnabled() && entity.isRemoved()) return false; // Leaves - Wool hopper counter
boolean changed = false;
// CraftBukkit start
if (org.bukkit.event.inventory.InventoryPickupItemEvent.getHandlerList().getRegisteredListeners().length > 0) { // Paper - optimize hoppers
@@ -0,0 +1,106 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Helvetica Volubi <suisuroru@blue-millennium.fun>
Date: Sun, 16 Nov 2025 15:55:47 +0800
Subject: [PATCH] Leaves: Replay Mod API
Co-authored by: violetc <58360096+s-yh-china@users.noreply.github.com>
As a part of : Leaves (https://github.com/LeavesMC/Leaves)
Licensed under: GPL-3.0 (https://www.gnu.org/licenses/gpl-3.0.html)
diff --git a/src/main/java/io/papermc/paper/plugin/manager/PaperEventManager.java b/src/main/java/io/papermc/paper/plugin/manager/PaperEventManager.java
index e1f456b31ffda9370b63e51f343847d6f9f62263..8a0f85241cc1530aacccc8b6ea24b9dec86fad66 100644
--- a/src/main/java/io/papermc/paper/plugin/manager/PaperEventManager.java
+++ b/src/main/java/io/papermc/paper/plugin/manager/PaperEventManager.java
@@ -57,7 +57,7 @@ class PaperEventManager {
}
// Leaves start - skip bot
- if (event instanceof org.bukkit.event.player.PlayerEvent playerEvent && playerEvent.getPlayer() instanceof org.leavesmc.leaves.entity.bot.Bot) {
+ if (event instanceof org.bukkit.event.player.PlayerEvent playerEvent && (playerEvent.getPlayer() instanceof org.leavesmc.leaves.entity.bot.Bot || playerEvent.getPlayer() instanceof org.leavesmc.leaves.entity.photographer.Photographer)) { // Leaves - and photographer
return;
}
// Leaves end - skip bot
diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java
index 054d9f4a09035771a329d6d561249d206b5dbee3..8fc75ef37f273433fb11cc1e0beee87484c3e18f 100644
--- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java
+++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java
@@ -304,6 +304,7 @@ public final class CraftServer implements Server {
public io.papermc.paper.SparksFly spark; // Paper - spark // Luminol - Force disable builtin spark
private final ServerConfiguration serverConfig = new PaperServerConfiguration();
private final org.leavesmc.leaves.entity.bot.CraftBotManager botManager; // Leaves
+ private final org.leavesmc.leaves.entity.photographer.CraftPhotographerManager photographerManager = new org.leavesmc.leaves.entity.photographer.CraftPhotographerManager(); // Leaves
// Paper start - Folia region threading API
private final io.papermc.paper.threadedregions.scheduler.FoliaRegionScheduler regionizedScheduler = new io.papermc.paper.threadedregions.scheduler.FoliaRegionScheduler(); // Folia - region threading
@@ -398,7 +399,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();
@@ -3128,4 +3129,11 @@ public final class CraftServer implements Server {
}
}
// Folia end - region TPS API
+
+ // Leaves start - replay mod api
+ @Override
+ public org.leavesmc.leaves.entity.photographer.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 42c2c6a05645e5aef26e9d3a91ad93563a297ca3..759d54f1f012461fc322d452bb26f7fd6e601039 100644
--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java
+++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java
@@ -128,6 +128,7 @@ public abstract class CraftEntity implements org.bukkit.entity.Entity {
}
if (entity instanceof org.leavesmc.leaves.bot.ServerBot bot) { return new org.leavesmc.leaves.entity.bot.CraftBot(server, bot); }
+ if (entity instanceof org.leavesmc.leaves.replay.ServerPhotographer photographer) { return new org.leavesmc.leaves.entity.photographer.CraftPhotographer(server, photographer); }
// Special case complex part, since there is no extra entity type for them
if (entity instanceof EnderDragonPart complexPart) {
diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java
index 47926ff2fba43bb83e2d9747420a526cce8ebe65..e72cd3c9919978a0ed91614d51d018bfb2cdb900 100644
--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java
+++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java
@@ -2086,7 +2086,7 @@ public class CraftPlayer extends CraftHumanEntity implements Player, PluginMessa
@Override
public boolean canSee(Player player) {
- return this.canSee((org.bukkit.entity.Entity) player);
+ return !(player instanceof org.leavesmc.leaves.entity.photographer.Photographer) && this.canSee((org.bukkit.entity.Entity) player); // Leaves - skip photographer
}
@Override
diff --git a/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java b/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java
index fa3e2bf2b31ca5173d71082e93eb7eaa4faa2816..bd3c4eb10489f13562b31f4b809e80b19717bc25 100644
--- a/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java
+++ b/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java
@@ -347,7 +347,7 @@ public final class CraftMagicNumbers implements UnsafeValues {
Bukkit.getLogger().log(Level.SEVERE, "Error saving advancement " + key, ex);
}
- MinecraftServer.getServer().getPlayerList().getPlayers().forEach(player -> {
+ net.minecraft.server.MinecraftServer.getServer().getPlayerList().realPlayers.forEach(player -> { // Leaves - only real players
player.getAdvancements().reload(MinecraftServer.getServer().getAdvancements());
player.getAdvancements().flushDirty(player, false);
});
diff --git a/src/main/java/org/bukkit/craftbukkit/util/LazyPlayerSet.java b/src/main/java/org/bukkit/craftbukkit/util/LazyPlayerSet.java
index 25aae550dcdcef2df268d0dd99bdcc9bbd49fcf8..83af50de50a03e164d572f3c3466b6d0b42ed138 100644
--- a/src/main/java/org/bukkit/craftbukkit/util/LazyPlayerSet.java
+++ b/src/main/java/org/bukkit/craftbukkit/util/LazyPlayerSet.java
@@ -23,7 +23,7 @@ public class LazyPlayerSet extends LazyHashSet<Player> {
}
public static Set<Player> makePlayerSet(final MinecraftServer server) {
- List<ServerPlayer> players = server.getPlayerList().players;
+ List<ServerPlayer> players = server.getPlayerList().realPlayers; // Leaves - only real players
Set<Player> reference = new HashSet<>(players.size());
for (ServerPlayer player : players) {
reference.add(player.getBukkitEntity());
@@ -0,0 +1,41 @@
package fun.bm.lophine.command.counter;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import fun.bm.lophine.command.counter.sub.DisplayCommand;
import fun.bm.lophine.command.counter.sub.ResetCommand;
import fun.bm.lophine.command.counter.sub.ToggleCommand;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.JoinConfiguration;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.NotNull;
import org.leavesmc.leaves.command.CommandContext;
import org.leavesmc.leaves.command.RootNode;
import org.leavesmc.leaves.util.HopperCounter;
public class CounterCommand extends RootNode {
private final String PERM_BASE;
public CounterCommand() {
super("counter", "lophine.commands.counter");
this.PERM_BASE = "lophine.commands.counter";
children(
new ToggleCommand(this),
new ResetCommand(this),
new DisplayCommand(this)
);
}
@Override
protected boolean execute(@NotNull CommandContext context) throws CommandSyntaxException {
context.getSender().sendMessage(Component.join(JoinConfiguration.noSeparators(),
Component.text("Hopper Counter: ", NamedTextColor.GRAY),
Component.text(HopperCounter.isEnabled(), HopperCounter.isEnabled() ? NamedTextColor.AQUA : NamedTextColor.GRAY)
));
return true;
}
public boolean hasPermission(@NotNull CommandSender sender, String... subcommand) {
return hasPermission(PERM_BASE, sender, subcommand);
}
}
@@ -0,0 +1,24 @@
package fun.bm.lophine.command.counter;
import io.papermc.paper.command.brigadier.CommandSourceStack;
import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.NotNull;
import org.leavesmc.leaves.command.LiteralNode;
public class CounterSubCommand extends LiteralNode {
protected final CounterCommand parent;
protected CounterSubCommand(String name, CounterCommand parent) {
super(name);
this.parent = parent;
}
@Override
public boolean requires(@NotNull CommandSourceStack source) {
return hasPermission(source.getSender());
}
protected boolean hasPermission(CommandSender sender) {
return parent.hasPermission(sender, this.name);
}
}
@@ -0,0 +1,91 @@
package fun.bm.lophine.command.counter.sub;
import com.mojang.brigadier.arguments.StringArgumentType;
import com.mojang.brigadier.suggestion.Suggestions;
import com.mojang.brigadier.suggestion.SuggestionsBuilder;
import fun.bm.lophine.command.counter.CounterCommand;
import fun.bm.lophine.command.counter.CounterSubCommand;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import net.minecraft.server.MinecraftServer;
import net.minecraft.world.item.DyeColor;
import org.jetbrains.annotations.NotNull;
import org.leavesmc.leaves.command.ArgumentNode;
import org.leavesmc.leaves.command.CommandContext;
import org.leavesmc.leaves.util.HopperCounter;
import java.util.concurrent.CompletableFuture;
public class DisplayCommand extends CounterSubCommand {
public DisplayCommand(CounterCommand parent) {
super("display", parent);
children(
DyeColorArg::new
);
}
public static void displayCounter(CommandContext context, @NotNull HopperCounter counter, boolean realTime) {
for (Component component : counter.format(MinecraftServer.getServer(), realTime)) {
context.getSender().sendMessage(component);
}
}
private static class DyeColorArg extends ArgumentNode<String> {
protected DyeColorArg() {
super("color", StringArgumentType.string());
children(
TimeArg::new
);
}
@Override
protected boolean execute(@NotNull CommandContext context) {
String color0 = context.getArgument(DyeColorArg.class);
DyeColor color = DyeColor.byName(color0, null);
if (color == null) return true;
HopperCounter counter = HopperCounter.getCounter(color);
displayCounter(context, counter, false);
return true;
}
@Override
protected CompletableFuture<Suggestions> getSuggestions(@NotNull CommandContext context, @NotNull SuggestionsBuilder builder) {
String path = context.getArgumentOrDefault(DyeColorArg.class, "");
for (DyeColor value : DyeColor.values()) {
String color = value.getName();
if (color.startsWith(path)) {
builder.suggest(color);
}
}
return builder.buildFuture();
}
private static class TimeArg extends ArgumentNode<String> {
protected TimeArg() {
super("time", StringArgumentType.string());
}
@Override
protected boolean execute(@NotNull CommandContext context) {
String color0 = context.getArgument(DyeColorArg.class);
DyeColor color = DyeColor.byName(color0, null);
if (color == null) return true;
HopperCounter counter = HopperCounter.getCounter(color);
String timeType = context.getArgument(TimeArg.class);
switch (timeType) {
case "realtime" -> displayCounter(context, counter, true);
case "gametick" -> displayCounter(context, counter, false);
default ->
context.getSender().sendMessage(Component.text("Invalid time type: " + timeType, NamedTextColor.RED));
}
return true;
}
protected CompletableFuture<Suggestions> getSuggestions(@NotNull CommandContext context, @NotNull SuggestionsBuilder builder) {
builder.suggest("realtime");
builder.suggest("gametick");
return builder.buildFuture();
}
}
}
}
@@ -0,0 +1,76 @@
package fun.bm.lophine.command.counter.sub;
import com.mojang.brigadier.arguments.StringArgumentType;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import com.mojang.brigadier.suggestion.Suggestions;
import com.mojang.brigadier.suggestion.SuggestionsBuilder;
import fun.bm.lophine.command.counter.CounterCommand;
import fun.bm.lophine.command.counter.CounterSubCommand;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.JoinConfiguration;
import net.kyori.adventure.text.format.TextColor;
import net.minecraft.server.MinecraftServer;
import net.minecraft.world.item.DyeColor;
import org.jetbrains.annotations.NotNull;
import org.leavesmc.leaves.command.ArgumentNode;
import org.leavesmc.leaves.command.CommandContext;
import org.leavesmc.leaves.util.HopperCounter;
import java.util.concurrent.CompletableFuture;
public class ResetCommand extends CounterSubCommand {
public ResetCommand(CounterCommand parent) {
super("reset", parent);
children(
DyeColorArg::new
);
}
@Override
protected boolean execute(@NotNull CommandContext context) throws CommandSyntaxException {
HopperCounter.resetAll(MinecraftServer.getServer(), false);
context.getSender().sendMessage(Component.text("Restarted all counters."));
return true;
}
private static class DyeColorArg extends ArgumentNode<String> {
protected DyeColorArg() {
super("color", StringArgumentType.string());
}
@Override
protected boolean execute(@NotNull CommandContext context) {
String color0 = context.getArgument(DyeColorArg.class);
if (color0.equals("all")) {
HopperCounter.resetAll(MinecraftServer.getServer(), false);
context.getSender().sendMessage(Component.text("Restarted all counters."));
return true;
}
DyeColor color = DyeColor.byName(color0, null);
if (color == null) return true;
HopperCounter counter = HopperCounter.getCounter(color);
counter.reset(MinecraftServer.getServer());
context.getSender().sendMessage(Component.join(JoinConfiguration.noSeparators(),
Component.text("Restarted "),
Component.text(color.getName(), TextColor.color(color.getTextColor())),
Component.text(" counter.")
));
return true;
}
@Override
protected CompletableFuture<Suggestions> getSuggestions(@NotNull CommandContext context, @NotNull SuggestionsBuilder builder) {
String path = context.getArgumentOrDefault(DyeColorArg.class, "");
if ("all".startsWith(path)) {
builder.suggest("all");
}
for (DyeColor value : DyeColor.values()) {
String color = value.getName();
if (color.startsWith(path)) {
builder.suggest(color);
}
}
return builder.buildFuture();
}
}
}
@@ -0,0 +1,55 @@
package fun.bm.lophine.command.counter.sub;
import com.mojang.brigadier.arguments.BoolArgumentType;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import fun.bm.lophine.command.counter.CounterCommand;
import fun.bm.lophine.command.counter.CounterSubCommand;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import org.jetbrains.annotations.NotNull;
import org.leavesmc.leaves.command.ArgumentNode;
import org.leavesmc.leaves.command.CommandContext;
import org.leavesmc.leaves.util.HopperCounter;
public class ToggleCommand extends CounterSubCommand {
public ToggleCommand(CounterCommand parent) {
super("toggle", parent);
children(
BooleanArg::new
);
}
@Override
protected boolean execute(@NotNull CommandContext context) throws CommandSyntaxException {
if (!HopperCounter.isEnabled()) {
HopperCounter.setEnabled(true);
context.getSender().sendMessage(Component.text("Hopper Counter now is enabled.", NamedTextColor.AQUA));
} else {
HopperCounter.setEnabled(false);
context.getSender().sendMessage(Component.text("Hopper Counter now is disabled.", NamedTextColor.RED));
}
return true;
}
private static class BooleanArg extends ArgumentNode<Boolean> {
protected BooleanArg() {
super("enabled", BoolArgumentType.bool());
}
@Override
protected boolean execute(@NotNull CommandContext context) {
boolean enabled = context.getArgument(BooleanArg.class);
if (enabled == HopperCounter.isEnabled()) {
context.getSender().sendMessage(Component.text("Hopper Counter is already " + (enabled ? "enabled" : "disabled") + ".", NamedTextColor.GRAY));
} else {
HopperCounter.setEnabled(enabled);
if (enabled) {
context.getSender().sendMessage(Component.text("Hopper Counter now is enabled.", NamedTextColor.AQUA));
} else {
context.getSender().sendMessage(Component.text("Hopper Counter now is disabled.", NamedTextColor.RED));
}
}
return true;
}
}
}
@@ -0,0 +1,25 @@
package fun.bm.lophine.config.modules.function;
import com.electronwill.nightconfig.core.file.CommentedFileConfig;
import me.earthme.luminol.config.IConfigModule;
import me.earthme.luminol.config.flags.ConfigClassInfo;
import me.earthme.luminol.config.flags.ConfigInfo;
import me.earthme.luminol.enums.EnumConfigCategory;
import org.jetbrains.annotations.Nullable;
import org.leavesmc.leaves.protocol.CarpetServerProtocol;
import java.util.Set;
@ConfigClassInfo(name = "creative_fly_no_clip", category = EnumConfigCategory.FUNCTION)
public class CreativeFlyNoClipConfig implements IConfigModule {
@ConfigInfo(name = "enabled", comments = """
Whether to enable creative fly no clip.
When enabled, players in creative mode will not collide with blocks while flying.
This allows them to pass through blocks without obstruction.""")
public static boolean enabled = false;
@Override
public void onLoaded(CommentedFileConfig configInstance, @Nullable Set<Exception> e) {
CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpet", "creativeNoClip", enabled));
}
}
@@ -0,0 +1,34 @@
package fun.bm.lophine.config.modules.function;
import com.electronwill.nightconfig.core.file.CommentedFileConfig;
import fun.bm.lophine.utils.RandomProfilePool;
import me.earthme.luminol.config.IConfigModule;
import me.earthme.luminol.config.flags.ConfigClassInfo;
import me.earthme.luminol.config.flags.ConfigInfo;
import me.earthme.luminol.config.flags.HotReloadUnsupported;
import me.earthme.luminol.enums.EnumConfigCategory;
import org.jetbrains.annotations.Nullable;
import java.util.Set;
@ConfigClassInfo(category = EnumConfigCategory.FUNCTION, name = "replay-api")
public class ReplayAPIConfig implements IConfigModule {
@HotReloadUnsupported
@ConfigInfo(name = "enable-cache")
public static boolean enableCache = true;
@HotReloadUnsupported
@ConfigInfo(name = "cache-photographer-time", comments = """
Time to cache photographer profile(in seconds)""")
public static int cachePhotographerTime = 3600;
@HotReloadUnsupported
@ConfigInfo(name = "cache-photographer-size", comments = """
Maximum size of cache photographer profile""")
public static int cachePhotographerSize = 100;
@Override
public void onLoaded(CommentedFileConfig configInstance, @Nullable Set<Exception> e) {
RandomProfilePool.init();
}
}
@@ -0,0 +1,41 @@
package fun.bm.lophine.config.modules.function;
import com.electronwill.nightconfig.core.file.CommentedFileConfig;
import fun.bm.lophine.command.counter.CounterCommand;
import me.earthme.luminol.config.IConfigModule;
import me.earthme.luminol.config.flags.ConfigClassInfo;
import me.earthme.luminol.config.flags.ConfigInfo;
import me.earthme.luminol.config.flags.DoNotLoad;
import me.earthme.luminol.enums.EnumConfigCategory;
import org.jetbrains.annotations.Nullable;
import java.util.Set;
@ConfigClassInfo(category = EnumConfigCategory.FUNCTION, name = "wool-hopper-counter")
public class WoolHopperCounterConfig implements IConfigModule {
@ConfigInfo(name = "enabled")
public static boolean enabled = false;
@ConfigInfo(name = "unlimited-speed")
public static boolean unlimitedSpeed = false;
@DoNotLoad
private static CounterCommand counterCommand = null;
@Override
public void onLoaded(CommentedFileConfig configInstance, @Nullable Set<Exception> exs) {
if (enabled) {
if (counterCommand == null) {
counterCommand = new CounterCommand();
}
counterCommand.register();
}
}
@Override
public void onUnloaded(CommentedFileConfig configInstance) {
if (counterCommand != null) {
counterCommand.unregister();
}
}
}
@@ -0,0 +1,59 @@
package fun.bm.lophine.utils;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.mojang.authlib.GameProfile;
import fun.bm.lophine.config.modules.function.ReplayAPIConfig;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
public class RandomProfilePool {
private static int lastUsedId = 0;
private static final Object lock = new Object();
private static Cache<Integer, GameProfile> cache;
private static final Object lockCache = new Object();
public static void init() {
if (ReplayAPIConfig.enableCache) {
cache = CacheBuilder.newBuilder()
.maximumSize(ReplayAPIConfig.cachePhotographerSize)
.expireAfterWrite(ReplayAPIConfig.cachePhotographerTime, TimeUnit.SECONDS)
.build();
}
}
public static GameProfile getRandomProfile(String id) {
if (ReplayAPIConfig.enableCache) {
synchronized (lockCache) {
for (Map.Entry<Integer, GameProfile> entry : cache.asMap().entrySet()) {
int key = entry.getKey();
cache.invalidate(key);
GameProfile gp = entry.getValue();
return new GameProfile(gp.id(), id, gp.properties());
}
}
}
return new GameProfile(UUID.randomUUID(), id);
}
public static void putProfile(GameProfile profile) {
if (ReplayAPIConfig.enableCache) {
synchronized (lockCache) {
cache.put(getNextId(), profile);
}
}
}
private static int getNextId() {
synchronized (lock) {
int newId = lastUsedId + 1;
while (cache.getIfPresent(newId) != null) {
newId++;
}
lastUsedId = newId;
return newId;
}
}
}
@@ -0,0 +1,90 @@
/*
* This file is part of Leaves (https://github.com/LeavesMC/Leaves)
*
* Leaves is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Leaves is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Leaves. If not, see <https://www.gnu.org/licenses/>.
*/
package org.leavesmc.leaves.entity.photographer;
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 org.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.stopRecording(async, true);
}
@Override
public void stopRecording(boolean async, boolean save) {
this.getHandle().remove(async, save);
}
@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() + '}';
}
}
@@ -0,0 +1,99 @@
/*
* This file is part of Leaves (https://github.com/LeavesMC/Leaves)
*
* Leaves is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Leaves is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Leaves. If not, see <https://www.gnu.org/licenses/>.
*/
package org.leavesmc.leaves.entity.photographer;
import com.google.common.collect.Lists;
import org.bukkit.Location;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.leavesmc.leaves.replay.BukkitRecorderOption;
import org.leavesmc.leaves.replay.RecorderOption;
import org.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;
}
}
@@ -0,0 +1,63 @@
/*
* This file is part of Leaves (https://github.com/LeavesMC/Leaves)
*
* Leaves is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Leaves is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Leaves. If not, see <https://www.gnu.org/licenses/>.
*/
package org.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);
}
}
@@ -0,0 +1,58 @@
/*
* This file is part of Leaves (https://github.com/LeavesMC/Leaves)
*
* Leaves is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Leaves is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Leaves. If not, see <https://www.gnu.org/licenses/>.
*/
package org.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 = "Lophine";
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<>();
public RecordMetaData copy() {
RecordMetaData ret = new RecordMetaData();
synchronized (this) {
ret.singleplayer = this.singleplayer;
ret.serverName = this.serverName;
ret.duration = this.duration;
ret.date = this.date;
ret.mcversion = this.mcversion;
ret.fileFormat = this.fileFormat;
ret.fileFormatVersion = this.fileFormatVersion;
ret.protocol = this.protocol;
ret.generator = this.generator;
ret.selfId = this.selfId;
ret.players = new HashSet<>(this.players);
}
return ret;
}
}
@@ -0,0 +1,287 @@
/*
* This file is part of Leaves (https://github.com/LeavesMC/Leaves)
*
* Leaves is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Leaves is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Leaves. If not, see <https://www.gnu.org/licenses/>.
*/
package org.leavesmc.leaves.replay;
import com.mojang.logging.LogUtils;
import com.mojang.serialization.DynamicOps;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.local.LocalChannel;
import net.minecraft.SharedConstants;
import net.minecraft.core.LayeredRegistryAccess;
import net.minecraft.core.RegistrySynchronization;
import net.minecraft.nbt.NbtOps;
import net.minecraft.nbt.Tag;
import net.minecraft.network.Connection;
import net.minecraft.network.ConnectionProtocol;
import net.minecraft.network.protocol.BundlePacket;
import net.minecraft.network.protocol.Packet;
import net.minecraft.network.protocol.PacketFlow;
import net.minecraft.network.protocol.common.*;
import net.minecraft.network.protocol.common.custom.BrandPayload;
import net.minecraft.network.protocol.configuration.ClientboundFinishConfigurationPacket;
import net.minecraft.network.protocol.configuration.ClientboundRegistryDataPacket;
import net.minecraft.network.protocol.configuration.ClientboundSelectKnownPacks;
import net.minecraft.network.protocol.configuration.ClientboundUpdateEnabledFeaturesPacket;
import net.minecraft.network.protocol.game.*;
import net.minecraft.network.protocol.login.ClientboundLoginFinishedPacket;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.RegistryLayer;
import net.minecraft.server.packs.repository.KnownPack;
import net.minecraft.tags.TagNetworkSerialization;
import net.minecraft.world.entity.EntityType;
import net.minecraft.world.entity.PositionMoveRotation;
import net.minecraft.world.flag.FeatureFlags;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import java.io.File;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicBoolean;
public class Recorder extends Connection {
public static final Logger LOGGER = LogUtils.getClassLogger();
public final ExecutorService saveService = Executors.newSingleThreadExecutor();
private final ReplayFile replayFile;
private final ServerPhotographer photographer;
private final RecorderOption recorderOption;
private final RecordMetaData metaData;
private final AtomicBoolean isSaving = new AtomicBoolean(false);
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 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, saveService);
this.channel = new LocalChannel();
}
public void start() {
startTime = System.currentTimeMillis();
metaData.singleplayer = false;
metaData.serverName = recorderOption.serverName;
metaData.date = startTime;
metaData.mcversion = SharedConstants.getCurrentVersion().name();
// TODO start event
this.savePacket(new ClientboundLoginFinishedPacket(photographer.getGameProfile()), ConnectionProtocol.LOGIN);
this.startConfiguration();
savePacket(ClientboundPlayerPositionPacket.of(photographer.getId(), PositionMoveRotation.of(photographer), Collections.emptySet()));
if (recorderOption.forceWeather != null) {
setWeather(recorderOption.forceWeather);
}
}
public void startConfiguration() {
this.state = ConnectionProtocol.CONFIGURATION;
MinecraftServer server = MinecraftServer.getServer();
this.savePacket(new ClientboundCustomPayloadPacket(new BrandPayload(server.getServerModName())), ConnectionProtocol.CONFIGURATION);
this.savePacket(new ClientboundServerLinksPacket(server.serverLinks().untrust()), ConnectionProtocol.CONFIGURATION);
this.savePacket(new ClientboundUpdateEnabledFeaturesPacket(FeatureFlags.REGISTRY.toNames(server.getWorldData().enabledFeatures())), ConnectionProtocol.CONFIGURATION);
List<KnownPack> knownPackslist = server.getResourceManager().listPacks().flatMap((iresourcepack) -> iresourcepack.location().knownPackInfo().stream()).toList();
this.savePacket(new ClientboundSelectKnownPacks(knownPackslist), ConnectionProtocol.CONFIGURATION);
server.getServerResourcePack().ifPresent((info) -> this.savePacket(new ClientboundResourcePackPushPacket(
info.id(), info.url(), info.hash(), info.isRequired(), Optional.ofNullable(info.prompt())
)));
LayeredRegistryAccess<RegistryLayer> layeredregistryaccess = server.registries();
DynamicOps<Tag> dynamicOps = layeredregistryaccess.compositeAccess().createSerializationContext(NbtOps.INSTANCE);
RegistrySynchronization.packRegistries(dynamicOps, layeredregistryaccess.getAccessFrom(RegistryLayer.WORLDGEN), Set.copyOf(knownPackslist),
(key, entries) ->
this.savePacket(new ClientboundRegistryDataPacket(key, entries), ConnectionProtocol.CONFIGURATION)
);
this.savePacket(new ClientboundUpdateTagsPacket(TagNetworkSerialization.serializeTagsToNetwork(layeredregistryaccess)), ConnectionProtocol.CONFIGURATION);
this.savePacket(ClientboundFinishConfigurationPacket.INSTANCE, ConnectionProtocol.CONFIGURATION);
state = ConnectionProtocol.PLAY;
}
@Override
public void flushChannel() {
}
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 ChannelFutureListener callbacks, boolean flush) {
if (stopped) {
return;
}
switch (packet) {
case BundlePacket<?> packet1 -> {
packet1.subPackets().forEach(subPacket -> send(subPacket, null));
return;
}
case ClientboundAddEntityPacket packet1 -> {
if (packet1.getType() == EntityType.PLAYER) {
synchronized (metaData) {
metaData.players.add(packet1.getUUID());
}
saveMetadata();
}
}
case ClientboundDisconnectPacket ignored -> {
return;
}
case ClientboundTrackedWaypointPacket ignored -> {
return;
}
default -> {
}
}
if (recorderOption.forceDayTime != -1 && packet instanceof ClientboundSetTimePacket packet1) {
// Leaves - Paper 26.1: SetTimePacket is now (gameTime, Map<Holder<WorldClock>, ClockNetworkState>).
// Replace each world clock with a frozen state at forceDayTime.
packet = new ClientboundSetTimePacket(
packet1.gameTime(),
net.minecraft.util.Util.mapValues(
packet1.clockUpdates(),
state -> new net.minecraft.world.clock.ClockNetworkState(recorderOption.forceDayTime, 0.0F, 0.0F)
)
);
}
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.execute(() -> {
try {
replayFile.saveMetaData(metaData);
} catch (IOException e) {
LOGGER.error("Error saving metadata", e);
}
});
}
private void savePacket(Packet<?> packet) {
this.savePacket(packet, state);
}
private void savePacket(Packet<?> packet, final ConnectionProtocol protocol) {
final long timestamp = getCurrentTimeAndUpdate();
try {
replayFile.savePacket(timestamp, packet, protocol);
} catch (Exception e) {
LOGGER.error("Error saving packet on thread {}. Are you using some plugin that modify data asynchronously?", Thread.currentThread(), e);
}
}
public boolean isSaved() {
return isSaved;
}
public CompletableFuture<Void> saveRecording(File dest, boolean save) {
if (!isSaving.compareAndSet(false, true)) {
LOGGER.error("saveRecording() called twice");
return CompletableFuture.failedFuture(new IllegalStateException("saveRecording() called twice"));
}
isSaved = true;
metaData.duration = (int) lastPacket;
return CompletableFuture.runAsync(() -> {
try {
replayFile.saveMetaData(metaData);
if (save) {
replayFile.closeAndSave(dest);
} else {
replayFile.closeNotSave();
}
} catch (IOException e) {
throw new CompletionException(e);
}
}, saveService);
}
}
@@ -0,0 +1,74 @@
/*
* This file is part of Leaves (https://github.com/LeavesMC/Leaves)
*
* Leaves is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Leaves is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Leaves. If not, see <https://www.gnu.org/licenses/>.
*/
package org.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 = "Lophine";
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;
RecordWeather(Packet<?>... packets) {
this.packets = List.of(packets);
}
public List<Packet<?>> getPackets() {
return packets;
}
}
}
@@ -0,0 +1,218 @@
/*
* This file is part of Leaves (https://github.com/LeavesMC/Leaves)
*
* Leaves is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Leaves is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Leaves. If not, see <https://www.gnu.org/licenses/>.
*/
package org.leavesmc.leaves.replay;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufUtil;
import io.netty.buffer.Unpooled;
import net.minecraft.SharedConstants;
import net.minecraft.network.ConnectionProtocol;
import net.minecraft.network.ProtocolInfo;
import net.minecraft.network.RegistryFriendlyByteBuf;
import net.minecraft.network.protocol.Packet;
import net.minecraft.network.protocol.configuration.ConfigurationProtocols;
import net.minecraft.network.protocol.game.GameProtocols;
import net.minecraft.network.protocol.login.LoginProtocols;
import net.minecraft.network.protocol.status.StatusProtocols;
import net.minecraft.server.MinecraftServer;
import org.jetbrains.annotations.NotNull;
import org.leavesmc.leaves.protocol.core.ProtocolUtils;
import org.leavesmc.leaves.util.UUIDSerializer;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ExecutorService;
import java.util.zip.CRC32;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import static org.leavesmc.leaves.replay.Recorder.LOGGER;
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;
private final Map<ConnectionProtocol, ProtocolInfo<?>> protocols;
private final ExecutorService saveService;
public ReplayFile(@NotNull File name, ExecutorService saveService) throws IOException {
this.saveService = saveService;
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);
this.metaFile = new File(tmpDir, META_FILE);
this.markerFile = new File(tmpDir, MARKER_FILE);
this.packetStream = new DataOutputStream(new DigestOutputStream(new BufferedOutputStream(new FileOutputStream(packetFile)), crc32));
this.protocols = Map.of(
ConnectionProtocol.STATUS, StatusProtocols.CLIENTBOUND,
ConnectionProtocol.LOGIN, LoginProtocols.CLIENTBOUND,
ConnectionProtocol.CONFIGURATION, ConfigurationProtocols.CLIENTBOUND,
ConnectionProtocol.PLAY, GameProtocols.CLIENTBOUND_TEMPLATE.bind(RegistryFriendlyByteBuf.decorator(MinecraftServer.getServer().registryAccess()))
);
}
@SuppressWarnings({"rawtypes", "unchecked"})
private byte @NotNull [] getPacketBytes(Packet packet, ConnectionProtocol state) {
ProtocolInfo<?> protocol = this.protocols.get(state);
if (protocol == null) {
throw new IllegalArgumentException("Unknown protocol state " + state);
}
ByteBuf buf = Unpooled.buffer();
protocol.codec().encode(buf, packet);
buf.readerIndex(0);
byte[] ret = ByteBufUtil.getBytes(buf);
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().protocolVersion();
data.generator = ProtocolUtils.buildProtocolVersion("replay");
RecordMetaData dataCopy = data.copy();
try (Writer writer = new OutputStreamWriter(new FileOutputStream(metaFile), StandardCharsets.UTF_8)) {
writer.write(META_GSON.toJson(dataCopy));
}
}
public void savePacket(long timestamp, Packet<?> packet, ConnectionProtocol protocol) {
byte[] data = getPacketBytes(packet, protocol);
saveService.execute(() -> {
try {
packetStream.writeInt((int) timestamp);
packetStream.writeInt(data.length);
packetStream.write(data);
} catch (Exception e) {
LOGGER.error("Error saving packet", e);
}
});
}
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());
}
public synchronized void closeNotSave() throws IOException {
packetStream.close();
String[] files = tmpDir.list();
if (files == null) {
return;
}
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();
}
}
@@ -0,0 +1,56 @@
/*
* This file is part of Leaves (https://github.com/LeavesMC/Leaves)
*
* Leaves is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Leaves is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Leaves. If not, see <https://www.gnu.org/licenses/>.
*/
package org.leavesmc.leaves.replay;
import com.google.gson.*;
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;
}
}
}
@@ -0,0 +1,264 @@
/*
* This file is part of Leaves (https://github.com/LeavesMC/Leaves)
*
* Leaves is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Leaves is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Leaves. If not, see <https://www.gnu.org/licenses/>.
*/
package org.leavesmc.leaves.replay;
import ca.spottedleaf.moonrise.common.util.TickThread;
import com.mojang.authlib.GameProfile;
import com.mojang.logging.LogUtils;
import fun.bm.lophine.utils.RandomProfilePool;
import io.papermc.paper.threadedregions.RegionizedServer;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ClientInformation;
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.phys.Vec3;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.craftbukkit.CraftWorld;
import org.jetbrains.annotations.NotNull;
import org.leavesmc.leaves.bot.BotStatsCounter;
import org.leavesmc.leaves.entity.photographer.CraftPhotographer;
import org.leavesmc.leaves.entity.photographer.Photographer;
import org.slf4j.Logger;
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;
public class ServerPhotographer extends ServerPlayer {
private static final Logger LOGGER = LogUtils.getClassLogger();
private static final List<ServerPhotographer> photographers = new CopyOnWriteArrayList<>();
public PhotographerCreateState createState;
private ServerPlayer followPlayer;
private Recorder recorder;
private File saveFile;
private Vec3 lastPosVec3;
private final ServerStatsCounter stats;
private ServerPhotographer(MinecraftServer server, ServerLevel world, GameProfile profile) {
super(server, world, profile, ClientInformation.createDefault());
this.gameMode = new ServerPhotographerGameMode(this);
this.followPlayer = null;
this.stats = new BotStatsCounter(server);
this.lastPosVec3 = this.position();
}
public static ServerPhotographer createPhotographer(@NotNull PhotographerCreateState state) throws IOException {
if (!isCreateLegal(state.id)) {
throw new IllegalArgumentException(state.id + " is a invalid photographer id");
}
MinecraftServer server = MinecraftServer.getServer();
ServerLevel world = ((CraftWorld) state.loc.getWorld()).getHandle();
GameProfile profile = RandomProfilePool.getRandomProfile(state.id); // Lophine - add cache
ServerPhotographer photographer = new ServerPhotographer(server, world, profile);
photographer.absSnapTo(state.loc.x(), state.loc.y(), state.loc.z(), state.loc.getYaw(), state.loc.getPitch());
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();
if (TickThread.isTickThreadFor(world, state.loc.x(), state.loc.z())) {
placePhotographer(server, photographer, world, state);
} else {
RegionizedServer.getInstance().taskQueue.queueTickTaskQueue(
world, state.loc.blockX() >> 4, state.loc.blockZ() >> 4,
() -> placePhotographer(server, photographer, world, state));
}
photographers.add(photographer);
// TODO record distance
return photographer;
}
private static void placePhotographer(MinecraftServer server, ServerPhotographer photographer, ServerLevel world, @NotNull PhotographerCreateState state) {
server.getPlayerList().placeNewPhotographer(photographer.recorder, photographer, world);
photographer.level().chunkSource.move(photographer);
photographer.setInvisible(true);
LOGGER.info("Photographer {} created", state.id);
}
@Override
public void tick() {
this.lastPos = this.blockPosition();
super.tick();
if (this.tickCount % 10 == 0) {
connection.resetPosition();
this.level().chunkSource.move(this);
}
if (this.followPlayer != null) {
if (this.getCamera() == this || this.getCamera().level() != this.level()) {
this.setCamera(followPlayer);
}
if (lastPosVec3.distanceToSqr(this.position()) > 1024D) {
((CraftPhotographer) this.getBukkitPlayer()).taskScheduler.schedule(ent -> {
this.getBukkitPlayer().teleportAsync(this.getCamera().getBukkitEntity().getLocation());
}, null, 1L);
}
}
lastPosVec3 = this.position();
}
@Override
public void die(@NotNull DamageSource damageSource) {
super.die(damageSource);
remove(true);
}
@Override
public boolean isInvulnerableTo(@NotNull ServerLevel world, @NotNull DamageSource damageSource) {
return true;
}
@Override
public boolean hurtServer(@NotNull ServerLevel world, @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) {
this.remove(async, true);
}
public void remove(boolean async, boolean save) {
LOGGER.info("Photographer {} removed", createState.id);
this.recorder.stop();
photographers.remove(this);
MinecraftServer.getServer().getPlayerList().removePhotographer(this);
RandomProfilePool.putProfile(this.gameProfile); // Lophine - add cache
if (!recorder.isSaved()) {
CompletableFuture<Void> future = recorder.saveRecording(saveFile, save);
if (!async) {
future.join();
}
}
}
public void setFollowPlayer(ServerPlayer followPlayer) {
this.setCamera(followPlayer);
this.followPlayer = followPlayer;
}
public ServerPlayer getFollowPlayer() {
return 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 boolean isCreateLegal(@NotNull String name) {
if (!name.matches("^[a-zA-Z0-9_]{4,16}$")) {
return false;
}
return Bukkit.getPlayerExact(name) == null && ServerPhotographer.getPhotographer(name) == null;
}
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) {
LOGGER.error("Error happened when create photographer: ", e);
}
return null;
}
}
}
@@ -0,0 +1,52 @@
/*
* This file is part of Leaves (https://github.com/LeavesMC/Leaves)
*
* Leaves is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Leaves is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Leaves. If not, see <https://www.gnu.org/licenses/>.
*/
package org.leavesmc.leaves.replay;
import net.kyori.adventure.text.Component;
import net.minecraft.server.level.ServerPlayerGameMode;
import net.minecraft.world.level.GameType;
import org.bukkit.event.player.PlayerGameModeChangeEvent;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public class ServerPhotographerGameMode extends ServerPlayerGameMode {
public ServerPhotographerGameMode(ServerPhotographer photographer) {
super(photographer);
super.setGameModeForPlayer(GameType.SPECTATOR, null);
}
@Override
public boolean changeGameModeForPlayer(@NotNull GameType gameMode) {
return false;
}
@Nullable
@Override
public PlayerGameModeChangeEvent changeGameModeForPlayer(@NotNull GameType gameMode, PlayerGameModeChangeEvent.@NotNull Cause cause, @Nullable Component cancelMessage) {
return null;
}
@Override
protected void setGameModeForPlayer(@NotNull GameType gameMode, @Nullable GameType previousGameMode) {
}
@Override
public void tick() {
}
}
@@ -0,0 +1,356 @@
/*
* This file is part of Leaves (https://github.com/LeavesMC/Leaves)
*
* Leaves is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Leaves is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Leaves. If not, see <https://www.gnu.org/licenses/>.
*/
package org.leavesmc.leaves.util;
import fun.bm.lophine.config.modules.function.WoolHopperCounterConfig;
import it.unimi.dsi.fastutil.objects.Object2LongLinkedOpenHashMap;
import it.unimi.dsi.fastutil.objects.Object2LongMap;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.TextComponent;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.Style;
import net.kyori.adventure.text.format.TextColor;
import net.kyori.adventure.text.format.TextDecoration;
import net.minecraft.core.Holder;
import net.minecraft.core.Registry;
import net.minecraft.core.RegistryAccess;
import net.minecraft.core.component.DataComponents;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.Identifier;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.MinecraftServer;
import net.minecraft.util.context.ContextMap;
import net.minecraft.world.item.*;
import net.minecraft.world.item.crafting.Ingredient;
import net.minecraft.world.item.crafting.Recipe;
import net.minecraft.world.item.crafting.RecipeManager;
import net.minecraft.world.item.crafting.display.RecipeDisplay;
import net.minecraft.world.item.crafting.display.SlotDisplayContext;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.AbstractBannerBlock;
import net.minecraft.world.level.block.BeaconBeamBlock;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.material.MapColor;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
import static java.util.Map.entry;
// Powered by fabric-carpet(https://github.com/gnembon/fabric-carpet)
public class HopperCounter {
private static boolean enabled = false;
private static final Map<DyeColor, HopperCounter> COUNTERS;
static {
EnumMap<DyeColor, HopperCounter> counterMap = new EnumMap<>(DyeColor.class);
for (DyeColor color : DyeColor.values()) {
counterMap.put(color, new HopperCounter(color));
}
COUNTERS = Collections.unmodifiableMap(counterMap);
}
public final DyeColor color;
private final TextComponent coloredName;
private final Object2LongMap<Item> counter = new Object2LongLinkedOpenHashMap<>();
private long startTick;
private long startMillis;
private HopperCounter(DyeColor color) {
this.startTick = -1;
this.color = color;
this.coloredName = Component.text(color.getName(), TextColor.color(color.getTextColor()));
}
public void add(MinecraftServer server, ItemStack stack) {
if (startTick < 0) {
startTick = server.overworld().getGameTime();
startMillis = System.currentTimeMillis();
}
Item item = stack.getItem();
counter.put(item, counter.getLong(item) + stack.getCount());
}
public void reset(MinecraftServer server) {
counter.clear();
startTick = server.overworld().getGameTime();
startMillis = System.currentTimeMillis();
}
public static void resetAll(MinecraftServer server, boolean fresh) {
for (HopperCounter counter : COUNTERS.values()) {
counter.reset(server);
if (fresh) {
counter.startTick = -1;
}
}
}
public List<Component> format(MinecraftServer server, boolean realTime) {
long ticks = Math.max(realTime ? (System.currentTimeMillis() - startMillis) / 50 : server.overworld().getGameTime() - startTick, -1);
if (startTick < 0 || ticks == -1) {
return Collections.singletonList(Component.text().append(coloredName, Component.text(" hasn't started counting yet")).build());
}
long total = getTotalItems();
if (total <= 0) {
return Collections.singletonList(Component.text()
.append(Component.text("No items for "), coloredName)
.append(Component.text(" yet ("), Component.text(String.format("%.2f ", ticks / (20.0 * 60.0)), Style.style(TextDecoration.BOLD)))
.append(Component.text("min"), Component.text(realTime ? " - real time" : ""), Component.text(")"))
.build());
}
List<Component> items = new ArrayList<>();
items.add(Component.text()
.append(Component.text("Items for "), coloredName, Component.text(" "))
.append(Component.text("("), Component.text(String.format("%.2f ", ticks * 1.0 / (20 * 60)), Style.style(TextDecoration.BOLD)))
.append(Component.text("min"), Component.text(realTime ? " - real time" : ""), Component.text("), "))
.append(Component.text("total: "), Component.text(total, Style.style(TextDecoration.BOLD)), Component.text(", "))
.append(Component.text("("), Component.text(String.format("%.1f", total * 1.0 * (20 * 60 * 60) / ticks), Style.style(TextDecoration.BOLD)))
.append(Component.text("/h):"))
.build());
items.addAll(counter.object2LongEntrySet().stream().sorted((e, f) -> Long.compare(f.getLongValue(), e.getLongValue())).map(entry -> {
Item item = entry.getKey();
Component name = Component.translatable(item.getDescriptionId());
TextColor textColor = guessColor(server, item);
if (textColor != null) {
name = name.style(name.style().merge(Style.style(textColor)));
} else {
name = name.style(name.style().merge(Style.style(TextDecoration.ITALIC)));
}
long count = entry.getLongValue();
return Component.text()
.append(Component.text("- ", NamedTextColor.GRAY))
.append(name)
.append(Component.text(": ", NamedTextColor.GRAY))
.append(Component.text(count, Style.style(TextDecoration.BOLD)), Component.text(", ", NamedTextColor.GRAY))
.append(Component.text(String.format("%.1f", count * (20.0 * 60.0 * 60.0) / ticks), Style.style(TextDecoration.BOLD)))
.append(Component.text("/h"))
.build();
}).toList());
return items;
}
private static final Map<Item, Block> DEFAULTS = Map.<Item, Block>ofEntries(
entry(Items.DANDELION, Blocks.YELLOW_WOOL),
entry(Items.POPPY, Blocks.RED_WOOL),
entry(Items.BLUE_ORCHID, Blocks.LIGHT_BLUE_WOOL),
entry(Items.ALLIUM, Blocks.MAGENTA_WOOL),
entry(Items.AZURE_BLUET, Blocks.SNOW_BLOCK),
entry(Items.RED_TULIP, Blocks.RED_WOOL),
entry(Items.ORANGE_TULIP, Blocks.ORANGE_WOOL),
entry(Items.WHITE_TULIP, Blocks.SNOW_BLOCK),
entry(Items.PINK_TULIP, Blocks.PINK_WOOL),
entry(Items.OXEYE_DAISY, Blocks.SNOW_BLOCK),
entry(Items.CORNFLOWER, Blocks.BLUE_WOOL),
entry(Items.WITHER_ROSE, Blocks.BLACK_WOOL),
entry(Items.LILY_OF_THE_VALLEY, Blocks.WHITE_WOOL),
entry(Items.BROWN_MUSHROOM, Blocks.BROWN_MUSHROOM_BLOCK),
entry(Items.RED_MUSHROOM, Blocks.RED_MUSHROOM_BLOCK),
entry(Items.STICK, Blocks.OAK_PLANKS),
entry(Items.GOLD_INGOT, Blocks.GOLD_BLOCK),
entry(Items.IRON_INGOT, Blocks.IRON_BLOCK),
entry(Items.DIAMOND, Blocks.DIAMOND_BLOCK),
entry(Items.NETHERITE_INGOT, Blocks.NETHERITE_BLOCK),
entry(Items.SUNFLOWER, Blocks.YELLOW_WOOL),
entry(Items.LILAC, Blocks.MAGENTA_WOOL),
entry(Items.ROSE_BUSH, Blocks.RED_WOOL),
entry(Items.PEONY, Blocks.PINK_WOOL),
entry(Items.CARROT, Blocks.ORANGE_WOOL),
entry(Items.APPLE, Blocks.RED_WOOL),
entry(Items.WHEAT, Blocks.HAY_BLOCK),
entry(Items.PORKCHOP, Blocks.PINK_WOOL),
entry(Items.RABBIT, Blocks.PINK_WOOL),
entry(Items.CHICKEN, Blocks.WHITE_TERRACOTTA),
entry(Items.BEEF, Blocks.NETHERRACK),
entry(Items.ENCHANTED_GOLDEN_APPLE, Blocks.GOLD_BLOCK),
entry(Items.COD, Blocks.WHITE_TERRACOTTA),
entry(Items.SALMON, Blocks.ACACIA_PLANKS),
entry(Items.ROTTEN_FLESH, Blocks.BROWN_WOOL),
entry(Items.PUFFERFISH, Blocks.YELLOW_TERRACOTTA),
entry(Items.TROPICAL_FISH, Blocks.ORANGE_WOOL),
entry(Items.POTATO, Blocks.WHITE_TERRACOTTA),
entry(Items.MUTTON, Blocks.RED_WOOL),
entry(Items.BEETROOT, Blocks.NETHERRACK),
entry(Items.MELON_SLICE, Blocks.MELON),
entry(Items.POISONOUS_POTATO, Blocks.SLIME_BLOCK),
entry(Items.SPIDER_EYE, Blocks.NETHERRACK),
entry(Items.GUNPOWDER, Blocks.GRAY_WOOL),
entry(Items.TURTLE_SCUTE, Blocks.LIME_WOOL),
entry(Items.ARMADILLO_SCUTE, Blocks.ANCIENT_DEBRIS),
entry(Items.FEATHER, Blocks.WHITE_WOOL),
entry(Items.FLINT, Blocks.BLACK_WOOL),
entry(Items.LEATHER, Blocks.SPRUCE_PLANKS),
entry(Items.GLOWSTONE_DUST, Blocks.GLOWSTONE),
entry(Items.PAPER, Blocks.WHITE_WOOL),
entry(Items.BRICK, Blocks.BRICKS),
entry(Items.INK_SAC, Blocks.BLACK_WOOL),
entry(Items.SNOWBALL, Blocks.SNOW_BLOCK),
entry(Items.WATER_BUCKET, Blocks.WATER),
entry(Items.LAVA_BUCKET, Blocks.LAVA),
entry(Items.MILK_BUCKET, Blocks.WHITE_WOOL),
entry(Items.CLAY_BALL, Blocks.CLAY),
entry(Items.COCOA_BEANS, Blocks.COCOA),
entry(Items.BONE, Blocks.BONE_BLOCK),
entry(Items.COD_BUCKET, Blocks.BROWN_TERRACOTTA),
entry(Items.PUFFERFISH_BUCKET, Blocks.YELLOW_TERRACOTTA),
entry(Items.SALMON_BUCKET, Blocks.PINK_TERRACOTTA),
entry(Items.TROPICAL_FISH_BUCKET, Blocks.ORANGE_TERRACOTTA),
entry(Items.SUGAR, Blocks.WHITE_WOOL),
entry(Items.BLAZE_POWDER, Blocks.GOLD_BLOCK),
entry(Items.ENDER_PEARL, Blocks.WARPED_PLANKS),
entry(Items.NETHER_STAR, Blocks.DIAMOND_BLOCK),
entry(Items.PRISMARINE_CRYSTALS, Blocks.SEA_LANTERN),
entry(Items.PRISMARINE_SHARD, Blocks.PRISMARINE),
entry(Items.RABBIT_HIDE, Blocks.OAK_PLANKS),
entry(Items.CHORUS_FRUIT, Blocks.PURPUR_BLOCK),
entry(Items.SHULKER_SHELL, Blocks.SHULKER_BOX),
entry(Items.NAUTILUS_SHELL, Blocks.BONE_BLOCK),
entry(Items.HEART_OF_THE_SEA, Blocks.CONDUIT),
entry(Items.HONEYCOMB, Blocks.HONEYCOMB_BLOCK),
entry(Items.NAME_TAG, Blocks.BONE_BLOCK),
entry(Items.TOTEM_OF_UNDYING, Blocks.YELLOW_TERRACOTTA),
entry(Items.TRIDENT, Blocks.PRISMARINE),
entry(Items.GHAST_TEAR, Blocks.WHITE_WOOL),
entry(Items.PHANTOM_MEMBRANE, Blocks.BONE_BLOCK),
entry(Items.EGG, Blocks.BONE_BLOCK),
entry(Items.COPPER_INGOT, Blocks.COPPER_BLOCK),
entry(Items.AMETHYST_SHARD, Blocks.AMETHYST_BLOCK)
);
@SuppressWarnings("deprecation")
@Nullable
public static TextColor guessColor(@NotNull MinecraftServer server, Item item) {
RegistryAccess registryAccess = server.registryAccess();
TextColor direct = fromItem(item, registryAccess);
if (direct != null) {
return direct;
}
Identifier id = registryAccess.lookupOrThrow(Registries.ITEM).getKey(item);
if (id == null) {
return null;
}
for (Recipe<?> recipe : getRecipesForOutput(server.getRecipeManager(), id, server.overworld())) {
for (Ingredient ingredient : recipe.placementInfo().ingredients()) {
Optional<Holder<Item>> match = ingredient.items().filter(stack -> fromItem(stack.value(), registryAccess) != null).findFirst();
if (match.isPresent()) {
return fromItem(match.get().value(), registryAccess);
}
}
}
return null;
}
@NotNull
public static List<Recipe<?>> getRecipesForOutput(@NotNull RecipeManager recipeManager, Identifier id, Level level) {
List<Recipe<?>> results = new ArrayList<>();
ContextMap context = SlotDisplayContext.fromLevel(level);
recipeManager.getRecipes().forEach(recipe -> {
for (RecipeDisplay recipeDisplay : recipe.value().display()) {
recipeDisplay.result().resolveForStacks(context).forEach(stack -> {
if (BuiltInRegistries.ITEM.wrapAsHolder(stack.getItem()).unwrapKey().map(ResourceKey::identifier).orElseThrow(IllegalStateException::new).equals(id)) {
results.add(recipe.value());
}
});
}
});
return results;
}
@Nullable
public static TextColor fromItem(Item item, RegistryAccess registryAccess) {
if (DEFAULTS.containsKey(item)) {
return TextColor.color(appropriateColor(DEFAULTS.get(item).defaultMapColor().col));
}
if (item instanceof DyeItem) {
// Leaves - Paper 26.1: DyeItem#getDyeColor() removed, color now lives in DataComponents.DYE
DyeColor dyeColor = item.components().get(DataComponents.DYE);
if (dyeColor != null) {
return TextColor.color(appropriateColor(dyeColor.getMapColor().col));
}
}
Block block = null;
final Registry<Item> itemRegistry = registryAccess.lookupOrThrow(Registries.ITEM);
final Registry<Block> blockRegistry = registryAccess.lookupOrThrow(Registries.BLOCK);
Identifier id = itemRegistry.getKey(item);
if (item instanceof BlockItem blockItem) {
block = blockItem.getBlock();
} else if (blockRegistry.getOptional(id).isPresent()) {
block = blockRegistry.getValue(id);
}
if (block != null) {
if (block instanceof AbstractBannerBlock) {
return TextColor.color(appropriateColor(((AbstractBannerBlock) block).getColor().getMapColor().col));
} else if (block instanceof BeaconBeamBlock) {
return TextColor.color(appropriateColor(((BeaconBeamBlock) block).getColor().getMapColor().col));
}
return TextColor.color(appropriateColor(block.defaultMapColor().col));
}
return null;
}
public static int appropriateColor(int color) {
if (color == 0) {
return MapColor.SNOW.col;
}
int r = (color >> 16 & 255);
int g = (color >> 8 & 255);
int b = (color & 255);
if (r < 70) {
r = 70;
}
if (g < 70) {
g = 70;
}
if (b < 70) {
b = 70;
}
return (r << 16) + (g << 8) + b;
}
public long getTotalItems() {
return counter.isEmpty() ? 0 : counter.values().longStream().sum();
}
public static HopperCounter getCounter(DyeColor color) {
return COUNTERS.get(color);
}
public static void setEnabled(boolean is) {
enabled = is;
}
public static boolean isEnabled() {
return WoolHopperCounterConfig.enabled && enabled;
}
}
@@ -0,0 +1,34 @@
/*
* This file is part of Leaves (https://github.com/LeavesMC/Leaves)
*
* Leaves is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Leaves is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Leaves. If not, see <https://www.gnu.org/licenses/>.
*/
package org.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());
}
}
@@ -0,0 +1,55 @@
/*
* This file is part of Leaves (https://github.com/LeavesMC/Leaves)
*
* Leaves is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Leaves is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Leaves. If not, see <https://www.gnu.org/licenses/>.
*/
package org.leavesmc.leaves.util;
import net.minecraft.core.BlockPos;
import net.minecraft.world.item.DyeColor;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.state.BlockState;
import java.util.Map;
import static java.util.Map.entry;
public class WoolUtils {
private static final Map<Block, DyeColor> WOOL_BLOCK_TO_DYE = Map.ofEntries(
entry(Blocks.WHITE_WOOL, DyeColor.WHITE),
entry(Blocks.ORANGE_WOOL, DyeColor.ORANGE),
entry(Blocks.MAGENTA_WOOL, DyeColor.MAGENTA),
entry(Blocks.LIGHT_BLUE_WOOL, DyeColor.LIGHT_BLUE),
entry(Blocks.YELLOW_WOOL, DyeColor.YELLOW),
entry(Blocks.LIME_WOOL, DyeColor.LIME),
entry(Blocks.PINK_WOOL, DyeColor.PINK),
entry(Blocks.GRAY_WOOL, DyeColor.GRAY),
entry(Blocks.LIGHT_GRAY_WOOL, DyeColor.LIGHT_GRAY),
entry(Blocks.CYAN_WOOL, DyeColor.CYAN),
entry(Blocks.PURPLE_WOOL, DyeColor.PURPLE),
entry(Blocks.BLUE_WOOL, DyeColor.BLUE),
entry(Blocks.BROWN_WOOL, DyeColor.BROWN),
entry(Blocks.GREEN_WOOL, DyeColor.GREEN),
entry(Blocks.RED_WOOL, DyeColor.RED),
entry(Blocks.BLACK_WOOL, DyeColor.BLACK)
);
public static DyeColor getWoolColorAtPosition(Level worldIn, BlockPos pos) {
BlockState state = worldIn.getBlockState(pos);
return WOOL_BLOCK_TO_DYE.get(state.getBlock());
}
}