diff --git a/migrate/api/0002-Client-Visibility-Settings-API.patch b/migrate/api/applied/0002-Client-Visibility-Settings-API.patch similarity index 100% rename from migrate/api/0002-Client-Visibility-Settings-API.patch rename to migrate/api/applied/0002-Client-Visibility-Settings-API.patch diff --git a/migrate/api/0003-Merge-Cannon-Entities.patch b/migrate/api/applied/0003-Merge-Cannon-Entities.patch similarity index 100% rename from migrate/api/0003-Merge-Cannon-Entities.patch rename to migrate/api/applied/0003-Merge-Cannon-Entities.patch diff --git a/migrate/server/feature/applied/.ignore b/migrate/server/feature/applied/.ignore deleted file mode 100644 index e69de29..0000000 diff --git a/migrate/server/feature/0005-Client-Visibility-Settings.patch b/migrate/server/feature/applied/0005-Client-Visibility-Settings.patch similarity index 100% rename from migrate/server/feature/0005-Client-Visibility-Settings.patch rename to migrate/server/feature/applied/0005-Client-Visibility-Settings.patch diff --git a/migrate/server/feature/0007-Load-Chunks-on-Movement.patch b/migrate/server/feature/applied/0007-Load-Chunks-on-Movement.patch similarity index 100% rename from migrate/server/feature/0007-Load-Chunks-on-Movement.patch rename to migrate/server/feature/applied/0007-Load-Chunks-on-Movement.patch diff --git a/migrate/server/feature/0010-Slice-Packet-obfuscation-and-reduction.patch b/migrate/server/feature/applied/0010-Slice-Packet-obfuscation-and-reduction.patch similarity index 100% rename from migrate/server/feature/0010-Slice-Packet-obfuscation-and-reduction.patch rename to migrate/server/feature/applied/0010-Slice-Packet-obfuscation-and-reduction.patch diff --git a/migrate/server/feature/0014-Optimise-paper-explosions.patch b/migrate/server/feature/applied/0014-Optimise-paper-explosions.patch similarity index 100% rename from migrate/server/feature/0014-Optimise-paper-explosions.patch rename to migrate/server/feature/applied/0014-Optimise-paper-explosions.patch diff --git a/migrate/server/feature/0015-Store-Entity-Data-State.patch b/migrate/server/feature/applied/0015-Store-Entity-Data-State.patch similarity index 100% rename from migrate/server/feature/0015-Store-Entity-Data-State.patch rename to migrate/server/feature/applied/0015-Store-Entity-Data-State.patch diff --git a/migrate/server/feature/0016-Merge-Cannon-Entities.patch b/migrate/server/feature/applied/0016-Merge-Cannon-Entities.patch similarity index 100% rename from migrate/server/feature/0016-Merge-Cannon-Entities.patch rename to migrate/server/feature/applied/0016-Merge-Cannon-Entities.patch diff --git a/sakura-api/paper-patches/features/0001-Client-Visibility-Settings-API.patch b/sakura-api/paper-patches/features/0001-Client-Visibility-Settings-API.patch new file mode 100644 index 0000000..6b6dd4d --- /dev/null +++ b/sakura-api/paper-patches/features/0001-Client-Visibility-Settings-API.patch @@ -0,0 +1,25 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Samsuik +Date: Tue, 21 Sep 2021 23:54:25 +0100 +Subject: [PATCH] Client Visibility Settings API + + +diff --git a/src/main/java/org/bukkit/entity/Player.java b/src/main/java/org/bukkit/entity/Player.java +index 1b5f3007a0afa7d007da84ba6afeb8b0185f6997..7dbfa59c02b0ce7d96796ff984d4a5879e9d6f95 100644 +--- a/src/main/java/org/bukkit/entity/Player.java ++++ b/src/main/java/org/bukkit/entity/Player.java +@@ -66,6 +66,14 @@ public interface Player extends HumanEntity, Conversable, OfflinePlayer, PluginM + + void setTrackingRangeModifier(double mod); + // Sakura end - entity tracking range modifier ++ // Sakura start - client visibility settings api ++ /** ++ * Server-side api to disable sending visual effects to the client. ++ * ++ * @return visibility api ++ */ ++ me.samsuik.sakura.player.visibility.VisibilitySettings getVisibility(); ++ // Sakura end - client visibility settings api + + // Paper start + @Override diff --git a/sakura-api/paper-patches/features/0002-Merge-Cannon-Entities-API.patch b/sakura-api/paper-patches/features/0002-Merge-Cannon-Entities-API.patch new file mode 100644 index 0000000..115e8e1 --- /dev/null +++ b/sakura-api/paper-patches/features/0002-Merge-Cannon-Entities-API.patch @@ -0,0 +1,32 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Samsuik +Date: Sat, 9 Sep 2023 18:39:15 +0100 +Subject: [PATCH] Merge Cannon Entities API + + +diff --git a/src/main/java/org/bukkit/entity/FallingBlock.java b/src/main/java/org/bukkit/entity/FallingBlock.java +index 0f7d64c1c0221ef7fa933df8c5572dbfdcba5128..78d62651760ff0c9ebc5c01d2417c8c86e116df5 100644 +--- a/src/main/java/org/bukkit/entity/FallingBlock.java ++++ b/src/main/java/org/bukkit/entity/FallingBlock.java +@@ -7,7 +7,7 @@ import org.jetbrains.annotations.NotNull; + /** + * Represents a falling block + */ +-public interface FallingBlock extends Entity { ++public interface FallingBlock extends Entity, me.samsuik.sakura.entity.merge.Mergeable { // Sakura - merge cannon entities api + + // Sakura start - falling block height parity api + /** +diff --git a/src/main/java/org/bukkit/entity/TNTPrimed.java b/src/main/java/org/bukkit/entity/TNTPrimed.java +index 87e717c9ea61b0cbf536bc62fa829ddcfae5ad8c..2e89ea4e896bdea552ec40fca927920f5f96fd59 100644 +--- a/src/main/java/org/bukkit/entity/TNTPrimed.java ++++ b/src/main/java/org/bukkit/entity/TNTPrimed.java +@@ -6,7 +6,7 @@ import org.jetbrains.annotations.Nullable; + /** + * Represents a Primed TNT. + */ +-public interface TNTPrimed extends Explosive { ++public interface TNTPrimed extends Explosive, me.samsuik.sakura.entity.merge.Mergeable { // Sakura - merge cannon entities api + + /** + * Set the number of ticks until the TNT blows up after being primed. diff --git a/sakura-api/src/main/java/me/samsuik/sakura/player/visibility/VisibilitySettings.java b/sakura-api/src/main/java/me/samsuik/sakura/player/visibility/VisibilitySettings.java new file mode 100644 index 0000000..50ba48e --- /dev/null +++ b/sakura-api/src/main/java/me/samsuik/sakura/player/visibility/VisibilitySettings.java @@ -0,0 +1,48 @@ +package me.samsuik.sakura.player.visibility; + +import org.jspecify.annotations.NullMarked; + +@NullMarked +public interface VisibilitySettings { + default boolean isEnabled(VisibilityType type) { + return this.get(type) == VisibilityState.ON; + } + + default boolean isDisabled(VisibilityType type) { + return this.get(type) == VisibilityState.OFF; + } + + default boolean isToggled(VisibilityType type) { + return !type.isDefault(this.get(type)); + } + + default VisibilityState toggle(VisibilityType type) { + VisibilityState state = this.get(type); + return this.set(type, toggleState(state)); + } + + default VisibilityState cycle(VisibilityType type) { + VisibilityState state = this.get(type); + return this.set(type, type.cycle(state)); + } + + default void toggleAll() { + VisibilityState state = this.currentState(); + VisibilityState newState = toggleState(state); + for (VisibilityType type : VisibilityTypes.types()) { + this.set(type, newState); + } + } + + VisibilityState get(VisibilityType type); + + VisibilityState set(VisibilityType type, VisibilityState state); + + VisibilityState currentState(); + + boolean playerModified(); + + static VisibilityState toggleState(VisibilityState state) { + return state != VisibilityState.OFF ? VisibilityState.OFF : VisibilityState.ON; + } +} diff --git a/sakura-api/src/main/java/me/samsuik/sakura/player/visibility/VisibilityState.java b/sakura-api/src/main/java/me/samsuik/sakura/player/visibility/VisibilityState.java new file mode 100644 index 0000000..d9cb2ea --- /dev/null +++ b/sakura-api/src/main/java/me/samsuik/sakura/player/visibility/VisibilityState.java @@ -0,0 +1,5 @@ +package me.samsuik.sakura.player.visibility; + +public enum VisibilityState { + ON, MODIFIED, MINIMAL, OFF; +} diff --git a/sakura-api/src/main/java/me/samsuik/sakura/player/visibility/VisibilityType.java b/sakura-api/src/main/java/me/samsuik/sakura/player/visibility/VisibilityType.java new file mode 100644 index 0000000..6b6b482 --- /dev/null +++ b/sakura-api/src/main/java/me/samsuik/sakura/player/visibility/VisibilityType.java @@ -0,0 +1,35 @@ +package me.samsuik.sakura.player.visibility; + +import com.google.common.collect.ImmutableList; +import org.jspecify.annotations.NullMarked; + +@NullMarked +public record VisibilityType(String key, ImmutableList states) { + public VisibilityState getDefault() { + return this.states.getFirst(); + } + + public boolean isDefault(VisibilityState state) { + return state == this.getDefault(); + } + + public VisibilityState cycle(VisibilityState state) { + int index = this.states.indexOf(state); + int next = (index + 1) % this.states.size(); + return this.states.get(next); + } + + public static VisibilityType from(String key, boolean minimal) { + return new VisibilityType(key, states(minimal)); + } + + private static ImmutableList states(boolean minimal) { + ImmutableList.Builder listBuilder = ImmutableList.builder(); + listBuilder.add(VisibilityState.ON); + if (minimal) { + listBuilder.add(VisibilityState.MINIMAL); + } + listBuilder.add(VisibilityState.OFF); + return listBuilder.build(); + } +} diff --git a/sakura-api/src/main/java/me/samsuik/sakura/player/visibility/VisibilityTypes.java b/sakura-api/src/main/java/me/samsuik/sakura/player/visibility/VisibilityTypes.java new file mode 100644 index 0000000..f323cfc --- /dev/null +++ b/sakura-api/src/main/java/me/samsuik/sakura/player/visibility/VisibilityTypes.java @@ -0,0 +1,31 @@ +package me.samsuik.sakura.player.visibility; + +import com.google.common.collect.ImmutableList; +import org.jspecify.annotations.NullMarked; + +import java.util.ArrayList; +import java.util.List; + +@NullMarked +public final class VisibilityTypes { + private static final List TYPES = new ArrayList<>(); + + public static final VisibilityType TNT = register(create("tnt", true)); + public static final VisibilityType SAND = register(create("sand", true)); + public static final VisibilityType EXPLOSIONS = register(create("explosions", true)); + public static final VisibilityType SPAWNERS = register(create("spawners", false)); + public static final VisibilityType PISTONS = register(create("pistons", false)); + + public static ImmutableList types() { + return ImmutableList.copyOf(TYPES); + } + + private static VisibilityType create(String key, boolean minimal) { + return VisibilityType.from(key, minimal); + } + + private static VisibilityType register(VisibilityType type) { + TYPES.add(type); + return type; + } +} diff --git a/sakura-server/minecraft-patches/features/0001-Client-Visibility-Settings.patch b/sakura-server/minecraft-patches/features/0001-Client-Visibility-Settings.patch new file mode 100644 index 0000000..2d775a7 --- /dev/null +++ b/sakura-server/minecraft-patches/features/0001-Client-Visibility-Settings.patch @@ -0,0 +1,251 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Samsuik +Date: Tue, 21 Sep 2021 23:54:25 +0100 +Subject: [PATCH] Client Visibility Settings + + +diff --git a/net/minecraft/server/MinecraftServer.java b/net/minecraft/server/MinecraftServer.java +index 3a926d4d34a7c68a24b9e00bcbcff271c8992ad2..f1f322f623756f0c0a06a4207a12765c9623e151 100644 +--- a/net/minecraft/server/MinecraftServer.java ++++ b/net/minecraft/server/MinecraftServer.java +@@ -1749,6 +1749,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop optional = Optional.ofNullable(serverExplosion.getHitPlayers().get(serverPlayer)); +- serverPlayer.connection.send(new ClientboundExplodePacket(vec3, optional, particleOptions, explosionSound)); ++ // Sakura start - client visibility settings; let players toggle explosion particles ++ ParticleOptions particle = particleOptions; ++ Vec3 position = vec3; ++ // In 1.22 and later this should be replaced with sending the motion through a PlayerPositionPacket. ++ // The problem here is SetEntityMotion is capped to 3.9 b/pt and the only other alternate mean was ++ // implemented in 1.21.3. I believe it's best to just wait on this issue and deal with this hack. ++ if (!this.checkExplosionVisibility(vec3, serverPlayer)) { ++ position = new Vec3(0.0, -1024.0, 0.0); ++ particle = net.minecraft.core.particles.ParticleTypes.SMOKE; ++ } ++ serverPlayer.connection.send(new ClientboundExplodePacket(position, optional, particle, explosionSound)); ++ // Sakura end - client visibility settings; let players toggle explosion particles + } + } + +diff --git a/net/minecraft/server/level/ServerPlayer.java b/net/minecraft/server/level/ServerPlayer.java +index 4535ea5fe788fc5c473d8289c031f2c9e56cfce6..92ee40a77f1b13c2c720cd41747206f82a735f9e 100644 +--- a/net/minecraft/server/level/ServerPlayer.java ++++ b/net/minecraft/server/level/ServerPlayer.java +@@ -424,6 +424,7 @@ public class ServerPlayer extends Player implements ca.spottedleaf.moonrise.patc + } + // Paper end - rewrite chunk system + public double trackingRangeModifier = 1.0; // Sakura - entity tracking range modifier ++ public final me.samsuik.sakura.player.visibility.PlayerVisibilitySettings visibilitySettings = new me.samsuik.sakura.player.visibility.PlayerVisibilitySettings(); // Sakura - client visibility settings + + public ServerPlayer(MinecraftServer server, ServerLevel level, GameProfile gameProfile, ClientInformation clientInformation) { + super(level, level.getSharedSpawnPos(), level.getSharedSpawnAngle(), gameProfile); +diff --git a/net/minecraft/server/network/ServerCommonPacketListenerImpl.java b/net/minecraft/server/network/ServerCommonPacketListenerImpl.java +index e71c1a564e5d4ac43460f89879ff709ee685706f..7d2fe5df38db1d492ae65aa72959200221cf32d5 100644 +--- a/net/minecraft/server/network/ServerCommonPacketListenerImpl.java ++++ b/net/minecraft/server/network/ServerCommonPacketListenerImpl.java +@@ -51,6 +51,21 @@ public abstract class ServerCommonPacketListenerImpl implements ServerCommonPack + public final java.util.Map packCallbacks = new java.util.concurrent.ConcurrentHashMap<>(); // Paper - adventure resource pack callbacks + private static final long KEEPALIVE_LIMIT = Long.getLong("paper.playerconnection.keepalive", 30) * 1000; // Paper - provide property to set keepalive limit + protected static final net.minecraft.resources.ResourceLocation MINECRAFT_BRAND = net.minecraft.resources.ResourceLocation.withDefaultNamespace("brand"); // Paper - Brand support ++ // Sakura start - client visibility settings ++ private @Nullable Packet recreatePacket(final Packet packet) { ++ final me.samsuik.sakura.player.visibility.VisibilitySettings settings = this.player.visibilitySettings; ++ if (packet instanceof net.minecraft.network.protocol.game.ClientboundBlockEntityDataPacket bedPacket) { ++ if (settings.isToggled(me.samsuik.sakura.player.visibility.VisibilityTypes.SPAWNERS) && bedPacket.getType() == net.minecraft.world.level.block.entity.BlockEntityType.MOB_SPAWNER) { ++ return null; ++ } ++ } else if (packet instanceof net.minecraft.network.protocol.game.ClientboundBlockEventPacket bePacket) { ++ if (settings.isToggled(me.samsuik.sakura.player.visibility.VisibilityTypes.PISTONS) && bePacket.getBlock() instanceof net.minecraft.world.level.block.piston.PistonBaseBlock) { ++ return null; ++ } ++ } ++ return packet; ++ } ++ // Sakura end - client visibility settings + + public ServerCommonPacketListenerImpl(MinecraftServer server, Connection connection, CommonListenerCookie cookie, net.minecraft.server.level.ServerPlayer player) { // CraftBukkit + this.server = server; +@@ -287,6 +302,12 @@ public abstract class ServerCommonPacketListenerImpl implements ServerCommonPack + } else if (packet instanceof net.minecraft.network.protocol.game.ClientboundSetDefaultSpawnPositionPacket defaultSpawnPositionPacket) { + this.player.compassTarget = org.bukkit.craftbukkit.util.CraftLocation.toBukkit(defaultSpawnPositionPacket.getPos(), this.getCraftPlayer().getWorld()); + } ++ // Sakura start - client visibility settings ++ if (this.player.visibilitySettings.playerModified()) { ++ packet = this.recreatePacket(packet); ++ if (packet == null) return; ++ } ++ // Sakura end - client visibility settings + // CraftBukkit end + if (packet.isTerminal()) { + this.close(); +@@ -299,7 +320,10 @@ public abstract class ServerCommonPacketListenerImpl implements ServerCommonPack + } catch (Throwable var7) { + CrashReport crashReport = CrashReport.forThrowable(var7, "Sending packet"); + CrashReportCategory crashReportCategory = crashReport.addCategory("Packet being sent"); +- crashReportCategory.setDetail("Packet class", () -> packet.getClass().getCanonicalName()); ++ // Sakura start - client visibility settings; packet has to be effectively final ++ final Packet packetFinal = packet; ++ crashReportCategory.setDetail("Packet class", () -> packetFinal.getClass().getCanonicalName()); ++ // Sakura end - client visibility settings; packet has to be effectively final + throw new ReportedException(crashReport); + } + } +diff --git a/net/minecraft/server/network/ServerGamePacketListenerImpl.java b/net/minecraft/server/network/ServerGamePacketListenerImpl.java +index 27ef385a85b13ceb58e8d149849983107c539b31..c44c9399ddbb34408b255550a98f5c54ecdf6aff 100644 +--- a/net/minecraft/server/network/ServerGamePacketListenerImpl.java ++++ b/net/minecraft/server/network/ServerGamePacketListenerImpl.java +@@ -3185,6 +3185,7 @@ public class ServerGamePacketListenerImpl + + event.setCancelled(cancelled); + AbstractContainerMenu oldContainer = this.player.containerMenu; // SPIGOT-1224 ++ me.samsuik.sakura.player.gui.FeatureGui.clickEvent(event); // Sakura - client visibility settings + this.cserver.getPluginManager().callEvent(event); + if (this.player.containerMenu != oldContainer) { + return; +diff --git a/net/minecraft/world/entity/Entity.java b/net/minecraft/world/entity/Entity.java +index 54a3ac38b70a9733173fb2ce4a7f86b534de1136..c4ace72f2eaf6a15c98490d18d3478b9ad5ed1a6 100644 +--- a/net/minecraft/world/entity/Entity.java ++++ b/net/minecraft/world/entity/Entity.java +@@ -526,6 +526,10 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + } + // Paper end - optimise entity tracker + public boolean pushedByFluid = true; // Sakura - entity pushed by fluid api ++ // Sakura start - client visibility settings ++ public boolean isPrimedTNT; ++ public boolean isFallingBlock; ++ // Sakura end - client visibility settings + + public Entity(EntityType entityType, Level level) { + this.type = entityType; +diff --git a/net/minecraft/world/entity/item/FallingBlockEntity.java b/net/minecraft/world/entity/item/FallingBlockEntity.java +index 6b708503367f989665b89a39d367935c80210d1d..5462a423be1317a306ca12ed10edcab636cd0c7f 100644 +--- a/net/minecraft/world/entity/item/FallingBlockEntity.java ++++ b/net/minecraft/world/entity/item/FallingBlockEntity.java +@@ -73,6 +73,7 @@ public class FallingBlockEntity extends Entity { + public FallingBlockEntity(EntityType entityType, Level level) { + super(entityType, level); + this.heightParity = level.sakuraConfig().cannons.mechanics.fallingBlockParity; // Sakura - configure cannon mechanics ++ this.isFallingBlock = true; // Sakura - client visibility settings + } + + public FallingBlockEntity(Level level, double x, double y, double z, BlockState state) { +diff --git a/net/minecraft/world/entity/item/PrimedTnt.java b/net/minecraft/world/entity/item/PrimedTnt.java +index 3c673e5b8a52e322d41ed442ed06337cacb58771..8118911019f7fc81218a656e1ecbd7eada505741 100644 +--- a/net/minecraft/world/entity/item/PrimedTnt.java ++++ b/net/minecraft/world/entity/item/PrimedTnt.java +@@ -61,6 +61,7 @@ public class PrimedTnt extends Entity implements TraceableEntity { + public PrimedTnt(EntityType entityType, Level level) { + super(entityType, level); + this.blocksBuilding = true; ++ this.isPrimedTNT = true; // Sakura - client visibility settings + } + + public PrimedTnt(Level level, double x, double y, double z, @Nullable LivingEntity owner) { diff --git a/sakura-server/minecraft-patches/features/0002-Load-Chunks-on-Movement.patch b/sakura-server/minecraft-patches/features/0002-Load-Chunks-on-Movement.patch new file mode 100644 index 0000000..e9efdd9 --- /dev/null +++ b/sakura-server/minecraft-patches/features/0002-Load-Chunks-on-Movement.patch @@ -0,0 +1,148 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Samsuik +Date: Sat, 11 Sep 2021 19:19:41 +0100 +Subject: [PATCH] Load Chunks on Movement + + +diff --git a/ca/spottedleaf/moonrise/patches/collisions/CollisionUtil.java b/ca/spottedleaf/moonrise/patches/collisions/CollisionUtil.java +index e04bd54744335fb5398c6e4f7ce8b981f35bfb7d..651a45b795818bd7b1364b95c52570fd99dd35e4 100644 +--- a/ca/spottedleaf/moonrise/patches/collisions/CollisionUtil.java ++++ b/ca/spottedleaf/moonrise/patches/collisions/CollisionUtil.java +@@ -1885,6 +1885,7 @@ public final class CollisionUtil { + public static final int COLLISION_FLAG_COLLIDE_WITH_UNLOADED_CHUNKS = 1 << 1; + public static final int COLLISION_FLAG_CHECK_BORDER = 1 << 2; + public static final int COLLISION_FLAG_CHECK_ONLY = 1 << 3; ++ public static final int COLLISION_FLAG_ADD_TICKET = 1 << 4; // Sakura - load chunks on movement + + public static boolean getCollisionsForBlocksOrWorldBorder(final Level world, final Entity entity, final AABB aabb, + final List intoVoxel, final List intoAABB, +@@ -1936,6 +1937,7 @@ public final class CollisionUtil { + final int maxChunkZ = maxBlockZ >> 4; + + final boolean loadChunks = (collisionFlags & COLLISION_FLAG_LOAD_CHUNKS) != 0; ++ final boolean addTicket = (collisionFlags & COLLISION_FLAG_ADD_TICKET) != 0; // Sakura - load chunks on movement + final ChunkSource chunkSource = world.getChunkSource(); + + for (int currChunkZ = minChunkZ; currChunkZ <= maxChunkZ; ++currChunkZ) { +@@ -1954,6 +1956,13 @@ public final class CollisionUtil { + continue; + } + ++ // Sakura start - load chunks on movement ++ if (addTicket && chunk.movementTicketNeedsUpdate() && chunkSource instanceof net.minecraft.server.level.ServerChunkCache chunkCache) { ++ final long chunkKey = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(currChunkX, currChunkZ); ++ chunkCache.chunkMap.getDistanceManager().moonrise$getChunkHolderManager().addTicketAtLevel(net.minecraft.server.level.TicketType.ENTITY_MOVEMENT, currChunkX, currChunkZ, 31, chunkKey); ++ chunk.updatedMovementTicket(); ++ } ++ // Sakura end - load chunks on movement + final LevelChunkSection[] sections = chunk.getSections(); + + // bound y +diff --git a/net/minecraft/server/level/TicketType.java b/net/minecraft/server/level/TicketType.java +index 8f12a4df5d63ecd11e6e615d910b6e3f6dde5f3c..56beffa0c5cdb0d6a4836a0ee496bd638432b143 100644 +--- a/net/minecraft/server/level/TicketType.java ++++ b/net/minecraft/server/level/TicketType.java +@@ -21,6 +21,7 @@ public class TicketType { + public static final TicketType PLUGIN = TicketType.create("plugin", (a, b) -> 0); // CraftBukkit + public static final TicketType PLUGIN_TICKET = TicketType.create("plugin_ticket", (plugin1, plugin2) -> plugin1.getClass().getName().compareTo(plugin2.getClass().getName())); // CraftBukkit + public static final TicketType POST_TELEPORT = TicketType.create("post_teleport", Integer::compare, 5); // Paper - post teleport ticket type ++ public static final TicketType ENTITY_MOVEMENT = TicketType.create("entity_movement", Long::compareTo, 10*20); // Sakura - load chunks on movement + + public static TicketType create(String name, Comparator comparator) { + return new TicketType<>(name, comparator, 0L); +diff --git a/net/minecraft/world/entity/Entity.java b/net/minecraft/world/entity/Entity.java +index c4ace72f2eaf6a15c98490d18d3478b9ad5ed1a6..00b3e16d5465547e7d4f8126664fb7eda3b3c568 100644 +--- a/net/minecraft/world/entity/Entity.java ++++ b/net/minecraft/world/entity/Entity.java +@@ -530,6 +530,20 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + public boolean isPrimedTNT; + public boolean isFallingBlock; + // Sakura end - client visibility settings ++ // Sakura start - load chunks on movement ++ protected boolean loadChunks = false; ++ ++ private int getExtraCollisionFlags() { ++ int flags = 0; ++ ++ if (this.loadChunks) { ++ flags |= ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_FLAG_LOAD_CHUNKS; ++ flags |= ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_FLAG_ADD_TICKET; ++ } ++ ++ return flags; ++ } ++ // Sakura end - load chunks on movement + + public Entity(EntityType entityType, Level level) { + this.type = entityType; +@@ -1469,7 +1483,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + + ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.getCollisionsForBlocksOrWorldBorder( + this.level, (Entity)(Object)this, initialCollisionBox, potentialCollisionsVoxel, potentialCollisionsBB, +- ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_FLAG_CHECK_BORDER, null ++ ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_FLAG_CHECK_BORDER | this.getExtraCollisionFlags(), null // Sakura - load chunks on movement + ); + potentialCollisionsBB.addAll(entityAABBs); + final Vec3 collided = ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.performCollisions(movement, currentBox, potentialCollisionsVoxel, potentialCollisionsBB); +@@ -4961,13 +4975,14 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + @Override + public boolean shouldBeSaved() { + return (this.removalReason == null || this.removalReason.shouldSave()) ++ && !this.loadChunks // Sakura - load chunks on movement; this is used to check if the chunk the entity is in can be unloaded + && !this.isPassenger() + && (!this.isVehicle() || !((ca.spottedleaf.moonrise.patches.chunk_system.entity.ChunkSystemEntity)this).moonrise$hasAnyPlayerPassengers()); // Paper - rewrite chunk system + } + + @Override + public boolean isAlwaysTicking() { +- return false; ++ return this.loadChunks; // Sakura - load chunks on movement; always tick in unloaded & lazy chunks + } + + public boolean mayInteract(ServerLevel level, BlockPos pos) { +diff --git a/net/minecraft/world/entity/item/FallingBlockEntity.java b/net/minecraft/world/entity/item/FallingBlockEntity.java +index 5462a423be1317a306ca12ed10edcab636cd0c7f..ee5ec7a780488182e30134b29a20cc192609d64b 100644 +--- a/net/minecraft/world/entity/item/FallingBlockEntity.java ++++ b/net/minecraft/world/entity/item/FallingBlockEntity.java +@@ -74,6 +74,7 @@ public class FallingBlockEntity extends Entity { + super(entityType, level); + this.heightParity = level.sakuraConfig().cannons.mechanics.fallingBlockParity; // Sakura - configure cannon mechanics + this.isFallingBlock = true; // Sakura - client visibility settings ++ this.loadChunks = level.sakuraConfig().cannons.loadChunks; // Sakura - load chunks on movement + } + + public FallingBlockEntity(Level level, double x, double y, double z, BlockState state) { +diff --git a/net/minecraft/world/entity/item/PrimedTnt.java b/net/minecraft/world/entity/item/PrimedTnt.java +index 8118911019f7fc81218a656e1ecbd7eada505741..e7b4efe35c20e11f130b5bce5c8c20390c65e0a4 100644 +--- a/net/minecraft/world/entity/item/PrimedTnt.java ++++ b/net/minecraft/world/entity/item/PrimedTnt.java +@@ -62,6 +62,7 @@ public class PrimedTnt extends Entity implements TraceableEntity { + super(entityType, level); + this.blocksBuilding = true; + this.isPrimedTNT = true; // Sakura - client visibility settings ++ this.loadChunks = level.sakuraConfig().cannons.loadChunks; // Sakura - load chunks on movement + } + + public PrimedTnt(Level level, double x, double y, double z, @Nullable LivingEntity owner) { +diff --git a/net/minecraft/world/level/chunk/ChunkAccess.java b/net/minecraft/world/level/chunk/ChunkAccess.java +index 6d565b52552534ce9cacfc35ad1bf4adcb69eac3..9b42bd1afb9a6c1729cb56e3c232f46112ba57d3 100644 +--- a/net/minecraft/world/level/chunk/ChunkAccess.java ++++ b/net/minecraft/world/level/chunk/ChunkAccess.java +@@ -138,6 +138,17 @@ public abstract class ChunkAccess implements BiomeManager.NoiseBiomeSource, Ligh + private final int minSection; + private final int maxSection; + // Paper end - get block chunk optimisation ++ // Sakura start - load chunks on movement ++ private long lastMovementLoadTicket = 0; ++ ++ public final boolean movementTicketNeedsUpdate() { ++ return net.minecraft.server.MinecraftServer.currentTick - this.lastMovementLoadTicket >= 100; ++ } ++ ++ public final void updatedMovementTicket() { ++ this.lastMovementLoadTicket = net.minecraft.server.MinecraftServer.currentTick; ++ } ++ // Sakura end - load chunks on movement + + public ChunkAccess( + ChunkPos chunkPos, diff --git a/sakura-server/minecraft-patches/features/0003-Slice-Packet-obfuscation-and-reduction.patch b/sakura-server/minecraft-patches/features/0003-Slice-Packet-obfuscation-and-reduction.patch new file mode 100644 index 0000000..016bde0 --- /dev/null +++ b/sakura-server/minecraft-patches/features/0003-Slice-Packet-obfuscation-and-reduction.patch @@ -0,0 +1,206 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Cryptite +Date: Wed, 6 Oct 2021 11:03:01 -0500 +Subject: [PATCH] (Slice) Packet obfuscation and reduction + +Minecraft is overzealous about packet updates for Entities. In Loka's case, we want to reduce as many unnecessary +packet updates as possible. This patch is likely to be updated over and over in terms of reducing packet sends. + +In summary, this patch creates the concept of a "foreignValue" of a packet's data. We treat packets in two ways: +1) The packet sent to the player itself (the normal way). This always has all of the values as usual. +2) The packet data as seen by any other (foreign) players. + +This patch adds the ability to set a "foreignValue" for an entity value so as to obfuscate data received by other players. +The current packets modified/obfuscated are the following: + + # This reduces the amount of health packet updates as well which is great for players in combat. + + # Air level packets are sent PER-TICK, and as such a player with any change in air level will only spam themselves + # with packets instead of every single player within tracking distance + +diff --git a/net/minecraft/network/syncher/SynchedEntityData.java b/net/minecraft/network/syncher/SynchedEntityData.java +index 3d90f9f1ac1bd281edf6bb0f93ea821657d5bd2f..6f3a6efe6624f9d4e500b2eee5d8aed3a6077e71 100644 +--- a/net/minecraft/network/syncher/SynchedEntityData.java ++++ b/net/minecraft/network/syncher/SynchedEntityData.java +@@ -20,6 +20,30 @@ public class SynchedEntityData { + private final SyncedDataHolder entity; + private final SynchedEntityData.DataItem[] itemsById; + private boolean isDirty; ++ // Slice start - packet obfuscation and reduction ++ private boolean isForeignDirty; ++ ++ public final boolean isForeignDirty() { ++ return this.isForeignDirty; ++ } ++ ++ @Nullable ++ public final List> packForeignDirty(List> unpackedData) { ++ List> list = null; ++ for (DataValue dataItem : unpackedData) { ++ DataItem item = this.itemsById[dataItem.id()]; ++ if (item.isDirty(true)) { ++ item.setForeignDirty(false); ++ if (list == null) { ++ list = new ArrayList<>(); ++ } ++ list.add(item.copy(true)); ++ } ++ } ++ this.isForeignDirty = false; ++ return list; ++ } ++ // Slice end - packet obfuscation and reduction + + SynchedEntityData(SyncedDataHolder entity, SynchedEntityData.DataItem[] itemsById) { + this.entity = entity; +@@ -58,6 +82,16 @@ public class SynchedEntityData { + } + + public void set(EntityDataAccessor key, T value, boolean force) { ++ // Slice start - packet obfuscation and reduction ++ this.set(key, value, null, force); ++ } ++ ++ public void set(EntityDataAccessor key, T value, T foreignValue) { ++ this.set(key, value, foreignValue, false); ++ } ++ ++ public void set(EntityDataAccessor key, T value, T foreignValue, boolean force) { ++ // Slice end - packet obfuscation and reduction + SynchedEntityData.DataItem item = this.getItem(key); + if (force || ObjectUtils.notEqual(value, item.getValue())) { + item.setValue(value); +@@ -65,6 +99,12 @@ public class SynchedEntityData { + item.setDirty(true); + this.isDirty = true; + } ++ // Slice start - packet obfuscation and reduction ++ if (foreignValue != null && ObjectUtils.notEqual(foreignValue, item.getForeignValue())) { ++ item.setForeignValue(foreignValue); ++ this.isForeignDirty = true; ++ } ++ // Slice end - packet obfuscation and reduction + } + + // CraftBukkit start - add method from above +@@ -195,6 +235,38 @@ public class SynchedEntityData { + T value; + private final T initialValue; + private boolean dirty; ++ // Slice start - packet obfuscation and reduction ++ @Nullable T foreignValue = null; ++ private boolean foreignDirty = true; ++ ++ public final void setForeignValue(T foreignValue) { ++ this.foreignValue = foreignValue; ++ this.foreignDirty = true; ++ } ++ ++ public final @Nullable T getForeignValue() { ++ return this.foreignValue; ++ } ++ ++ public final boolean isDirty(boolean foreign) { ++ if (foreign) { ++ //There must be a foreign value in order for this to be dirty, otherwise we consider this a normal ++ //value and check the normal dirty flag. ++ return this.foreignValue == null || this.foreignDirty; ++ } ++ ++ return this.dirty; ++ } ++ ++ public final void setForeignDirty(boolean dirty) { ++ this.foreignDirty = dirty; ++ } ++ ++ public final SynchedEntityData.DataValue copy(boolean foreign) { ++ return SynchedEntityData.DataValue.create(this.accessor, this.accessor.serializer() ++ .copy(foreign && this.foreignValue != null ? this.foreignValue : this.value)); ++ } ++ // Slice end - packet obfuscation and reduction + + public DataItem(EntityDataAccessor accessor, T value) { + this.accessor = accessor; +diff --git a/net/minecraft/server/level/ServerEntity.java b/net/minecraft/server/level/ServerEntity.java +index 0fb253aa55a24b56b17f524b3261c5b75c7d7e59..8abe899d19434ad4c7cc6c1596bab16df7b14275 100644 +--- a/net/minecraft/server/level/ServerEntity.java ++++ b/net/minecraft/server/level/ServerEntity.java +@@ -138,7 +138,7 @@ public class ServerEntity { + this.sendDirtyEntityData(); + } + +- if (this.forceStateResync || this.tickCount % this.updateInterval == 0 || this.entity.hasImpulse || this.entity.getEntityData().isDirty()) { // Paper - fix desync when a player is added to the tracker ++ if (this.forceStateResync || this.tickCount % this.updateInterval == 0 || this.entity.hasImpulse || this.entity.getEntityData().isForeignDirty()) { // Slice - packet obfuscation and reduction // Paper - fix desync when a player is added to the tracker + byte b = Mth.packDegrees(this.entity.getYRot()); + byte b1 = Mth.packDegrees(this.entity.getXRot()); + boolean flag = Math.abs(b - this.lastSentYRot) >= 1 || Math.abs(b1 - this.lastSentXRot) >= 1; +@@ -404,7 +404,15 @@ public class ServerEntity { + List> list = entityData.packDirty(); + if (list != null) { + this.trackedDataValues = entityData.getNonDefaultValues(); +- this.broadcastAndSend(new ClientboundSetEntityDataPacket(this.entity.getId(), list)); ++ // Slice start - packet obfuscation and reduction ++ if (!(this.entity instanceof ServerPlayer)) { ++ list = entityData.packForeignDirty(list); ++ } ++ ++ if (list != null) { ++ this.broadcastAndSend(new ClientboundSetEntityDataPacket(this.entity.getId(), list)); ++ } ++ // Slice end - packet obfuscation and reduction + } + + if (this.entity instanceof LivingEntity) { +diff --git a/net/minecraft/world/entity/Entity.java b/net/minecraft/world/entity/Entity.java +index 00b3e16d5465547e7d4f8126664fb7eda3b3c568..7bbc4a982f442fdb9821221442737ae65e55289e 100644 +--- a/net/minecraft/world/entity/Entity.java ++++ b/net/minecraft/world/entity/Entity.java +@@ -3495,7 +3495,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + this.entityData.markDirty(Entity.DATA_AIR_SUPPLY_ID); + return; + } +- this.entityData.set(Entity.DATA_AIR_SUPPLY_ID, event.getAmount()); ++ this.entityData.set(Entity.DATA_AIR_SUPPLY_ID, event.getAmount(), getMaxAirSupply()); // Slice + // CraftBukkit end + } + +diff --git a/net/minecraft/world/entity/item/FallingBlockEntity.java b/net/minecraft/world/entity/item/FallingBlockEntity.java +index ee5ec7a780488182e30134b29a20cc192609d64b..4836be29ff5e87e4f2b9beb0d4a9943281ab5262 100644 +--- a/net/minecraft/world/entity/item/FallingBlockEntity.java ++++ b/net/minecraft/world/entity/item/FallingBlockEntity.java +@@ -120,7 +120,7 @@ public class FallingBlockEntity extends Entity { + } + + public void setStartPos(BlockPos startPos) { +- this.entityData.set(DATA_START_POS, startPos); ++ this.entityData.set(DATA_START_POS, startPos, BlockPos.ZERO); // Slice - packet obfuscation and reduction + } + + public BlockPos getStartPos() { +diff --git a/net/minecraft/world/entity/item/PrimedTnt.java b/net/minecraft/world/entity/item/PrimedTnt.java +index e7b4efe35c20e11f130b5bce5c8c20390c65e0a4..9e9463d62aa1618a4a749bb7e2636c9b090991e9 100644 +--- a/net/minecraft/world/entity/item/PrimedTnt.java ++++ b/net/minecraft/world/entity/item/PrimedTnt.java +@@ -241,7 +241,7 @@ public class PrimedTnt extends Entity implements TraceableEntity { + } + + public void setFuse(int life) { +- this.entityData.set(DATA_FUSE_ID, life); ++ this.entityData.set(DATA_FUSE_ID, life, (life / 10) * 10); // Slice - packet obfuscation and reduction + } + + public int getFuse() { +diff --git a/net/minecraft/world/entity/player/Player.java b/net/minecraft/world/entity/player/Player.java +index ee6d5d3ccd55cffdeb96eeb77f648fd82feceb80..09209e5a9b61244a0877818c0841d19aee72522b 100644 +--- a/net/minecraft/world/entity/player/Player.java ++++ b/net/minecraft/world/entity/player/Player.java +@@ -673,7 +673,7 @@ public abstract class Player extends LivingEntity { + + public void increaseScore(int score) { + int score1 = this.getScore(); +- this.entityData.set(DATA_SCORE_ID, score1 + score); ++ this.entityData.set(DATA_SCORE_ID, score1 + score, 0); // Slice - packet obfuscation and reduction + } + + public void startAutoSpinAttack(int ticks, float damage, ItemStack itemStack) { diff --git a/sakura-server/minecraft-patches/features/0004-Optimise-paper-explosions.patch b/sakura-server/minecraft-patches/features/0004-Optimise-paper-explosions.patch new file mode 100644 index 0000000..2965781 --- /dev/null +++ b/sakura-server/minecraft-patches/features/0004-Optimise-paper-explosions.patch @@ -0,0 +1,160 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Samsuik +Date: Fri, 19 Apr 2024 22:20:03 +0100 +Subject: [PATCH] Optimise paper explosions + + +diff --git a/net/minecraft/world/level/ServerExplosion.java b/net/minecraft/world/level/ServerExplosion.java +index df584256e133bfc0b7effd56961f1b91c264c7bd..9f06e32101f494d94d9210210255d5d72ca4ff36 100644 +--- a/net/minecraft/world/level/ServerExplosion.java ++++ b/net/minecraft/world/level/ServerExplosion.java +@@ -88,7 +88,7 @@ public class ServerExplosion implements Explosion { + } + } + +- CACHED_RAYS = rayCoords.toDoubleArray(); ++ CACHED_RAYS = sortExplosionRays(rayCoords); // Sakura - optimise paper explosions + } + + private static final int CHUNK_CACHE_SHIFT = 2; +@@ -307,6 +307,39 @@ public class ServerExplosion implements Explosion { + } + // Paper end - collisions optimisations + private final boolean consistentRadius; // Sakura - consistent explosion radius ++ // Sakura start - optimise paper explosions ++ /* ++ * Sort the explosion rays to better utilise the chunk and block cache. ++ * x + Vanilla Sorted ++ * z @ z 8 5 ++ * - x 6 7 6 4 ++ * 4 @ 5 7 @ 3 ++ * 2 3 8 2 ++ * 1 1 ++ */ ++ private static double[] sortExplosionRays(it.unimi.dsi.fastutil.doubles.DoubleArrayList rayCoords) { ++ List explosionRays = new ArrayList<>(); ++ ++ for (int i = 0; i < rayCoords.size(); i += 3) { ++ double[] ray = new double[3]; ++ rayCoords.getElements(i, ray, 0, 3); ++ explosionRays.add(ray); ++ } ++ ++ rayCoords.clear(); ++ explosionRays.sort(java.util.Comparator.comparingDouble(vec -> { ++ double sign = Math.signum(vec[0]); ++ double dir = (sign - 1) / 2; ++ return sign + 8 + vec[2] * dir; ++ })); ++ ++ double[] rays = new double[explosionRays.size() * 3]; ++ for (int i = 0; i < explosionRays.size() * 3; i++) { ++ rays[i] = explosionRays.get(i / 3)[i % 3]; ++ } ++ return rays; ++ } ++ // Sakura end - optimise paper explosions + + public ServerExplosion( + ServerLevel level, +@@ -398,6 +431,12 @@ public class ServerExplosion implements Explosion { + initialCache = this.getOrCacheExplosionBlock(blockX, blockY, blockZ, key, true); + } + ++ // Sakura start - optimise paper explosions ++ if (!this.interactsWithBlocks() || initialCache.resistance > (this.radius * 1.3f)) { ++ return ret; ++ } ++ // Sakura end - optimise paper explosions ++ + // only ~1/3rd of the loop iterations in vanilla will result in a ray, as it is iterating the perimeter of + // a 16x16x16 cube + // we can cache the rays and their normals as well, so that we eliminate the excess iterations / checks and +@@ -477,16 +516,55 @@ public class ServerExplosion implements Explosion { + // Paper end - collision optimisations + } + +- private void hurtEntities() { +- float f = this.radius * 2.0F; ++ // Sakura start - optimise paper explosions ++ protected final AABB getExplosionBounds(float f) { + int floor = Mth.floor(this.center.x - f - 1.0); + int floor1 = Mth.floor(this.center.x + f + 1.0); + int floor2 = Mth.floor(this.center.y - f - 1.0); + int floor3 = Mth.floor(this.center.y + f + 1.0); + int floor4 = Mth.floor(this.center.z - f - 1.0); + int floor5 = Mth.floor(this.center.z + f + 1.0); +- List list = this.level.getEntities(excludeSourceFromDamage ? this.source : null, new AABB(floor, floor2, floor4, floor1, floor3, floor5), entity -> entity.isAlive() && !entity.isSpectator()); // Paper - Fix lag from explosions processing dead entities, Allow explosions to damage source +- for (Entity entity : list) { // Paper - used in loop ++ return new AABB(floor, floor2, floor4, floor1, floor3, floor5); ++ } ++ ++ private void hurtEntities() { ++ float f = this.radius * 2.0F; ++ ++ int minSection = ca.spottedleaf.moonrise.common.util.WorldUtil.getMinSection(this.level); ++ int maxSection = ca.spottedleaf.moonrise.common.util.WorldUtil.getMaxSection(this.level); ++ ++ int minChunkX = Mth.floor(this.center.x - f) >> 4; ++ int maxChunkX = Mth.floor(this.center.x + f) >> 4; ++ int minChunkY = Mth.clamp(Mth.floor(this.center.y - f) >> 4, minSection, maxSection); ++ int maxChunkY = Mth.clamp(Mth.floor(this.center.y + f) >> 4, minSection, maxSection); ++ int minChunkZ = Mth.floor(this.center.z - f) >> 4; ++ int maxChunkZ = Mth.floor(this.center.z + f) >> 4; ++ ++ ca.spottedleaf.moonrise.patches.chunk_system.level.entity.EntityLookup entityLookup = this.level.moonrise$getEntityLookup(); ++ for (int chunkX = minChunkX; chunkX <= maxChunkX; ++chunkX) { ++ for (int chunkZ = minChunkZ; chunkZ <= maxChunkZ; ++chunkZ) { ++ ca.spottedleaf.moonrise.patches.chunk_system.level.entity.ChunkEntitySlices chunk = entityLookup.getChunk(chunkX, chunkZ); ++ if (chunk == null) continue; // empty slice ++ ++ for (int chunkY = minChunkY; chunkY <= maxChunkY; ++chunkY) { ++ this.impactEntities(f, chunk.getSectionEntities(chunkY)); ++ } ++ } ++ } ++ } ++ ++ protected final void impactEntities(float f, Entity[] entities) { ++ for (int i = 0; i < entities.length; i++) { ++ Entity entity = entities[i]; ++ if (entity == null) break; // end of entity section ++ this.impactEntity(f, entity); ++ if (entity != entities[i]) i--; // entities can be removed mid-explosion ++ } ++ } ++ ++ protected final void impactEntity(float f, Entity entity) { ++ if (entity.isAlive() && !entity.isSpectator() && (!this.excludeSourceFromDamage || entity != this.source)) { // Paper - Fix lag from explosions processing dead entities, Allow explosions to damage source ++ // Sakura end - optimise paper explosions + if (!entity.ignoreExplosion(this)) { + double d = Math.sqrt(entity.distanceToSqr(this.center)) / f; + if (d <= 1.0) { +@@ -511,15 +589,16 @@ public class ServerExplosion implements Explosion { + // - Damaging EntityEnderDragon does nothing + // - EnderDragon hitbock always covers the other parts and is therefore always present + if (entity instanceof EnderDragonPart) { +- continue; ++ return; // Sakura - optimise paper explosions + } + + entity.lastDamageCancelled = false; + + if (entity instanceof EnderDragon) { ++ final AABB bounds = this.getExplosionBounds(f); // Sakura - optimise paper explosions + for (EnderDragonPart dragonPart : ((EnderDragon) entity).getSubEntities()) { + // Calculate damage separately for each EntityComplexPart +- if (list.contains(dragonPart)) { ++ if (dragonPart.getBoundingBox().intersects(bounds)) { // Sakura - optimise paper explosions + dragonPart.hurtServer(this.level, this.damageSource, this.damageCalculator.getEntityDamageAmount(this, entity, f1)); + } + } +@@ -528,7 +607,7 @@ public class ServerExplosion implements Explosion { + } + + if (entity.lastDamageCancelled) { // SPIGOT-5339, SPIGOT-6252, SPIGOT-6777: Skip entity if damage event was cancelled +- continue; ++ return; // Sakura - optimise paper explosions + } + // CraftBukkit end + } diff --git a/sakura-server/minecraft-patches/features/0005-Store-Entity-Data-State.patch b/sakura-server/minecraft-patches/features/0005-Store-Entity-Data-State.patch new file mode 100644 index 0000000..14ed289 --- /dev/null +++ b/sakura-server/minecraft-patches/features/0005-Store-Entity-Data-State.patch @@ -0,0 +1,48 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Samsuik +Date: Wed, 16 Aug 2023 22:34:49 +0100 +Subject: [PATCH] Store Entity Data/State + + +diff --git a/net/minecraft/world/entity/Entity.java b/net/minecraft/world/entity/Entity.java +index 7bbc4a982f442fdb9821221442737ae65e55289e..3671003b9fdf787fc5095d12df9ee2f15bd998f1 100644 +--- a/net/minecraft/world/entity/Entity.java ++++ b/net/minecraft/world/entity/Entity.java +@@ -544,6 +544,25 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + return flags; + } + // Sakura end - load chunks on movement ++ // Sakura start - store entity data/state ++ private me.samsuik.sakura.entity.EntityState entityState = null; ++ ++ public final Vec3 stuckSpeedMultiplier() { ++ return this.stuckSpeedMultiplier; ++ } ++ ++ public final void storeEntityState() { ++ this.entityState = me.samsuik.sakura.entity.EntityState.of(this); ++ } ++ ++ public final me.samsuik.sakura.entity.EntityState entityState() { ++ return this.entityState; ++ } ++ ++ public final boolean compareState(Entity to) { ++ return to.entityState() != null && to.entityState().comparePositionAndMotion(this); ++ } ++ // Sakura end - store entity data/state + + public Entity(EntityType entityType, Level level) { + this.type = entityType; +diff --git a/net/minecraft/world/level/Level.java b/net/minecraft/world/level/Level.java +index 3ba2228fd83bcd9af5f46d71409abe9fd2328f7e..e4584cd58d84f2b64748dfec7a1aa69fca119021 100644 +--- a/net/minecraft/world/level/Level.java ++++ b/net/minecraft/world/level/Level.java +@@ -1510,6 +1510,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl + + public void guardEntityTick(Consumer consumerEntity, T entity) { + try { ++ entity.storeEntityState(); // Sakura - store entity data/state + consumerEntity.accept(entity); + } catch (Throwable var6) { + // Paper start - Prevent block entity and entity crashes diff --git a/sakura-server/minecraft-patches/features/0006-Merge-Cannon-Entities.patch b/sakura-server/minecraft-patches/features/0006-Merge-Cannon-Entities.patch new file mode 100644 index 0000000..971fa7a --- /dev/null +++ b/sakura-server/minecraft-patches/features/0006-Merge-Cannon-Entities.patch @@ -0,0 +1,343 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Samsuik +Date: Sat, 9 Sep 2023 18:39:15 +0100 +Subject: [PATCH] Merge Cannon Entities + + +diff --git a/net/minecraft/server/MinecraftServer.java b/net/minecraft/server/MinecraftServer.java +index f1f322f623756f0c0a06a4207a12765c9623e151..ae1fad06d85de83f53884449cff21fc0ae62bf97 100644 +--- a/net/minecraft/server/MinecraftServer.java ++++ b/net/minecraft/server/MinecraftServer.java +@@ -1750,6 +1750,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop { +@@ -828,6 +829,15 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + entity.stopRiding(); + } + ++ // Sakura start - merge cannon entities ++ Entity previous = previousEntity[0]; ++ if (this.mergeHandler.tryMerge(entity, previous)) { ++ return; ++ } else { ++ previousEntity[0] = entity; ++ } ++ // Sakura end - merge cannon entities ++ + profilerFiller.push("tick"); + this.guardEntityTick(this::tickNonPassenger, entity); + profilerFiller.pop(); +diff --git a/net/minecraft/world/entity/Entity.java b/net/minecraft/world/entity/Entity.java +index 3671003b9fdf787fc5095d12df9ee2f15bd998f1..2a617bd6d5d14cd69b149d6c5f82f8b2c3bc2d5d 100644 +--- a/net/minecraft/world/entity/Entity.java ++++ b/net/minecraft/world/entity/Entity.java +@@ -563,6 +563,23 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + return to.entityState() != null && to.entityState().comparePositionAndMotion(this); + } + // Sakura end - store entity data/state ++ // Sakura start - merge cannon entities ++ public final void updateBukkitHandle(Entity entity) { ++ if (this.bukkitEntity != null) { ++ this.bukkitEntity.setHandle(entity); ++ } ++ this.bukkitEntity = entity.getBukkitEntity(); ++ } ++ ++ public final long getPackedOriginPosition() { ++ org.bukkit.util.Vector origin = this.getOriginVector(); ++ if (origin != null) { ++ return BlockPos.asLong(origin.getBlockX(), origin.getBlockY(), origin.getBlockZ()); ++ } else { ++ return Long.MIN_VALUE; ++ } ++ } ++ // Sakura end - merge cannon entities + + public Entity(EntityType entityType, Level level) { + this.type = entityType; +@@ -4964,6 +4981,11 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + if (this.removalReason != Entity.RemovalReason.UNLOADED_TO_CHUNK) { this.getPassengers().forEach(Entity::stopRiding); } // Paper - rewrite chunk system + this.levelCallback.onRemove(removalReason); + this.onRemoval(removalReason); ++ // Sakura start - merge cannon entities ++ if (removalReason == RemovalReason.DISCARDED) { ++ this.level.mergeHandler.removeEntity(this); ++ } ++ // Sakura end - merge cannon entities + // Paper start - Folia schedulers + if (!(this instanceof ServerPlayer) && removalReason != RemovalReason.CHANGED_DIMENSION && !alreadyRemoved) { + // Players need to be special cased, because they are regularly removed from the world +diff --git a/net/minecraft/world/entity/item/FallingBlockEntity.java b/net/minecraft/world/entity/item/FallingBlockEntity.java +index 4836be29ff5e87e4f2b9beb0d4a9943281ab5262..1d9afcf995ee734f13803e26956439e5c3450f44 100644 +--- a/net/minecraft/world/entity/item/FallingBlockEntity.java ++++ b/net/minecraft/world/entity/item/FallingBlockEntity.java +@@ -54,7 +54,7 @@ import org.bukkit.craftbukkit.event.CraftEventFactory; + import org.bukkit.event.entity.EntityRemoveEvent; + // CraftBukkit end + +-public class FallingBlockEntity extends Entity { ++public class FallingBlockEntity extends Entity implements me.samsuik.sakura.entity.merge.MergeableEntity { // Sakura - merge cannon entities + private static final Logger LOGGER = LogUtils.getLogger(); + public BlockState blockState = Blocks.SAND.defaultBlockState(); + public int time; +@@ -70,11 +70,62 @@ public class FallingBlockEntity extends Entity { + public boolean autoExpire = true; // Paper - Expand FallingBlock API + public boolean heightParity; // Sakura - falling block height parity api + ++ // Sakura start - merge cannon entities ++ private final me.samsuik.sakura.entity.merge.MergeEntityData mergeData = new me.samsuik.sakura.entity.merge.MergeEntityData(this); ++ ++ @Override ++ public final me.samsuik.sakura.entity.merge.MergeEntityData getMergeEntityData() { ++ return this.mergeData; ++ } ++ ++ @Override ++ public final boolean isSafeToMergeInto(me.samsuik.sakura.entity.merge.MergeableEntity entity, boolean ticksLived) { ++ return entity instanceof FallingBlockEntity fbe ++ && fbe.blockState.equals(this.blockState) ++ && (!ticksLived || fbe.time - 1 == this.time); ++ } ++ ++ @Override ++ public final void respawnEntity(int count) { ++ while (count-- >= 1) { ++ // Unlike PrimedTnt we have to try respawn each stacked entity ++ FallingBlockEntity fallingBlock = new FallingBlockEntity(EntityType.FALLING_BLOCK, this.level()); ++ ++ // Try to stack the falling block ++ this.entityState().apply(fallingBlock); ++ fallingBlock.blockState = this.blockState; ++ fallingBlock.spawnReason = this.spawnReason; ++ fallingBlock.time = this.time - 1; ++ fallingBlock.tick(); ++ ++ // If you horizontal stack into a moving piston block this condition will be met. ++ if (!fallingBlock.isRemoved()) { ++ this.mergeData.setCount(count + 1); ++ fallingBlock.storeEntityState(); ++ fallingBlock.entityState().apply(this); ++ break; ++ } else if (count == 0) { ++ this.discard(EntityRemoveEvent.Cause.DESPAWN); ++ } ++ } ++ } ++ ++ @Override ++ public @Nullable ItemEntity spawnAtLocation(ServerLevel level, net.minecraft.world.level.ItemLike item) { // may be overridden by plugins ++ ItemEntity itemEntity = null; ++ for (int i = 0; i < this.mergeData.getCount(); ++i) { ++ itemEntity = super.spawnAtLocation(level, item); ++ } ++ return itemEntity; ++ } ++ // Sakura end - merge cannon entities ++ + public FallingBlockEntity(EntityType entityType, Level level) { + super(entityType, level); + this.heightParity = level.sakuraConfig().cannons.mechanics.fallingBlockParity; // Sakura - configure cannon mechanics + this.isFallingBlock = true; // Sakura - client visibility settings + this.loadChunks = level.sakuraConfig().cannons.loadChunks; // Sakura - load chunks on movement ++ this.mergeData.setMergeLevel(level.sakuraConfig().cannons.mergeLevel); // Sakura - merge cannon entities + } + + public FallingBlockEntity(Level level, double x, double y, double z, BlockState state) { +@@ -222,6 +273,7 @@ public class FallingBlockEntity extends Entity { + return; + } + // CraftBukkit end ++ if (this.respawnEntity()) return; // Sakura - merge cannon entities + if (this.level().setBlock(blockPos, this.blockState, 3)) { + ((ServerLevel)this.level()) + .getChunkSource() +@@ -328,6 +380,7 @@ public class FallingBlockEntity extends Entity { + + compound.putBoolean("CancelDrop", this.cancelDrop); + if (!this.autoExpire) compound.putBoolean("Paper.AutoExpire", false); // Paper - Expand FallingBlock API ++ compound.putInt("merge_count", this.mergeData.getCount()); // Sakura - merge cannon entities + } + + @Override +@@ -360,6 +413,11 @@ public class FallingBlockEntity extends Entity { + this.autoExpire = compound.getBoolean("Paper.AutoExpire"); + } + // Paper end - Expand FallingBlock API ++ // Sakura start - merge cannon entities ++ if (compound.contains("merge_count", 3)) { ++ this.mergeData.setCount(compound.getInt("merge_count")); ++ } ++ // Sakura end - merge cannon entities + } + + public void setHurtsEntities(float fallDamagePerDistance, int fallDamageMax) { +diff --git a/net/minecraft/world/entity/item/PrimedTnt.java b/net/minecraft/world/entity/item/PrimedTnt.java +index 9e9463d62aa1618a4a749bb7e2636c9b090991e9..b378b4c4930c4ebd55795591aca173fd1fee46c9 100644 +--- a/net/minecraft/world/entity/item/PrimedTnt.java ++++ b/net/minecraft/world/entity/item/PrimedTnt.java +@@ -33,7 +33,7 @@ import org.bukkit.event.entity.EntityRemoveEvent; + import org.bukkit.event.entity.ExplosionPrimeEvent; + // CraftBukkit end + +-public class PrimedTnt extends Entity implements TraceableEntity { ++public class PrimedTnt extends Entity implements TraceableEntity, me.samsuik.sakura.entity.merge.MergeableEntity { // Sakura - merge cannon entities + private static final EntityDataAccessor DATA_FUSE_ID = SynchedEntityData.defineId(PrimedTnt.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_BLOCK_STATE_ID = SynchedEntityData.defineId(PrimedTnt.class, EntityDataSerializers.BLOCK_STATE); + private static final int DEFAULT_FUSE_TIME = 80; +@@ -58,11 +58,48 @@ public class PrimedTnt extends Entity implements TraceableEntity { + public float explosionPower = 4.0F; + public boolean isIncendiary = false; // CraftBukkit - add field + ++ // Sakura start - merge cannon entities ++ private final me.samsuik.sakura.entity.merge.MergeEntityData mergeData = new me.samsuik.sakura.entity.merge.MergeEntityData(this); ++ ++ @Override ++ public final me.samsuik.sakura.entity.merge.MergeEntityData getMergeEntityData() { ++ return this.mergeData; ++ } ++ ++ @Override ++ public final boolean isSafeToMergeInto(me.samsuik.sakura.entity.merge.MergeableEntity entity, boolean ticksLived) { ++ return entity instanceof PrimedTnt tnt ++ && tnt.getFuse() + 1 == this.getFuse() ++ // required to prevent issues with powdered snow ++ && (tnt.entityState().fallDistance() == this.fallDistance ++ || tnt.entityState().fallDistance() > 2.5f && this.fallDistance > 2.5f); ++ } ++ ++ @Override ++ public final void respawnEntity(int count) { ++ PrimedTnt tnt = new PrimedTnt(EntityType.TNT, this.level()); ++ tnt.updateBukkitHandle(this); // update handle for plugins ++ while (count-- > 1) { ++ this.setFuse(100); // Prevent unwanted explosions while ticking ++ ++ // Cause an explosion to affect this entity ++ tnt.setPos(this.position()); ++ tnt.setDeltaMovement(this.getDeltaMovement()); ++ this.entityState().apply(this); ++ tnt.explode(); ++ this.storeEntityState(); ++ ++ this.tick(); ++ } ++ } ++ // Sakura end - merge cannon entities ++ + public PrimedTnt(EntityType entityType, Level level) { + super(entityType, level); + this.blocksBuilding = true; + this.isPrimedTNT = true; // Sakura - client visibility settings + this.loadChunks = level.sakuraConfig().cannons.loadChunks; // Sakura - load chunks on movement ++ this.mergeData.setMergeLevel(level.sakuraConfig().cannons.mergeLevel); // Sakura - merge cannon entities + } + + public PrimedTnt(Level level, double x, double y, double z, @Nullable LivingEntity owner) { +@@ -142,6 +179,7 @@ public class PrimedTnt extends Entity implements TraceableEntity { + if (i <= 0) { + // CraftBukkit start - Need to reverse the order of the explosion and the entity death so we have a location for the event + //this.discard(); ++ this.respawnEntity(); // Sakura - merge cannon entities + if (!this.level().isClientSide) { + this.explode(); + } +@@ -212,6 +250,7 @@ public class PrimedTnt extends Entity implements TraceableEntity { + if (this.explosionPower != 4.0F) { + compound.putFloat("explosion_power", this.explosionPower); + } ++ compound.putInt("merge_count", this.mergeData.getCount()); // Sakura - merge cannon entities + } + + @Override +@@ -224,6 +263,11 @@ public class PrimedTnt extends Entity implements TraceableEntity { + if (compound.contains("explosion_power", 99)) { + this.explosionPower = Mth.clamp(compound.getFloat("explosion_power"), 0.0F, 128.0F); + } ++ // Sakura start - merge cannon entities ++ if (compound.contains("merge_count", 3)) { ++ this.mergeData.setCount(compound.getInt("merge_count")); ++ } ++ // Sakura end - merge cannon entities + } + + @Nullable +diff --git a/net/minecraft/world/level/Level.java b/net/minecraft/world/level/Level.java +index e4584cd58d84f2b64748dfec7a1aa69fca119021..a4d7c95514c3db5799efe178efee796413d1bac8 100644 +--- a/net/minecraft/world/level/Level.java ++++ b/net/minecraft/world/level/Level.java +@@ -837,6 +837,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl + return chunk != null ? chunk.getNoiseBiome(x, y, z) : this.getUncachedNoiseBiome(x, y, z); + } + // Paper end - optimise random ticking ++ public final me.samsuik.sakura.entity.merge.EntityMergeHandler mergeHandler = new me.samsuik.sakura.entity.merge.EntityMergeHandler(); // Sakura - merge cannon entities + + protected Level( + WritableLevelData levelData, +diff --git a/net/minecraft/world/level/block/BasePressurePlateBlock.java b/net/minecraft/world/level/block/BasePressurePlateBlock.java +index 108c1d23bf80777b943edfa0b5585ebb928540a7..69d490c79e30fb42da69bbd804ecaea7b88fe7b0 100644 +--- a/net/minecraft/world/level/block/BasePressurePlateBlock.java ++++ b/net/minecraft/world/level/block/BasePressurePlateBlock.java +@@ -91,7 +91,7 @@ public abstract class BasePressurePlateBlock extends Block { + } + + private void checkPressed(@Nullable Entity entity, Level level, BlockPos pos, BlockState state, int currentSignal) { +- int signalStrength = this.getSignalStrength(level, pos); ++ int signalStrength = this.getSignalStrength(level, pos, currentSignal == 0); // Sakura - merge cannon entities + boolean flag = currentSignal > 0; + boolean flag1 = signalStrength > 0; + +@@ -168,6 +168,12 @@ public abstract class BasePressurePlateBlock extends Block { + // CraftBukkit end + } + ++ // Sakura start - merge cannon entities ++ protected int getSignalStrength(Level world, BlockPos pos, boolean entityInside) { ++ return this.getSignalStrength(world, pos); ++ } ++ // Sakura end - merge cannon entities ++ + protected abstract int getSignalStrength(Level level, BlockPos pos); + + protected abstract int getSignalForState(BlockState state); +diff --git a/net/minecraft/world/level/block/WeightedPressurePlateBlock.java b/net/minecraft/world/level/block/WeightedPressurePlateBlock.java +index 0ad494a861c04aeacb0620000e306cfab813fdde..c49044097fa8d3294de10a681717cd424e6c1078 100644 +--- a/net/minecraft/world/level/block/WeightedPressurePlateBlock.java ++++ b/net/minecraft/world/level/block/WeightedPressurePlateBlock.java +@@ -40,6 +40,11 @@ public class WeightedPressurePlateBlock extends BasePressurePlateBlock { + + @Override + protected int getSignalStrength(Level level, BlockPos pos) { ++ // Sakura start - merge cannon entities ++ return this.getSignalStrength(level, pos, false); ++ } ++ protected final int getSignalStrength(Level level, BlockPos pos, boolean entityInside) { ++ // Sakura end - merge cannon entities + // CraftBukkit start + // int min = Math.min(getEntityCount(level, TOUCH_AABB.move(pos), Entity.class), this.maxWeight); + int min = 0; +@@ -55,7 +60,7 @@ public class WeightedPressurePlateBlock extends BasePressurePlateBlock { + + // We only want to block turning the plate on if all events are cancelled + if (!cancellable.isCancelled()) { +- min++; ++ min += !entityInside && entity instanceof me.samsuik.sakura.entity.merge.MergeableEntity mergeEntity ? mergeEntity.getMergeEntityData().getCount() : 1; // Sakura - merge cannon entities + } + } + diff --git a/sakura-server/paper-patches/features/0001-Client-Visibility-Settings.patch b/sakura-server/paper-patches/features/0001-Client-Visibility-Settings.patch new file mode 100644 index 0000000..2d95576 --- /dev/null +++ b/sakura-server/paper-patches/features/0001-Client-Visibility-Settings.patch @@ -0,0 +1,50 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Samsuik +Date: Tue, 21 Sep 2021 23:54:25 +0100 +Subject: [PATCH] Client Visibility Settings + + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java +index dab4d38a2408af5f8f415e5a916845bf08ec536c..7f8bbddc7b5b0c8d13a2772b91df44d6e99aadf3 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java +@@ -2385,6 +2385,13 @@ public class CraftPlayer extends CraftHumanEntity implements Player { + handle.keepLevel = data.getBoolean("keepLevel"); + } + } ++ ++ // Sakura start - client visibility settings; load from nbt ++ if (nbttagcompound.contains("sakura", 10)) { ++ CompoundTag sakuraTag = nbttagcompound.getCompound("sakura"); ++ this.getHandle().visibilitySettings.loadData(sakuraTag); ++ } ++ // Sakura end - client visibility settings; load from nbt + } + + public void setExtraData(CompoundTag nbttagcompound) { +@@ -2414,6 +2421,11 @@ public class CraftPlayer extends CraftHumanEntity implements Player { + paper.putLong("LastLogin", handle.loginTime); + paper.putLong("LastSeen", System.currentTimeMillis()); + // Paper end ++ // Sakura start - client visibility settings; save to nbt ++ CompoundTag sakuraTag = nbttagcompound.getCompound("sakura"); ++ this.getHandle().visibilitySettings.saveData(sakuraTag); ++ nbttagcompound.put("sakura", sakuraTag); ++ // Sakura end - client visibility settings; save to nbt + } + + @Override +@@ -3085,6 +3097,13 @@ public class CraftPlayer extends CraftHumanEntity implements Player { + return this.getHandle().allowsListing(); + } + ++ // Sakura start - client visibility settings; api ++ @Override ++ public final me.samsuik.sakura.player.visibility.VisibilitySettings getVisibility() { ++ return this.getHandle().visibilitySettings; ++ } ++ // Sakura end - client visibility settings; api ++ + // Paper start + @Override + public net.kyori.adventure.text.Component displayName() { diff --git a/sakura-server/paper-patches/features/0002-Merge-Cannon-Entities.patch b/sakura-server/paper-patches/features/0002-Merge-Cannon-Entities.patch new file mode 100644 index 0000000..67233a1 --- /dev/null +++ b/sakura-server/paper-patches/features/0002-Merge-Cannon-Entities.patch @@ -0,0 +1,72 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Samsuik +Date: Sat, 9 Sep 2023 18:39:15 +0100 +Subject: [PATCH] Merge Cannon Entities + + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftFallingBlock.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftFallingBlock.java +index 22b6016a8d6828b2b10c028b24fd160b3b9f9f59..ad924326b6bfd01b719096ff53ed4b8e513e25af 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftFallingBlock.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftFallingBlock.java +@@ -26,6 +26,28 @@ public class CraftFallingBlock extends CraftEntity implements FallingBlock { + } + // Sakura end - falling block height parity api + ++ // Sakura start - merge cannon entities ++ @Override ++ public @org.jetbrains.annotations.NotNull me.samsuik.sakura.entity.merge.MergeLevel getMergeLevel() { ++ return this.getHandle().getMergeEntityData().getMergeLevel(); ++ } ++ ++ @Override ++ public void setMergeLevel(@org.jetbrains.annotations.NotNull me.samsuik.sakura.entity.merge.MergeLevel level) { ++ this.getHandle().getMergeEntityData().setMergeLevel(level); ++ } ++ ++ @Override ++ public int getStacked() { ++ return this.getHandle().getMergeEntityData().getCount(); ++ } ++ ++ @Override ++ public void setStacked(int stacked) { ++ this.getHandle().getMergeEntityData().setCount(stacked); ++ } ++ // Sakura end - merge cannon entities ++ + @Override + public FallingBlockEntity getHandle() { + return (FallingBlockEntity) this.entity; +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftTNTPrimed.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftTNTPrimed.java +index a61aec087fa7cec27a803668bdc1b9e6eb336755..c6f36ab2368d0e2e4555d5f8edc0132dcb61a53c 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftTNTPrimed.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftTNTPrimed.java +@@ -12,6 +12,28 @@ public class CraftTNTPrimed extends CraftEntity implements TNTPrimed { + super(server, entity); + } + ++ // Sakura start - merge cannon entities ++ @Override ++ public @org.jetbrains.annotations.NotNull me.samsuik.sakura.entity.merge.MergeLevel getMergeLevel() { ++ return this.getHandle().getMergeEntityData().getMergeLevel(); ++ } ++ ++ @Override ++ public void setMergeLevel(@org.jetbrains.annotations.NotNull me.samsuik.sakura.entity.merge.MergeLevel level) { ++ this.getHandle().getMergeEntityData().setMergeLevel(level); ++ } ++ ++ @Override ++ public int getStacked() { ++ return this.getHandle().getMergeEntityData().getCount(); ++ } ++ ++ @Override ++ public void setStacked(int stacked) { ++ this.getHandle().getMergeEntityData().setCount(stacked); ++ } ++ // Sakura end - merge cannon entities ++ + @Override + public float getYield() { + return this.getHandle().explosionPower; diff --git a/sakura-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java.patch b/sakura-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java.patch index 52fab98..627c305 100644 --- a/sakura-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java.patch +++ b/sakura-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java.patch @@ -1,6 +1,6 @@ --- a/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java +++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java -@@ -115,6 +_,18 @@ +@@ -116,6 +_,18 @@ throw new AssertionError("Unknown entity " + (entity == null ? null : entity.getClass())); } diff --git a/sakura-server/src/main/java/me/samsuik/sakura/command/SakuraCommands.java b/sakura-server/src/main/java/me/samsuik/sakura/command/SakuraCommands.java index 30393fd..ee42cca 100644 --- a/sakura-server/src/main/java/me/samsuik/sakura/command/SakuraCommands.java +++ b/sakura-server/src/main/java/me/samsuik/sakura/command/SakuraCommands.java @@ -2,6 +2,9 @@ package me.samsuik.sakura.command; import me.samsuik.sakura.command.subcommands.ConfigCommand; import me.samsuik.sakura.command.subcommands.TPSCommand; +import me.samsuik.sakura.command.subcommands.FPSCommand; +import me.samsuik.sakura.command.subcommands.VisualCommand; +import me.samsuik.sakura.player.visibility.VisibilityTypes; import net.minecraft.server.MinecraftServer; import org.bukkit.command.Command; @@ -14,6 +17,9 @@ public final class SakuraCommands { COMMANDS.put("sakura", new SakuraCommand("sakura")); COMMANDS.put("config", new ConfigCommand("config")); COMMANDS.put("tps", new TPSCommand("tps")); + COMMANDS.put("fps", new FPSCommand("fps")); + COMMANDS.put("tntvisibility", new VisualCommand(VisibilityTypes.TNT, "tnttoggle")); + COMMANDS.put("sandvisibility", new VisualCommand(VisibilityTypes.SAND, "sandtoggle")); } public static void registerCommands(MinecraftServer server) { diff --git a/sakura-server/src/main/java/me/samsuik/sakura/command/subcommands/FPSCommand.java b/sakura-server/src/main/java/me/samsuik/sakura/command/subcommands/FPSCommand.java new file mode 100644 index 0000000..a85f84e --- /dev/null +++ b/sakura-server/src/main/java/me/samsuik/sakura/command/subcommands/FPSCommand.java @@ -0,0 +1,24 @@ +package me.samsuik.sakura.command.subcommands; + +import me.samsuik.sakura.command.BaseSubCommand; +import me.samsuik.sakura.player.visibility.VisibilityGui; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.framework.qual.DefaultQualifier; + +@DefaultQualifier(NonNull.class) +public final class FPSCommand extends BaseSubCommand { + private final VisibilityGui visibilityGui = new VisibilityGui(); + + public FPSCommand(String name) { + super(name); + } + + @Override + public void execute(CommandSender sender, String[] args) { + if (sender instanceof Player player) { + this.visibilityGui.showTo(player); + } + } +} diff --git a/sakura-server/src/main/java/me/samsuik/sakura/command/subcommands/VisualCommand.java b/sakura-server/src/main/java/me/samsuik/sakura/command/subcommands/VisualCommand.java new file mode 100644 index 0000000..cd9ebbb --- /dev/null +++ b/sakura-server/src/main/java/me/samsuik/sakura/command/subcommands/VisualCommand.java @@ -0,0 +1,41 @@ +package me.samsuik.sakura.command.subcommands; + +import me.samsuik.sakura.command.BaseSubCommand; +import me.samsuik.sakura.configuration.GlobalConfiguration; +import me.samsuik.sakura.player.visibility.VisibilitySettings; +import me.samsuik.sakura.player.visibility.VisibilityState; +import me.samsuik.sakura.player.visibility.VisibilityType; +import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.framework.qual.DefaultQualifier; + +import java.util.Arrays; + +@DefaultQualifier(NonNull.class) +public final class VisualCommand extends BaseSubCommand { + private final VisibilityType type; + + public VisualCommand(VisibilityType type, String... aliases) { + super(type.key() + "visibility"); + this.setAliases(Arrays.asList(aliases)); + this.type = type; + } + + @Override + public void execute(CommandSender sender, String[] args) { + if (!(sender instanceof Player player)) { + return; + } + + VisibilitySettings settings = player.getVisibility(); + VisibilityState state = settings.toggle(type); + + String stateName = (state == VisibilityState.ON) ? "Enabled" : "Disabled"; + player.sendRichMessage(GlobalConfiguration.get().messages.fpsSettingChange, + Placeholder.unparsed("name", this.type.key()), + Placeholder.unparsed("state", stateName) + ); + } +} diff --git a/sakura-server/src/main/java/me/samsuik/sakura/entity/EntityState.java b/sakura-server/src/main/java/me/samsuik/sakura/entity/EntityState.java new file mode 100644 index 0000000..aeb5f12 --- /dev/null +++ b/sakura-server/src/main/java/me/samsuik/sakura/entity/EntityState.java @@ -0,0 +1,41 @@ +package me.samsuik.sakura.entity; + +import net.minecraft.core.BlockPos; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.NullMarked; + +import java.util.Optional; + +@NullMarked +public record EntityState(Vec3 position, Vec3 momentum, AABB bb, Vec3 stuckSpeed, Optional supportingPos, boolean onGround, float fallDistance) { + public static EntityState of(Entity entity) { + return new EntityState( + entity.position(), entity.getDeltaMovement(), entity.getBoundingBox(), + entity.stuckSpeedMultiplier(), entity.mainSupportingBlockPos, + entity.onGround(), entity.fallDistance + ); + } + + public void apply(Entity entity) { + entity.setPos(this.position); + entity.setDeltaMovement(this.momentum); + entity.setBoundingBox(this.bb); + entity.makeStuckInBlock(Blocks.AIR.defaultBlockState(), this.stuckSpeed); + entity.onGround = this.onGround; + entity.mainSupportingBlockPos = this.supportingPos; + entity.fallDistance = this.fallDistance; + } + + public void applyEntityPosition(Entity entity) { + entity.setPos(this.position); + entity.setBoundingBox(this.bb); + } + + public boolean comparePositionAndMotion(Entity entity) { + return entity.position().equals(this.position) + && entity.getDeltaMovement().equals(this.momentum); + } +} diff --git a/sakura-server/src/main/java/me/samsuik/sakura/entity/merge/EntityMergeHandler.java b/sakura-server/src/main/java/me/samsuik/sakura/entity/merge/EntityMergeHandler.java new file mode 100644 index 0000000..917c2d4 --- /dev/null +++ b/sakura-server/src/main/java/me/samsuik/sakura/entity/merge/EntityMergeHandler.java @@ -0,0 +1,76 @@ +package me.samsuik.sakura.entity.merge; + +import net.minecraft.world.entity.Entity; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public final class EntityMergeHandler { + private final TrackedMergeHistory trackedHistory = new TrackedMergeHistory(); + + /** + * Tries to merge the provided entities using the {@link MergeStrategy}. + * + * @param previous the last entity to tick + * @param entity the entity being merged + * @return success + */ + public boolean tryMerge(@Nullable Entity entity, @Nullable Entity previous) { + if (entity instanceof MergeableEntity mergeEntity && previous instanceof MergeableEntity) { + MergeEntityData mergeEntityData = mergeEntity.getMergeEntityData(); + MergeStrategy strategy = MergeStrategy.from(mergeEntityData.getMergeLevel()); + Entity into = strategy.mergeEntity(entity, previous, this.trackedHistory); + if (into instanceof MergeableEntity intoEntity && !into.isRemoved() && mergeEntity.isSafeToMergeInto(intoEntity, strategy.trackHistory())) { + return this.mergeEntity(mergeEntity, intoEntity); + } + } + + return false; + } + + /** + * Stores the merged data of the provided entities if the {@link MergeStrategy} requires it. + * + * @param entity provided entity + */ + public void removeEntity(@Nullable Entity entity) { + if (entity instanceof MergeableEntity mergeEntity) { + MergeEntityData mergeEntityData = mergeEntity.getMergeEntityData(); + MergeStrategy strategy = MergeStrategy.from(mergeEntityData.getMergeLevel()); + if (mergeEntityData.hasMerged() && strategy.trackHistory()) { + this.trackedHistory.trackHistory(entity, mergeEntityData); + } + } + } + + /** + * Called every tick and provided the current server tick to remove any unneeded merge history. + * + * @param tick server tick + */ + public void expire(int tick) { + if (tick % 200 == 0) { + this.trackedHistory.expire(tick); + } + } + + /** + * Merges the first entity into the second. The entity merge count can be retrieved through the {@link MergeEntityData}. + *

+ * This method also updates the bukkit handle so that plugins reference the first entity after the second entity has been removed. + * + * @param mergeEntity the first entity + * @param into the entity to merge into + * @return if successful + */ + public boolean mergeEntity(@NotNull MergeableEntity mergeEntity, @NotNull MergeableEntity into) { + MergeEntityData entities = mergeEntity.getMergeEntityData(); + MergeEntityData mergeInto = into.getMergeEntityData(); + mergeInto.mergeWith(entities); // merge entities together + + // discard the entity and update the bukkit handle + Entity nmsEntity = (Entity) mergeEntity; + nmsEntity.discard(); + nmsEntity.updateBukkitHandle((Entity) into); + return true; + } +} diff --git a/sakura-server/src/main/java/me/samsuik/sakura/entity/merge/MergeCondition.java b/sakura-server/src/main/java/me/samsuik/sakura/entity/merge/MergeCondition.java new file mode 100644 index 0000000..6c0c224 --- /dev/null +++ b/sakura-server/src/main/java/me/samsuik/sakura/entity/merge/MergeCondition.java @@ -0,0 +1,16 @@ +package me.samsuik.sakura.entity.merge; + +import net.minecraft.world.entity.Entity; +import org.jetbrains.annotations.NotNull; + +public interface MergeCondition { + default MergeCondition and(@NotNull MergeCondition condition) { + return (e,c,t) -> this.accept(e,c,t) && condition.accept(e,c,t); + } + + default MergeCondition or(@NotNull MergeCondition condition) { + return (e,c,t) -> this.accept(e,c,t) || condition.accept(e,c,t); + } + + boolean accept(@NotNull Entity entity, int attempts, long sinceCreation); +} diff --git a/sakura-server/src/main/java/me/samsuik/sakura/entity/merge/MergeEntityData.java b/sakura-server/src/main/java/me/samsuik/sakura/entity/merge/MergeEntityData.java new file mode 100644 index 0000000..b13e3dc --- /dev/null +++ b/sakura-server/src/main/java/me/samsuik/sakura/entity/merge/MergeEntityData.java @@ -0,0 +1,52 @@ +package me.samsuik.sakura.entity.merge; + +import it.unimi.dsi.fastutil.longs.LongOpenHashSet; +import it.unimi.dsi.fastutil.objects.ObjectArrayList; +import net.minecraft.world.entity.Entity; +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +public final class MergeEntityData { + private final Entity entity; + private List connected = new ObjectArrayList<>(); + private int count = 1; + private MergeLevel mergeLevel = MergeLevel.NONE; + + public MergeEntityData(Entity entity) { + this.entity = entity; + } + + public void mergeWith(@NotNull MergeEntityData mergeEntityData) { + this.connected.add(mergeEntityData); + this.connected.addAll(mergeEntityData.connected); + this.count += mergeEntityData.getCount(); + mergeEntityData.setCount(0); + } + + public LongOpenHashSet getOriginPositions() { + LongOpenHashSet positions = new LongOpenHashSet(); + this.connected.forEach(entityData -> positions.add(entityData.entity.getPackedOriginPosition())); + return positions; + } + + public boolean hasMerged() { + return !this.connected.isEmpty() && this.count != 0; + } + + public void setMergeLevel(MergeLevel mergeLevel) { + this.mergeLevel = mergeLevel; + } + + public MergeLevel getMergeLevel() { + return mergeLevel; + } + + public void setCount(int count) { + this.count = count; + } + + public int getCount() { + return this.count; + } +} diff --git a/sakura-server/src/main/java/me/samsuik/sakura/entity/merge/MergeStrategy.java b/sakura-server/src/main/java/me/samsuik/sakura/entity/merge/MergeStrategy.java new file mode 100644 index 0000000..3b490dd --- /dev/null +++ b/sakura-server/src/main/java/me/samsuik/sakura/entity/merge/MergeStrategy.java @@ -0,0 +1,118 @@ +package me.samsuik.sakura.entity.merge; + +import me.samsuik.sakura.utils.collections.FixedSizeCustomObjectTable; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.entity.EntityTickList; +import org.jetbrains.annotations.NotNull; + +public interface MergeStrategy { + /** + * If this merge strategy requires the merge history to be tracked. + * + * @return should track history + */ + boolean trackHistory(); + + /** + * Tries to merge the first entity into another entity. + *

+ * The first entity should always be positioned right after the second entity in the + * {@link EntityTickList}. This method should only + * be called before the first entity and after the second entity has ticked. + * + * @param entity current entity + * @param previous last entity to tick + * @return success + */ + Entity mergeEntity(@NotNull Entity entity, @NotNull Entity previous, @NotNull TrackedMergeHistory mergeHistory); + + /** + * Gets the {@link MergeStrategy} for the {@link MergeLevel}. + * + * @param level provided level + * @return strategy + */ + static MergeStrategy from(MergeLevel level) { + return switch (level) { + case NONE -> None.INSTANCE; + case STRICT -> Strict.INSTANCE; + case LENIENT -> Lenient.INSTANCE; + case SPAWN -> Spawn.INSTANCE; + }; + } + + final class None implements MergeStrategy { + private static final None INSTANCE = new None(); + + @Override + public boolean trackHistory() { + return false; + } + + @Override + public Entity mergeEntity(@NotNull Entity entity, @NotNull Entity previous, @NotNull TrackedMergeHistory mergeHistory) { + return null; + } + } + + final class Strict implements MergeStrategy { + private static final Strict INSTANCE = new Strict(); + + @Override + public boolean trackHistory() { + return false; + } + + @Override + public Entity mergeEntity(@NotNull Entity entity, @NotNull Entity previous, @NotNull TrackedMergeHistory mergeHistory) { + return entity.compareState(previous) ? previous : null; + } + } + + final class Lenient implements MergeStrategy { + private static final Lenient INSTANCE = new Lenient(); + private final FixedSizeCustomObjectTable entityTable = new FixedSizeCustomObjectTable<>(512, entity -> { + return entity.blockPosition().hashCode(); + }); + + @Override + public boolean trackHistory() { + return true; + } + + @Override + public Entity mergeEntity(@NotNull Entity entity, @NotNull Entity previous, @NotNull TrackedMergeHistory mergeHistory) { + if (entity.compareState(previous)) { + return previous; + } + + Entity nextEntity = this.entityTable.getAndWrite(entity); + if (nextEntity == null || !nextEntity.level().equals(entity.level())) { + return null; + } + + return mergeHistory.hasPreviousMerged(entity, nextEntity) && entity.compareState(nextEntity) ? nextEntity : null; + } + } + + final class Spawn implements MergeStrategy { + private static final Spawn INSTANCE = new Spawn(); + private static final MergeCondition CONDITION = (e, shots, time) -> (shots > 16 || time >= 200); + + @Override + public boolean trackHistory() { + return true; + } + + @Override + public Entity mergeEntity(@NotNull Entity entity, @NotNull Entity previous, @NotNull TrackedMergeHistory mergeHistory) { + final Entity mergeInto; + if (entity.tickCount == 1 && mergeHistory.hasPreviousMerged(entity, previous) && mergeHistory.hasMetCondition(previous, CONDITION)) { + mergeInto = previous; + } else { + mergeInto = entity.compareState(previous) ? previous : null; + } + return mergeInto; + } + } +} diff --git a/sakura-server/src/main/java/me/samsuik/sakura/entity/merge/MergeableEntity.java b/sakura-server/src/main/java/me/samsuik/sakura/entity/merge/MergeableEntity.java new file mode 100644 index 0000000..3061c3a --- /dev/null +++ b/sakura-server/src/main/java/me/samsuik/sakura/entity/merge/MergeableEntity.java @@ -0,0 +1,22 @@ +package me.samsuik.sakura.entity.merge; + +import org.jetbrains.annotations.NotNull; + +public interface MergeableEntity { + @NotNull MergeEntityData getMergeEntityData(); + + boolean isSafeToMergeInto(@NotNull MergeableEntity entity, boolean ticksLived); + + default boolean respawnEntity() { + MergeEntityData mergeData = this.getMergeEntityData(); + int count = mergeData.getCount(); + if (count > 1) { + mergeData.setCount(0); + this.respawnEntity(count); + return true; + } + return false; + } + + void respawnEntity(int count); +} diff --git a/sakura-server/src/main/java/me/samsuik/sakura/entity/merge/TrackedMergeHistory.java b/sakura-server/src/main/java/me/samsuik/sakura/entity/merge/TrackedMergeHistory.java new file mode 100644 index 0000000..aac4571 --- /dev/null +++ b/sakura-server/src/main/java/me/samsuik/sakura/entity/merge/TrackedMergeHistory.java @@ -0,0 +1,83 @@ +package me.samsuik.sakura.entity.merge; + +import it.unimi.dsi.fastutil.longs.Long2ObjectMap; +import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.longs.LongOpenHashSet; +import me.samsuik.sakura.configuration.WorldConfiguration.Cannons.Mechanics.TNTSpread; +import me.samsuik.sakura.utils.TickExpiry; +import net.minecraft.server.MinecraftServer; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.item.FallingBlockEntity; +import org.jetbrains.annotations.NotNull; + +public final class TrackedMergeHistory { + private final Long2ObjectMap historyMap = new Long2ObjectOpenHashMap<>(); + + public boolean hasPreviousMerged(@NotNull Entity entity, @NotNull Entity into) { + PositionHistory positions = this.getHistory(into); + return positions != null && positions.has(entity.getPackedOriginPosition()); + } + + public void trackHistory(@NotNull Entity entity, @NotNull MergeEntityData mergeEntityData) { + long originPosition = entity.getPackedOriginPosition(); + PositionHistory positions = this.historyMap.computeIfAbsent(originPosition, p -> new PositionHistory()); + LongOpenHashSet originPositions = mergeEntityData.getOriginPositions(); + boolean createHistory = positions.hasTimePassed(160); + if (createHistory && (entity instanceof FallingBlockEntity || entity.level().sakuraConfig().cannons.mechanics.tntSpread == TNTSpread.ALL)) { + originPositions.forEach(pos -> this.historyMap.put(pos, positions)); + } + positions.trackPositions(originPositions, !createHistory); + } + + public boolean hasMetCondition(@NotNull Entity entity, MergeCondition condition) { + PositionHistory positions = this.getHistory(entity); + return positions != null && positions.hasMetConditions(entity, condition); + } + + private PositionHistory getHistory(Entity entity) { + long originPosition = entity.getPackedOriginPosition(); + return this.historyMap.get(originPosition); + } + + public void expire(int tick) { + this.historyMap.values().removeIf(p -> p.expiry().isExpired(tick)); + } + + private static final class PositionHistory { + private final LongOpenHashSet positions = new LongOpenHashSet(); + private final TickExpiry expiry = new TickExpiry(MinecraftServer.currentTick, 200); + private final long created = MinecraftServer.currentTick; + private int cycles = 0; + + public TickExpiry expiry() { + return this.expiry; + } + + public boolean has(long position) { + this.expiry.refresh(MinecraftServer.currentTick); + return this.positions.contains(position); + } + + public void trackPositions(LongOpenHashSet positions, boolean retain) { + if (retain) { + this.positions.retainAll(positions); + } else { + this.positions.addAll(positions); + } + this.cycles++; + } + + public boolean hasMetConditions(@NotNull Entity entity, @NotNull MergeCondition condition) { + this.expiry.refresh(MinecraftServer.currentTick); + return condition.accept(entity, this.cycles, this.timeSinceCreation()); + } + + public boolean hasTimePassed(int ticks) { + return this.timeSinceCreation() > ticks; + } + + private long timeSinceCreation() { + return MinecraftServer.currentTick - this.created; + } + } +} diff --git a/sakura-server/src/main/java/me/samsuik/sakura/local/config/LocalConfigManager.java b/sakura-server/src/main/java/me/samsuik/sakura/local/config/LocalConfigManager.java index 7de60ff..598766b 100644 --- a/sakura-server/src/main/java/me/samsuik/sakura/local/config/LocalConfigManager.java +++ b/sakura-server/src/main/java/me/samsuik/sakura/local/config/LocalConfigManager.java @@ -23,7 +23,7 @@ import java.util.function.LongConsumer; public final class LocalConfigManager implements LocalStorageHandler { private static final int SMALL_REGION_SIZE = 12; private static final int CONFIG_CACHE_EXPIRATION = 600; - + private final Map storageMap = new Object2ObjectOpenHashMap<>(); private final List largeRegions = new ObjectArrayList<>(); private final Long2ObjectMap> smallRegions = new Long2ObjectOpenHashMap<>(); @@ -41,7 +41,7 @@ public final class LocalConfigManager implements LocalStorageHandler { int regionX = x >> this.regionExponent; int regionZ = z >> this.regionExponent; long regionPos = ChunkPos.asLong(regionX, regionZ); - List regions = this.smallRegions.get(regionPos); + List regions = this.smallRegions.getOrDefault(regionPos, List.of()); for (LocalRegion region : Iterables.concat(regions, this.largeRegions)) { if (region.contains(x, z)) { return Optional.of(region); diff --git a/sakura-server/src/main/java/me/samsuik/sakura/player/gui/FeatureGui.java b/sakura-server/src/main/java/me/samsuik/sakura/player/gui/FeatureGui.java new file mode 100644 index 0000000..49f53c0 --- /dev/null +++ b/sakura-server/src/main/java/me/samsuik/sakura/player/gui/FeatureGui.java @@ -0,0 +1,44 @@ +package me.samsuik.sakura.player.gui; + +import me.samsuik.sakura.player.gui.components.GuiComponent; +import net.kyori.adventure.text.Component; +import org.bukkit.entity.Player; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.inventory.Inventory; +import org.jetbrains.annotations.ApiStatus; +import org.jspecify.annotations.NullMarked; + +@NullMarked +public abstract class FeatureGui { + private final int size; + private final Component title; + + public FeatureGui(int size, Component title) { + this.size = size; + this.title = title; + } + + protected abstract void fillInventory(Inventory inventory); + + protected abstract void afterFill(Player player, FeatureGuiInventory inventory); + + public final void showTo(Player bukkitPlayer) { + FeatureGuiInventory featureInventory = new FeatureGuiInventory(this, this.size, this.title); + this.fillInventory(featureInventory.getInventory()); + this.afterFill(bukkitPlayer, featureInventory); + bukkitPlayer.openInventory(featureInventory.getInventory()); + } + + @ApiStatus.Internal + public static void clickEvent(InventoryClickEvent event) { + Inventory clicked = event.getClickedInventory(); + if (clicked != null && clicked.getHolder(false) instanceof FeatureGuiInventory featureInventory) { + event.setCancelled(true); + for (GuiComponent component : featureInventory.getComponents().reversed()) { + if (component.interaction(event, featureInventory)) { + break; + } + } + } + } +} diff --git a/sakura-server/src/main/java/me/samsuik/sakura/player/gui/FeatureGuiInventory.java b/sakura-server/src/main/java/me/samsuik/sakura/player/gui/FeatureGuiInventory.java new file mode 100644 index 0000000..6c86501 --- /dev/null +++ b/sakura-server/src/main/java/me/samsuik/sakura/player/gui/FeatureGuiInventory.java @@ -0,0 +1,90 @@ +package me.samsuik.sakura.player.gui; + +import com.google.common.base.Preconditions; +import com.google.common.collect.HashMultimap; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Multimap; +import it.unimi.dsi.fastutil.objects.Object2ObjectLinkedOpenHashMap; +import it.unimi.dsi.fastutil.objects.Object2ObjectMap; +import me.samsuik.sakura.player.gui.components.GuiComponent; +import net.kyori.adventure.text.Component; +import org.bukkit.Bukkit; +import org.bukkit.NamespacedKey; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.InventoryHolder; +import org.jspecify.annotations.NullMarked; + +import java.util.Collection; +import java.util.Optional; + +@NullMarked +public final class FeatureGuiInventory implements InventoryHolder { + private final Inventory inventory; + private final FeatureGui gui; + private final Multimap componentsUnderKey = HashMultimap.create(); + private final Object2ObjectMap componentKeys = new Object2ObjectLinkedOpenHashMap<>(); + + public FeatureGuiInventory(FeatureGui gui, int size, Component component) { + this.inventory = Bukkit.createInventory(this, size, component); + this.gui = gui; + } + + @Override + public Inventory getInventory() { + return this.inventory; + } + + public FeatureGui getGui() { + return this.gui; + } + + public ImmutableList getComponents() { + return ImmutableList.copyOf(this.componentKeys.keySet()); + } + + public ImmutableList findComponents(NamespacedKey key) { + return ImmutableList.copyOf(this.componentsUnderKey.get(key)); + } + + public Optional findFirst(NamespacedKey key) { + Collection components = this.componentsUnderKey.get(key); + return components.stream().findFirst(); + } + + public void removeComponents(NamespacedKey key) { + Collection removed = this.componentsUnderKey.removeAll(key); + for (GuiComponent component : removed) { + this.componentKeys.remove(component); + } + } + + public void addComponent(GuiComponent component, NamespacedKey key) { + Preconditions.checkArgument(!this.componentKeys.containsKey(component), "component has already been added"); + this.componentKeys.put(component, key); + this.componentsUnderKey.put(key, component); + this.inventoryUpdate(component); + } + + public void removeComponent(GuiComponent component) { + NamespacedKey key = this.componentKeys.remove(component); + this.componentsUnderKey.remove(key, component); + } + + public void replaceComponent(GuiComponent component, GuiComponent replacement) { + NamespacedKey key = this.componentKeys.remove(component); + Preconditions.checkNotNull(key, "component does not exist"); + this.componentKeys.put(replacement, key); + this.componentsUnderKey.remove(key, component); + this.componentsUnderKey.put(key, replacement); + this.inventoryUpdate(replacement); + } + + public void removeAllComponents() { + this.componentKeys.clear(); + this.componentsUnderKey.clear(); + } + + private void inventoryUpdate(GuiComponent component) { + component.creation(this.inventory); + } +} diff --git a/sakura-server/src/main/java/me/samsuik/sakura/player/gui/ItemStackUtil.java b/sakura-server/src/main/java/me/samsuik/sakura/player/gui/ItemStackUtil.java new file mode 100644 index 0000000..eddf763 --- /dev/null +++ b/sakura-server/src/main/java/me/samsuik/sakura/player/gui/ItemStackUtil.java @@ -0,0 +1,19 @@ +package me.samsuik.sakura.player.gui; + +import net.kyori.adventure.text.Component; +import org.bukkit.Material; +import org.bukkit.inventory.ItemStack; +import org.jspecify.annotations.NullMarked; + +@NullMarked +public class ItemStackUtil { + public static ItemStack itemWithBlankName(Material material) { + return itemWithName(material, Component.empty()); + } + + public static ItemStack itemWithName(Material material, Component component) { + ItemStack item = new ItemStack(material); + item.editMeta(m -> m.itemName(component)); + return item; + } +} diff --git a/sakura-server/src/main/java/me/samsuik/sakura/player/gui/components/GuiClickEvent.java b/sakura-server/src/main/java/me/samsuik/sakura/player/gui/components/GuiClickEvent.java new file mode 100644 index 0000000..39065d5 --- /dev/null +++ b/sakura-server/src/main/java/me/samsuik/sakura/player/gui/components/GuiClickEvent.java @@ -0,0 +1,10 @@ +package me.samsuik.sakura.player.gui.components; + +import me.samsuik.sakura.player.gui.FeatureGuiInventory; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.jspecify.annotations.NullMarked; + +@NullMarked +public interface GuiClickEvent { + void doSomething(InventoryClickEvent event, FeatureGuiInventory inventory); +} diff --git a/sakura-server/src/main/java/me/samsuik/sakura/player/gui/components/GuiComponent.java b/sakura-server/src/main/java/me/samsuik/sakura/player/gui/components/GuiComponent.java new file mode 100644 index 0000000..5a2ff0c --- /dev/null +++ b/sakura-server/src/main/java/me/samsuik/sakura/player/gui/components/GuiComponent.java @@ -0,0 +1,13 @@ +package me.samsuik.sakura.player.gui.components; + +import me.samsuik.sakura.player.gui.FeatureGuiInventory; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.inventory.Inventory; +import org.jspecify.annotations.NullMarked; + +@NullMarked +public interface GuiComponent { + boolean interaction(InventoryClickEvent event, FeatureGuiInventory featureInventory); + + void creation(Inventory inventory); +} diff --git a/sakura-server/src/main/java/me/samsuik/sakura/player/gui/components/ItemButton.java b/sakura-server/src/main/java/me/samsuik/sakura/player/gui/components/ItemButton.java new file mode 100644 index 0000000..6d49d33 --- /dev/null +++ b/sakura-server/src/main/java/me/samsuik/sakura/player/gui/components/ItemButton.java @@ -0,0 +1,34 @@ +package me.samsuik.sakura.player.gui.components; + +import me.samsuik.sakura.player.gui.FeatureGuiInventory; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; +import org.jspecify.annotations.NullMarked; + +@NullMarked +public final class ItemButton implements GuiComponent { + private final ItemStack bukkitItem; + private final int slot; + private final GuiClickEvent whenClicked; + + public ItemButton(ItemStack bukkitItem, int slot, GuiClickEvent whenClicked) { + this.bukkitItem = bukkitItem; + this.slot = slot; + this.whenClicked = whenClicked; + } + + @Override + public boolean interaction(InventoryClickEvent event, FeatureGuiInventory featureInventory) { + if (event.getSlot() == this.slot) { + this.whenClicked.doSomething(event, featureInventory); + return true; + } + return false; + } + + @Override + public void creation(Inventory inventory) { + inventory.setItem(this.slot, this.bukkitItem); + } +} diff --git a/sakura-server/src/main/java/me/samsuik/sakura/player/gui/components/ItemSwitch.java b/sakura-server/src/main/java/me/samsuik/sakura/player/gui/components/ItemSwitch.java new file mode 100644 index 0000000..243f1cb --- /dev/null +++ b/sakura-server/src/main/java/me/samsuik/sakura/player/gui/components/ItemSwitch.java @@ -0,0 +1,44 @@ +package me.samsuik.sakura.player.gui.components; + +import com.google.common.base.Preconditions; +import me.samsuik.sakura.player.gui.FeatureGuiInventory; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; +import org.jspecify.annotations.NullMarked; + +import java.util.Collections; +import java.util.List; + +@NullMarked +public final class ItemSwitch implements GuiComponent { + private final List items; + private final int slot; + private final int selected; + private final GuiClickEvent whenClicked; + + public ItemSwitch(List items, int slot, int selected, GuiClickEvent whenClicked) { + Preconditions.checkArgument(!items.isEmpty()); + this.items = Collections.unmodifiableList(items); + this.slot = slot; + this.selected = selected; + this.whenClicked = whenClicked; + } + + @Override + public boolean interaction(InventoryClickEvent event, FeatureGuiInventory featureInventory) { + if (this.slot == event.getSlot()) { + int next = (this.selected + 1) % this.items.size(); + ItemSwitch itemSwitch = new ItemSwitch(this.items, this.slot, next, this.whenClicked); + featureInventory.replaceComponent(this, itemSwitch); + this.whenClicked.doSomething(event, featureInventory); + return true; + } + return false; + } + + @Override + public void creation(Inventory inventory) { + inventory.setItem(this.slot, this.items.get(this.selected)); + } +} diff --git a/sakura-server/src/main/java/me/samsuik/sakura/player/visibility/PlayerVisibilitySettings.java b/sakura-server/src/main/java/me/samsuik/sakura/player/visibility/PlayerVisibilitySettings.java new file mode 100644 index 0000000..938fe21 --- /dev/null +++ b/sakura-server/src/main/java/me/samsuik/sakura/player/visibility/PlayerVisibilitySettings.java @@ -0,0 +1,65 @@ +package me.samsuik.sakura.player.visibility; + +import it.unimi.dsi.fastutil.objects.Reference2ObjectMap; +import it.unimi.dsi.fastutil.objects.Reference2ObjectOpenHashMap; +import net.minecraft.nbt.CompoundTag; +import org.jetbrains.annotations.NotNull; +import org.jspecify.annotations.NonNull; + +public final class PlayerVisibilitySettings implements VisibilitySettings { + private static final String SETTINGS_COMPOUND_TAG = "clientVisibilitySettings"; + private final Reference2ObjectMap visibilityStates = new Reference2ObjectOpenHashMap<>(); + + @Override + public @NonNull VisibilityState get(@NonNull VisibilityType type) { + VisibilityState state = this.visibilityStates.get(type); + return state != null ? state : type.getDefault(); + } + + @Override + public @NotNull VisibilityState set(@NonNull VisibilityType type, @NonNull VisibilityState state) { + if (type.isDefault(state)) { + this.visibilityStates.remove(type); + } else { + this.visibilityStates.put(type, state); + } + return state; + } + + @Override + public @NonNull VisibilityState currentState() { + int modifiedCount = this.visibilityStates.size(); + if (modifiedCount == 0) { + return VisibilityState.ON; + } else if (modifiedCount != VisibilityTypes.types().size()) { + return VisibilityState.MODIFIED; + } else { + return VisibilityState.OFF; + } + } + + @Override + public boolean playerModified() { + return !this.visibilityStates.isEmpty(); + } + + public void loadData(@NonNull CompoundTag tag) { + if (!tag.contains(SETTINGS_COMPOUND_TAG, CompoundTag.TAG_COMPOUND)) { + return; + } + + CompoundTag settingsTag = tag.getCompound(SETTINGS_COMPOUND_TAG); + for (VisibilityType type : VisibilityTypes.types()) { + if (settingsTag.contains(type.key(), CompoundTag.TAG_STRING)) { + VisibilityState state = VisibilityState.valueOf(settingsTag.getString(type.key())); + this.visibilityStates.put(type, state); + } + } + } + + public void saveData(@NonNull CompoundTag tag) { + CompoundTag settingsTag = new CompoundTag(); + this.visibilityStates.forEach((t, s) -> settingsTag.putString(t.key(), s.name())); + tag.put(SETTINGS_COMPOUND_TAG, settingsTag); + } +} diff --git a/sakura-server/src/main/java/me/samsuik/sakura/player/visibility/VisibilityGui.java b/sakura-server/src/main/java/me/samsuik/sakura/player/visibility/VisibilityGui.java new file mode 100644 index 0000000..67b821b --- /dev/null +++ b/sakura-server/src/main/java/me/samsuik/sakura/player/visibility/VisibilityGui.java @@ -0,0 +1,96 @@ +package me.samsuik.sakura.player.visibility; + +import it.unimi.dsi.fastutil.ints.IntArrayFIFOQueue; +import me.samsuik.sakura.configuration.GlobalConfiguration; +import me.samsuik.sakura.player.gui.FeatureGui; +import me.samsuik.sakura.player.gui.FeatureGuiInventory; +import me.samsuik.sakura.player.gui.components.ItemButton; +import me.samsuik.sakura.player.gui.components.ItemSwitch; +import net.kyori.adventure.text.Component; +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.entity.Player; +import org.bukkit.inventory.Inventory; +import org.jspecify.annotations.NullMarked; + +import static me.samsuik.sakura.player.gui.ItemStackUtil.itemWithBlankName; + +@NullMarked +public final class VisibilityGui extends FeatureGui { + private static final NamespacedKey TOGGLE_BUTTON_KEY = new NamespacedKey("sakura", "toggle_button"); + private static final NamespacedKey MENU_ITEMS_KEY = new NamespacedKey("sakura", "menu_items"); + + public VisibilityGui() { + super(45, Component.text("FPS Settings")); + } + + @Override + protected void fillInventory(Inventory inventory) { + for (int slot = 0; slot < inventory.getSize(); ++slot) { + // x, y from top left of the inventory + int x = slot % 9; + int y = slot / 9; + // from center + int rx = x - 4; + int ry = y - 2; + double d = Math.sqrt(rx * rx + ry * ry); + if (d <= 3.25) { + inventory.setItem(slot, itemWithBlankName(GlobalConfiguration.get().fps.material)); + } else if (x % 8 == 0) { + inventory.setItem(slot, itemWithBlankName(Material.BLACK_STAINED_GLASS_PANE)); + } else { + inventory.setItem(slot, itemWithBlankName(Material.WHITE_STAINED_GLASS_PANE)); + } + } + } + + @Override + protected void afterFill(Player player, FeatureGuiInventory inventory) { + VisibilitySettings settings = player.getVisibility(); + IntArrayFIFOQueue slots = this.availableSlots(); + this.updateToggleButton(settings, player, inventory); + for (VisibilityType type : VisibilityTypes.types()) { + VisibilityState state = settings.get(type); + int index = type.states().indexOf(state); + int slot = slots.dequeueInt(); + + ItemSwitch itemSwitch = new ItemSwitch( + VisibilityGuiItems.GUI_ITEMS.get(type), + slot, index, + (e, inv) -> { + settings.cycle(type); + this.updateToggleButton(settings, player, inv); + } + ); + + inventory.addComponent(itemSwitch, MENU_ITEMS_KEY); + } + } + + private void updateToggleButton(VisibilitySettings settings, Player player, FeatureGuiInventory inventory) { + inventory.removeComponents(TOGGLE_BUTTON_KEY); + VisibilityState settingsState = settings.currentState(); + ItemButton button = new ItemButton( + VisibilityGuiItems.TOGGLE_BUTTON_ITEMS.get(settingsState), + (2 * 9) + 8, + (e, inv) -> { + settings.toggleAll(); + inventory.removeAllComponents(); + this.afterFill(player, inv); + } + ); + inventory.addComponent(button, TOGGLE_BUTTON_KEY); + } + + private IntArrayFIFOQueue availableSlots() { + IntArrayFIFOQueue slots = new IntArrayFIFOQueue(); + for (int row = 1; row < 4; ++row) { + for (int column = 3; column < 6; ++column) { + if ((column + row) % 2 == 0) { + slots.enqueue((row * 9) + column); + } + } + } + return slots; + } +} diff --git a/sakura-server/src/main/java/me/samsuik/sakura/player/visibility/VisibilityGuiItems.java b/sakura-server/src/main/java/me/samsuik/sakura/player/visibility/VisibilityGuiItems.java new file mode 100644 index 0000000..ca26410 --- /dev/null +++ b/sakura-server/src/main/java/me/samsuik/sakura/player/visibility/VisibilityGuiItems.java @@ -0,0 +1,55 @@ +package me.samsuik.sakura.player.visibility; + +import com.google.common.collect.ImmutableList; +import it.unimi.dsi.fastutil.objects.Reference2ObjectMap; +import it.unimi.dsi.fastutil.objects.Reference2ObjectOpenHashMap; +import me.samsuik.sakura.player.gui.ItemStackUtil; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import org.apache.commons.lang.StringUtils; +import org.bukkit.Material; +import org.bukkit.inventory.ItemStack; + +import java.util.Locale; + +public final class VisibilityGuiItems { + static final Reference2ObjectMap> GUI_ITEMS = new Reference2ObjectOpenHashMap<>(); + static final Reference2ObjectMap TOGGLE_BUTTON_ITEMS = new Reference2ObjectOpenHashMap<>(); + + static { + Reference2ObjectMap items = new Reference2ObjectOpenHashMap<>(); + + items.put(VisibilityTypes.TNT, ItemStackUtil.itemWithName(Material.TNT, Component.text("Tnt", NamedTextColor.RED))); + items.put(VisibilityTypes.SAND, ItemStackUtil.itemWithName(Material.SAND, Component.text("Sand", NamedTextColor.YELLOW))); + items.put(VisibilityTypes.EXPLOSIONS, ItemStackUtil.itemWithName(Material.COBWEB, Component.text("Explosions", NamedTextColor.WHITE))); + items.put(VisibilityTypes.SPAWNERS, ItemStackUtil.itemWithName(Material.SPAWNER, Component.text("Spawners", NamedTextColor.DARK_GRAY))); + items.put(VisibilityTypes.PISTONS, ItemStackUtil.itemWithName(Material.PISTON, Component.text("Pistons", NamedTextColor.GOLD))); + + for (VisibilityType type : VisibilityTypes.types()) { + ItemStack item = items.get(type); + + ImmutableList stateItems = type.states().stream() + .map(s -> createItemForState(item, s)) + .collect(ImmutableList.toImmutableList()); + + GUI_ITEMS.put(type, stateItems); + } + + TOGGLE_BUTTON_ITEMS.put(VisibilityState.ON, ItemStackUtil.itemWithName(Material.GREEN_STAINED_GLASS_PANE, Component.text("Enabled", NamedTextColor.GREEN))); + TOGGLE_BUTTON_ITEMS.put(VisibilityState.MODIFIED, ItemStackUtil.itemWithName(Material.MAGENTA_STAINED_GLASS_PANE, Component.text("Modified", NamedTextColor.LIGHT_PURPLE))); + TOGGLE_BUTTON_ITEMS.put(VisibilityState.OFF, ItemStackUtil.itemWithName(Material.RED_STAINED_GLASS_PANE, Component.text("Disabled", NamedTextColor.RED))); + } + + private static String lowercaseThenCapitalise(String name) { + String lowercaseName = name.toLowerCase(Locale.ENGLISH); + return StringUtils.capitalize(lowercaseName); + } + + private static ItemStack createItemForState(ItemStack in, VisibilityState state) { + String capitalisedName = lowercaseThenCapitalise(state.name()); + Component component = Component.text(" | " + capitalisedName, NamedTextColor.GRAY); + ItemStack itemCopy = in.clone(); + itemCopy.editMeta(m -> m.itemName(m.itemName().append(component))); + return itemCopy; + } +} diff --git a/sakura-server/src/minecraft/java b/sakura-server/src/minecraft/java index 7a738b2..51af061 160000 --- a/sakura-server/src/minecraft/java +++ b/sakura-server/src/minecraft/java @@ -1 +1 @@ -Subproject commit 7a738b2575321ce50cc91c4782cc336e9174f611 +Subproject commit 51af06117fa091fdea173dad22bb5774479c0907