Start Pre-release 26.1.2
some patches still not applied, need further update
This commit is contained in:
@@ -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);
|
||||
}
|
||||
+4
-3
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+4
-3
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+25
@@ -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));
|
||||
}
|
||||
}
|
||||
+34
@@ -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();
|
||||
}
|
||||
}
|
||||
+41
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+90
@@ -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() + '}';
|
||||
}
|
||||
}
|
||||
+99
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+52
@@ -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());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user