From 7dd2d204ced2664b4095ea13c45078ead8b04599 Mon Sep 17 00:00:00 2001 From: Cubicake <74905@stpeters.sa.edu.au> Date: Sun, 28 Jun 2026 12:03:46 +0930 Subject: [PATCH 01/21] Include ParallelepipedLeafPile --- .gitmodules | 4 ++++ core/build.gradle.kts | 3 +++ leafpile | 1 + platform-paper/build.gradle.kts | 7 +++++++ settings.gradle | 11 ++++++++++- 5 files changed, 25 insertions(+), 1 deletion(-) create mode 160000 leafpile diff --git a/.gitmodules b/.gitmodules index 43aaadcf..ef2aaf96 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,7 @@ [submodule "logging"] path = logging url = https://github.com/Cubicake/CubiLogging.git +[submodule "leafpile"] + path = leafpile + url = https://github.com/Cubicake/ParallelepipedLeafPile.git + branch = java-21-main diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 387a5adb..ee283694 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -22,7 +22,10 @@ dependencies { compileOnly("org.spongepowered:configurate-core:4.2.0") compileOnly("org.spongepowered:configurate-yaml:4.2.0") compileOnly("it.unimi.dsi:fastutil:8.5.18") + implementation(project(":locatable-lib")) + + compileOnly(project(":leafpile")) compileOnly(project(":logging")) testImplementation(platform("org.junit:junit-bom:5.10.2")) diff --git a/leafpile b/leafpile new file mode 160000 index 00000000..041980bc --- /dev/null +++ b/leafpile @@ -0,0 +1 @@ +Subproject commit 041980bc0428ffc8b10ec9ff115099b6bc6d8f93 diff --git a/platform-paper/build.gradle.kts b/platform-paper/build.gradle.kts index 6fd51229..3f0b2370 100644 --- a/platform-paper/build.gradle.kts +++ b/platform-paper/build.gradle.kts @@ -35,6 +35,7 @@ dependencies { implementation("org.jetbrains:annotations:24.0.1") + implementation(project(":leafpile")) implementation(project(":locatable-lib")) implementation(project(":logging")) implementation(project(":core")) @@ -126,7 +127,13 @@ tasks.shadowJar { include(project(":locatable-lib")) include(project(":core")) include(project(":packetevents")) + + include(project(":leafpile")) } + relocate( + "ca.spottedleaf", + "games.cubi.libs.raycastedantiesp.spottedleaf" + ) archiveBaseName.set("RaycastedAntiESP") archiveClassifier.set("") } diff --git a/settings.gradle b/settings.gradle index 9e6dc69c..56471f6b 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,6 +1,15 @@ rootProject.name = 'RaycastedAntiESP' +dependencyResolutionManagement { + versionCatalogs { + libs { + from(files("leafpile/gradle/libs.versions.toml")) + } + } +} + include 'locatable-lib' include 'logging' include 'core' include 'platform-paper' -include 'packetevents' \ No newline at end of file +include 'packetevents' +include 'leafpile' From d5a55ef3930710df3edfe644ee01984299c560ca Mon Sep 17 00:00:00 2001 From: Cubicake <74905@stpeters.sa.edu.au> Date: Sun, 28 Jun 2026 12:22:09 +0930 Subject: [PATCH 02/21] Update leafpile and include it in PE module --- leafpile | 2 +- packetevents/build.gradle.kts | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/leafpile b/leafpile index 041980bc..899341f9 160000 --- a/leafpile +++ b/leafpile @@ -1 +1 @@ -Subproject commit 041980bc0428ffc8b10ec9ff115099b6bc6d8f93 +Subproject commit 899341f9976a2f224a1a8914999c99baa98dc0c4 diff --git a/packetevents/build.gradle.kts b/packetevents/build.gradle.kts index 58cee0da..257bb517 100644 --- a/packetevents/build.gradle.kts +++ b/packetevents/build.gradle.kts @@ -16,6 +16,8 @@ dependencies { implementation(project(":logging")) implementation(project(":core")) + compileOnly(project(":leafpile")) + compileOnly("com.github.retrooper:packetevents-api:2.12.0") compileOnly("org.spongepowered:configurate-core:4.2.0") compileOnly("it.unimi.dsi:fastutil:8.5.18") From de3ddd88daff50ebefc438057b3aefb079a7ea98 Mon Sep 17 00:00:00 2001 From: Cubicake <74905@stpeters.sa.edu.au> Date: Sun, 28 Jun 2026 12:50:41 +0930 Subject: [PATCH 03/21] Use SWMR and MTQueue --- .../core/view/AbstractBlockView.java | 3 +- .../view/PacketEventsEntityView.java | 31 ++++++++++++------- 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/core/src/main/java/games/cubi/raycastedantiesp/core/view/AbstractBlockView.java b/core/src/main/java/games/cubi/raycastedantiesp/core/view/AbstractBlockView.java index 1a3cda8c..db02577d 100644 --- a/core/src/main/java/games/cubi/raycastedantiesp/core/view/AbstractBlockView.java +++ b/core/src/main/java/games/cubi/raycastedantiesp/core/view/AbstractBlockView.java @@ -1,5 +1,6 @@ package games.cubi.raycastedantiesp.core.view; +import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue; import games.cubi.locatables.BlockLocatable; import games.cubi.locatables.ChunkSectionLocatable; import games.cubi.locatables.Locatable; @@ -20,7 +21,7 @@ public abstract class AbstractBlockView> implem private final Map chunks = new ConcurrentHashMap<>(); private final CanonicalSet knownTileEntities = new ConcurrentSelfMap<>(); - private final ConcurrentLinkedQueue transitions = new ConcurrentLinkedQueue<>(); + private final MultiThreadedQueue transitions = new MultiThreadedQueue<>(); @Deprecated protected abstract T createTrackedTileEntity(BlockLocatable location, int blockID, boolean visible); diff --git a/packetevents/src/main/java/games/cubi/raycastedantiesp/packetevents/view/PacketEventsEntityView.java b/packetevents/src/main/java/games/cubi/raycastedantiesp/packetevents/view/PacketEventsEntityView.java index 21bdc420..bc9a7554 100644 --- a/packetevents/src/main/java/games/cubi/raycastedantiesp/packetevents/view/PacketEventsEntityView.java +++ b/packetevents/src/main/java/games/cubi/raycastedantiesp/packetevents/view/PacketEventsEntityView.java @@ -1,5 +1,8 @@ package games.cubi.raycastedantiesp.packetevents.view; +import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue; +import ca.spottedleaf.concurrentutil.map.SWMRHashTable; +import ca.spottedleaf.concurrentutil.map.SWMRInt2ObjectHashTable; import games.cubi.locatables.Locatable; import games.cubi.logs.Logger; import games.cubi.raycastedantiesp.core.locatables.NettyEntityLocatable; @@ -9,14 +12,12 @@ import org.jetbrains.annotations.NotNull; import java.util.*; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentLinkedQueue; import java.util.function.Consumer; public class PacketEventsEntityView implements EntityView { - private final Map entitiesByUUID = new ConcurrentHashMap<>(); - private final Map entityUUIDsByID = new ConcurrentHashMap<>(); - private final ConcurrentLinkedQueue transitions = new ConcurrentLinkedQueue<>(); + private final Map entitiesByUUID = new SWMRHashTable<>(); + private final SWMRInt2ObjectHashTable entityUUIDsByID = new SWMRInt2ObjectHashTable<>(); + private final MultiThreadedQueue transitions = new MultiThreadedQueue<>(); private final boolean isPlayerView; public PacketEventsEntityView(boolean isPlayerView) { @@ -167,7 +168,14 @@ public Collection getKnownEntities() { @Override public int[] getKnownEntityIDs() { - return entityUUIDsByID.keySet().stream().mapToInt(Integer::intValue).toArray(); + // This is only called while clearing on the Netty thread, so size() + // is stable and can be used as the exact output array length. + int[] entityIDs = new int[entityUUIDsByID.size()]; + int[] count = new int[1]; + entityUUIDsByID.forEachKey(entityID -> { + entityIDs[count[0]++] = entityID; + }); + return entityIDs; } @Override @@ -218,14 +226,13 @@ private PacketEventsEntity getTrackedEntity(int entityID) { public String getStringDataForDebugging() { StringBuilder builder = new StringBuilder(); builder.append("EntityView isPlayerView=").append(isPlayerView).append("\n"); - Set> entries = new HashSet<>(entityUUIDsByID.entrySet()); - for (Map.Entry entry : entries) { - PacketEventsEntity entity = entitiesByUUID.get(entry.getValue()); - builder.append("EntityID=").append(entry.getKey()) - .append(" UUID=").append(entry.getValue()) + entityUUIDsByID.forEach((entityID, entityUUID) -> { + PacketEventsEntity entity = entitiesByUUID.get(entityUUID); + builder.append("EntityID=").append(entityID) + .append(" UUID=").append(entityUUID) .append(" Entity=").append(entity) .append("\n"); - } + }); return builder.toString(); } } From b4e65f96538776fecff71e548ad809b22ecfdb8a Mon Sep 17 00:00:00 2001 From: Cubicake <74905@stpeters.sa.edu.au> Date: Sun, 28 Jun 2026 13:13:31 +0930 Subject: [PATCH 04/21] Add thread guards for netty thread --- .../core/utils/SingleThreadedGuard.java | 19 +++++++++++++++++++ .../view/PacketEventsEntityView.java | 9 ++++++++- 2 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 core/src/main/java/games/cubi/raycastedantiesp/core/utils/SingleThreadedGuard.java diff --git a/core/src/main/java/games/cubi/raycastedantiesp/core/utils/SingleThreadedGuard.java b/core/src/main/java/games/cubi/raycastedantiesp/core/utils/SingleThreadedGuard.java new file mode 100644 index 00000000..0a6cbe9a --- /dev/null +++ b/core/src/main/java/games/cubi/raycastedantiesp/core/utils/SingleThreadedGuard.java @@ -0,0 +1,19 @@ +package games.cubi.raycastedantiesp.core.utils; + +/** + * Classes where specific methods must be called from a single thread can extend this class and call {@link #guardThread()} + * at the start of those methods to ensure that they are only called from the permitted thread. + */ +public abstract class SingleThreadedGuard { + private final Thread permittedThread; + + protected SingleThreadedGuard(Thread thread) { + permittedThread = thread; + } + + protected void guardThread() { + if (Thread.currentThread() != permittedThread) { + throw new IllegalStateException("Method called from wrong thread. Expected: " + permittedThread + ", actual: " + Thread.currentThread()); + } + } +} diff --git a/packetevents/src/main/java/games/cubi/raycastedantiesp/packetevents/view/PacketEventsEntityView.java b/packetevents/src/main/java/games/cubi/raycastedantiesp/packetevents/view/PacketEventsEntityView.java index bc9a7554..01b5add4 100644 --- a/packetevents/src/main/java/games/cubi/raycastedantiesp/packetevents/view/PacketEventsEntityView.java +++ b/packetevents/src/main/java/games/cubi/raycastedantiesp/packetevents/view/PacketEventsEntityView.java @@ -6,6 +6,7 @@ import games.cubi.locatables.Locatable; import games.cubi.logs.Logger; import games.cubi.raycastedantiesp.core.locatables.NettyEntityLocatable; +import games.cubi.raycastedantiesp.core.utils.SingleThreadedGuard; import games.cubi.raycastedantiesp.core.view.EntityView; import games.cubi.raycastedantiesp.core.view.EntityViewTransition; import games.cubi.raycastedantiesp.packetevents.locatables.PacketEventsEntity; @@ -14,13 +15,14 @@ import java.util.*; import java.util.function.Consumer; -public class PacketEventsEntityView implements EntityView { +public class PacketEventsEntityView extends SingleThreadedGuard implements EntityView { private final Map entitiesByUUID = new SWMRHashTable<>(); private final SWMRInt2ObjectHashTable entityUUIDsByID = new SWMRInt2ObjectHashTable<>(); private final MultiThreadedQueue transitions = new MultiThreadedQueue<>(); private final boolean isPlayerView; public PacketEventsEntityView(boolean isPlayerView) { + super(Thread.currentThread()); // Should be player's netty thread this.isPlayerView = isPlayerView; } @@ -34,6 +36,7 @@ public static PacketEventsEntityView createEntityView() { @Override public void insertEntity(PacketEventsEntity entity) { + guardThread(); if (entity == null || entity.entityUUID() == null) { Logger.error(new RuntimeException("Attempted to insert null entity or entity with null UUID into EntityView"), 2, PacketEventsEntityView.class); return; @@ -65,6 +68,7 @@ public void removeEntity(int entityID, int currentTick) { @Override public void removeEntity(int entityID) { + guardThread(); UUID entityUUID = entityUUIDsByID.remove(entityID); if (entityUUID == null) { return; @@ -78,6 +82,7 @@ public void removeEntity(int entityID) { @Override public void removeEntity(UUID entityUUID, int currentTick) { + guardThread(); int entityID = getEntityID(entityUUID); removeEntity(entityID, currentTick); @@ -168,6 +173,7 @@ public Collection getKnownEntities() { @Override public int[] getKnownEntityIDs() { + guardThread(); // This is only called while clearing on the Netty thread, so size() // is stable and can be used as the exact output array length. int[] entityIDs = new int[entityUUIDsByID.size()]; @@ -213,6 +219,7 @@ public boolean isPlayerView() { @Override public void clear() { + guardThread(); entitiesByUUID.clear(); entityUUIDsByID.clear(); transitions.clear(); From bdf22111adf69048ed1bf65afc8cd35fbc9716d7 Mon Sep 17 00:00:00 2001 From: Cubicake <74905@stpeters.sa.edu.au> Date: Sun, 28 Jun 2026 13:17:32 +0930 Subject: [PATCH 05/21] Mark fields volatile --- .../core/locatables/NettyEntityLocatable.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/core/src/main/java/games/cubi/raycastedantiesp/core/locatables/NettyEntityLocatable.java b/core/src/main/java/games/cubi/raycastedantiesp/core/locatables/NettyEntityLocatable.java index 2e898483..7ca9ca2c 100644 --- a/core/src/main/java/games/cubi/raycastedantiesp/core/locatables/NettyEntityLocatable.java +++ b/core/src/main/java/games/cubi/raycastedantiesp/core/locatables/NettyEntityLocatable.java @@ -33,10 +33,10 @@ public abstract class NettyEntityLocatable Date: Sun, 28 Jun 2026 13:17:46 +0930 Subject: [PATCH 06/21] Remove unused import --- .../raycastedantiesp/core/locatables/NettyEntityLocatable.java | 1 - 1 file changed, 1 deletion(-) diff --git a/core/src/main/java/games/cubi/raycastedantiesp/core/locatables/NettyEntityLocatable.java b/core/src/main/java/games/cubi/raycastedantiesp/core/locatables/NettyEntityLocatable.java index 7ca9ca2c..88fd57d5 100644 --- a/core/src/main/java/games/cubi/raycastedantiesp/core/locatables/NettyEntityLocatable.java +++ b/core/src/main/java/games/cubi/raycastedantiesp/core/locatables/NettyEntityLocatable.java @@ -1,6 +1,5 @@ package games.cubi.raycastedantiesp.core.locatables; -import games.cubi.locatables.Locatable; import games.cubi.locatables.MutableLocatable; import games.cubi.locatables.implementations.MutableLocatableImpl; import games.cubi.raycastedantiesp.core.players.PlayerData; From 26ce1ff826ba34601efeae5952ee059daa537953 Mon Sep 17 00:00:00 2001 From: Cubicake <74905@stpeters.sa.edu.au> Date: Sun, 28 Jun 2026 13:21:56 +0930 Subject: [PATCH 07/21] Don't handle PlayerData lifecycle from main thread --- .../raycastedantiesp/paper/EventListener.java | 20 ++++++++----------- 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/platform-paper/src/main/java/games/cubi/raycastedantiesp/paper/EventListener.java b/platform-paper/src/main/java/games/cubi/raycastedantiesp/paper/EventListener.java index 3d61284b..0cee91fc 100644 --- a/platform-paper/src/main/java/games/cubi/raycastedantiesp/paper/EventListener.java +++ b/platform-paper/src/main/java/games/cubi/raycastedantiesp/paper/EventListener.java @@ -7,6 +7,7 @@ import games.cubi.raycastedantiesp.paper.engine.PaperSimpleEngine; import games.cubi.raycastedantiesp.core.players.PlayerData; import games.cubi.raycastedantiesp.paper.utils.PaperListener; +import net.kyori.adventure.text.minimessage.MiniMessage; import org.bukkit.Bukkit; import org.bukkit.Location; import org.bukkit.entity.Player; @@ -15,7 +16,6 @@ import org.bukkit.event.player.PlayerChangedWorldEvent; import org.bukkit.event.player.PlayerJoinEvent; import org.bukkit.event.player.PlayerMoveEvent; -import org.bukkit.event.player.PlayerQuitEvent; import org.bukkit.event.player.PlayerRespawnEvent; import org.bukkit.event.player.PlayerTeleportEvent; @@ -42,26 +42,22 @@ public static EventListener initialise(RaycastedAntiESP plugin, PaperSimpleEngin } return instance; } - @EventHandler(priority = EventPriority.MONITOR) - public void onPlayerDisconnect(PlayerQuitEvent e) { - PlayerRegistry.getInstance().unregisterPlayer(e.getPlayer().getUniqueId()); - } - @EventHandler(priority = EventPriority.LOWEST) //Runs first public void onPlayerJoin(PlayerJoinEvent e) { Player player = e.getPlayer(); + PlayerData playerData = PlayerRegistry.getInstance().getPlayerData(player.getUniqueId()); + if (playerData == null) { + Logger.error("Player joined before packet state was registered. Kicking player=" + player.getName() + " uuid=" + player.getUniqueId(), 1, EventListener.class); + player.kick(MiniMessage.miniMessage().deserialize("RaycastedAntiESP failed to initialise your packet state. Please reconnect. Report this issue to the server you are playing on if you are still unable to join.")); + return; + } + if (player.hasPermission("raycastedantiesp.updatecheck")) { //todo: centralise permission strings to prevent issues when perm names are changed checkForUpdates(plugin, player); } boolean hasBypassPermission = player.hasPermission("raycastedantiesp.bypass"); - PlayerData playerData = PlayerRegistry.getInstance().getPlayerData(player.getUniqueId()); - - if (playerData == null) { - Logger.warning("Failed to load player data for " + player.getName() + " (" + player.getUniqueId() + "). Attempting to reconstruct.", 3, EventListener.class); - playerData = PlayerRegistry.getInstance().registerAndGetPlayerIfAbsent(player.getUniqueId(), hasBypassPermission, currentTickSupplier.getAsInt()); - } playerData.setBypassPermission(hasBypassPermission); updateOwnLocation(playerData, player.getEyeLocation()); } From d3a2b43ba370f51d7e8b5639874abd0116796a5d Mon Sep 17 00:00:00 2001 From: Cubicake <74905@stpeters.sa.edu.au> Date: Sun, 28 Jun 2026 13:23:09 +0930 Subject: [PATCH 08/21] Make self entity fields final --- .../core/players/NettyData.java | 23 +++++++------------ 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/core/src/main/java/games/cubi/raycastedantiesp/core/players/NettyData.java b/core/src/main/java/games/cubi/raycastedantiesp/core/players/NettyData.java index ac0a5643..805777b5 100644 --- a/core/src/main/java/games/cubi/raycastedantiesp/core/players/NettyData.java +++ b/core/src/main/java/games/cubi/raycastedantiesp/core/players/NettyData.java @@ -28,7 +28,6 @@ */ public class NettyData implements Clearable { private static final int DEFAULT_MAP_SIZE = 16; - public static final int NO_SELF_ENTITY_ID = -1; // // --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- // START Leash tracking: @@ -250,20 +249,16 @@ public void evictOldPendingPostSpawnTasks(int currentTick) { // --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- // START Self entity tracking: // - private NettyEntityLocatable selfEntity; - private int selfEntityID = NO_SELF_ENTITY_ID; + private final NettyEntityLocatable selfEntity; + private final int selfEntityID; - public NettyEntityLocatable getSelfEntity() { - return selfEntity; + public NettyData(NettyEntityLocatable selfEntity) { + this.selfEntity = selfEntity; + this.selfEntityID = selfEntity.entityID(); } - public NettyData setSelfEntity(NettyEntityLocatable selfEntity) { - if (this.selfEntity != null && this.selfEntity != selfEntity) { - this.selfEntity.clear(); - } - this.selfEntity = selfEntity; - selfEntityID = selfEntity == null ? NO_SELF_ENTITY_ID : selfEntity.entityID(); - return this; + public NettyEntityLocatable getSelfEntity() { + return selfEntity; } public int getSelfEntityID() { @@ -271,7 +266,7 @@ public int getSelfEntityID() { } public boolean isSelfEntityID(int entityID) { - return selfEntityID != NO_SELF_ENTITY_ID && entityID == selfEntityID; + return entityID == selfEntityID; } // // END Self entity tracking. @@ -360,8 +355,6 @@ public void clear() { if (selfEntity != null) { selfEntity.clear(); } - selfEntity = null; - selfEntityID = NO_SELF_ENTITY_ID; currentWorldMinHeight = Integer.MIN_VALUE; currentWorldName = null; } From ea36d636e9e6d227bfa8ef702fb0625844d86586 Mon Sep 17 00:00:00 2001 From: Cubicake <74905@stpeters.sa.edu.au> Date: Sun, 28 Jun 2026 13:26:44 +0930 Subject: [PATCH 09/21] Move player registration to play phase only and handle disconnect from netty --- .../controller/PacketEntityViewController.java | 15 ++++++++------- .../PacketEventsEntityViewController.java | 13 ++++++------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/core/src/main/java/games/cubi/raycastedantiesp/core/view/controller/PacketEntityViewController.java b/core/src/main/java/games/cubi/raycastedantiesp/core/view/controller/PacketEntityViewController.java index 2020aa55..02348260 100644 --- a/core/src/main/java/games/cubi/raycastedantiesp/core/view/controller/PacketEntityViewController.java +++ b/core/src/main/java/games/cubi/raycastedantiesp/core/view/controller/PacketEntityViewController.java @@ -80,17 +80,18 @@ protected void handleWorldStatePacket(UUID player, String world, int minWorldHei nettyData.setCurrentWorldName(world).setCurrentWorldMinHeight(minWorldHeight); } - protected void handlePlayPhaseLoginPacket(int entityID, UUID playerUUID, int currentTick) { - PlayerData playerData = PlayerRegistry.getInstance().getPlayerData(playerUUID); - playerData.nettyData().setSelfEntity(Logger.requireNonNull(createSelfEntity(playerData, entityID, playerUUID), "createSelfEntity returned null", 3, PacketEntityViewController.class)); - } - - protected PlayerData handleLoginPhaseLoginPacket(UUID playerUUID, int currentTick) { - return PlayerRegistry.getInstance().registerAndGetPlayer(playerUUID, currentTick); + protected PlayerData handlePlayPhaseLoginPacket(int entityID, UUID playerUUID, int currentTick) { + return PlayerRegistry.getInstance().registerAndGetPlayer(playerUUID, currentTick, entityID, this::createSelfEntity); } protected abstract NettyEntityLocatable createSelfEntity(PlayerData ownData, int entityID, UUID playerUUID); + protected void handlePlayerDisconnect(UUID player) { + if (player == null) { + return; + } + PlayerRegistry.getInstance().unregisterPlayer(player); + } /** * @return Whether or not to cancel the packet event. true to cancel, false to do nothing. */ diff --git a/packetevents/src/main/java/games/cubi/raycastedantiesp/packetevents/viewcontrollers/PacketEventsEntityViewController.java b/packetevents/src/main/java/games/cubi/raycastedantiesp/packetevents/viewcontrollers/PacketEventsEntityViewController.java index e2a1adfe..1feb07f9 100644 --- a/packetevents/src/main/java/games/cubi/raycastedantiesp/packetevents/viewcontrollers/PacketEventsEntityViewController.java +++ b/packetevents/src/main/java/games/cubi/raycastedantiesp/packetevents/viewcontrollers/PacketEventsEntityViewController.java @@ -11,6 +11,7 @@ import com.github.retrooper.packetevents.PacketEvents; import com.github.retrooper.packetevents.event.PacketListener; import com.github.retrooper.packetevents.event.PacketSendEvent; +import com.github.retrooper.packetevents.event.UserDisconnectEvent; import com.github.retrooper.packetevents.protocol.entity.data.EntityData; import com.github.retrooper.packetevents.protocol.entity.type.EntityType; import com.github.retrooper.packetevents.protocol.entity.type.EntityTypes; @@ -62,7 +63,10 @@ protected PacketEventsEntityViewController(IntSupplier currentTickSupplier) { COMMON = PacketEventsCommonViewController.get(currentTickSupplier); } - public void removeViewer(UUID viewerUUID) { + @Override + public void onUserDisconnect(UserDisconnectEvent event) { + UUID viewerUUID = event.getUser().getUUID(); + handlePlayerDisconnect(viewerUUID); } @Override @@ -74,16 +78,11 @@ public void onPacketSend(PacketSendEvent event) { PlayerData playerData = PlayerRegistry.getInstance().getPlayerData(viewerUUID); - if (event.getPacketType() == PacketType.Login.Server.LOGIN_SUCCESS) { - handleLoginPhaseLoginPacket(viewerUUID, CURRENT_TICK_SUPPLIER.getAsInt()); - return; - } - if (event.getPacketType() == PacketType.Play.Server.JOIN_GAME) { WrapperPlayServerJoinGame packet = new WrapperPlayServerJoinGame(event); int currentTick = CURRENT_TICK_SUPPLIER.getAsInt(); + playerData = handlePlayPhaseLoginPacket(packet.getEntityId(), viewerUUID, currentTick); handleWorldStatePacket(viewerUUID, packet.getWorldName(), packet.getDimensionType().getMinY(), currentTick); - handlePlayPhaseLoginPacket(packet.getEntityId(), viewerUUID, currentTick); } if (playerData == null) { From d66bf182b9f111c0df3148303e9107932547a4f2 Mon Sep 17 00:00:00 2001 From: Cubicake <74905@stpeters.sa.edu.au> Date: Sun, 28 Jun 2026 13:27:14 +0930 Subject: [PATCH 10/21] Skip already visible entities to prevent duplicate packets --- .../viewcontrollers/PacketEventsEntityViewController.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packetevents/src/main/java/games/cubi/raycastedantiesp/packetevents/viewcontrollers/PacketEventsEntityViewController.java b/packetevents/src/main/java/games/cubi/raycastedantiesp/packetevents/viewcontrollers/PacketEventsEntityViewController.java index 1feb07f9..319954b2 100644 --- a/packetevents/src/main/java/games/cubi/raycastedantiesp/packetevents/viewcontrollers/PacketEventsEntityViewController.java +++ b/packetevents/src/main/java/games/cubi/raycastedantiesp/packetevents/viewcontrollers/PacketEventsEntityViewController.java @@ -420,6 +420,9 @@ private void processEntityTransitions(PlayerData data, User viewer, EntityView

Date: Sun, 28 Jun 2026 13:32:36 +0930 Subject: [PATCH 11/21] Create NettyData inside PlayerData, and just mark PlayerData as disconnected so that engine threads ticking the object don't break --- .../core/engine/SimpleEngine.java | 3 ++ .../core/players/PlayerData.java | 25 +++++++++++----- .../core/players/PlayerRegistry.java | 30 ++++++++----------- 3 files changed, 34 insertions(+), 24 deletions(-) diff --git a/core/src/main/java/games/cubi/raycastedantiesp/core/engine/SimpleEngine.java b/core/src/main/java/games/cubi/raycastedantiesp/core/engine/SimpleEngine.java index 6e550941..7f5e9482 100644 --- a/core/src/main/java/games/cubi/raycastedantiesp/core/engine/SimpleEngine.java +++ b/core/src/main/java/games/cubi/raycastedantiesp/core/engine/SimpleEngine.java @@ -324,6 +324,9 @@ private void processTickForPlayers(List playerDataList, EntityConfig boolean debugParticles, int currentTick, TickTimingBatch timings) { for (PlayerData playerData : playerDataList) { + if (!playerData.isConnected()) { + continue; + } playerData.nettyData().markPendingPostSpawnTasksForEviction(); if (playerData.hasBypassPermission()) { timings.incrementBypassSkippedPlayers(); diff --git a/core/src/main/java/games/cubi/raycastedantiesp/core/players/PlayerData.java b/core/src/main/java/games/cubi/raycastedantiesp/core/players/PlayerData.java index 3485d674..b56a3148 100644 --- a/core/src/main/java/games/cubi/raycastedantiesp/core/players/PlayerData.java +++ b/core/src/main/java/games/cubi/raycastedantiesp/core/players/PlayerData.java @@ -15,6 +15,7 @@ public class PlayerData { private final UUID playerUUID; private final int joinTick; private volatile boolean hasBypassPermission; + private volatile boolean connected = true; private final ThreadSafeLocatable ownLocation; private final BlockView blockView; @@ -22,20 +23,22 @@ public class PlayerData { private final EntityView playerView; private final NettyData nettyData; - public PlayerData(UUID player, boolean hasBypassPermission, int joinTick) { - this(player, joinTick); - this.hasBypassPermission = hasBypassPermission; - } - - public PlayerData(UUID player, int joinTick) { + PlayerData(UUID player, boolean hasBypassPermission, int joinTick, int selfEntityID, PlayerRegistry.SelfEntityCreator selfEntityCreator) { this.joinTick = joinTick; this.playerUUID = player; + this.hasBypassPermission = hasBypassPermission; blockView = ViewRegistry.createBlockView(); entityView = ViewRegistry.createEntityView(); playerView = ViewRegistry.createPlayerEntityView(); - nettyData = new NettyData(); ownLocation = new ThreadSafeLocatable(null, 0, 0, 0); + NettyEntityLocatable selfEntity = Logger.requireNonNull( + selfEntityCreator.createSelfEntity(this, selfEntityID, player), + "Self entity creator returned null", + 3, + PlayerData.class + ); + nettyData = new NettyData(selfEntity); } public EntityView entityView() { @@ -75,6 +78,14 @@ public int getJoinTick() { return joinTick; } + public boolean isConnected() { + return connected; + } + + public void markDisconnected() { + connected = false; + } + /** * @return Either the entity or player view for this player, depending on the entity ID */ diff --git a/core/src/main/java/games/cubi/raycastedantiesp/core/players/PlayerRegistry.java b/core/src/main/java/games/cubi/raycastedantiesp/core/players/PlayerRegistry.java index 45208a4f..a2709536 100644 --- a/core/src/main/java/games/cubi/raycastedantiesp/core/players/PlayerRegistry.java +++ b/core/src/main/java/games/cubi/raycastedantiesp/core/players/PlayerRegistry.java @@ -1,11 +1,19 @@ package games.cubi.raycastedantiesp.core.players; +import games.cubi.locatables.Locatable; +import games.cubi.raycastedantiesp.core.locatables.NettyEntityLocatable; + import java.util.Collection; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; public class PlayerRegistry { + @FunctionalInterface + public interface SelfEntityCreator { + NettyEntityLocatable createSelfEntity(PlayerData playerData, int selfEntityID, UUID playerUUID); + } + private static PlayerRegistry instance; private PlayerRegistry() {} @@ -19,32 +27,20 @@ public static PlayerRegistry getInstance() { private final ConcurrentHashMap playerDataMap = new ConcurrentHashMap<>(); - public void registerPlayerIfAbsent(UUID playerUUID, boolean hasBypassPermission, int joinTick) { - playerDataMap.putIfAbsent(playerUUID, new PlayerData(playerUUID, hasBypassPermission, joinTick)); - } - /** Forcefully registers a player and returns the new PlayerData, even if they were already registered.**/ - public PlayerData registerAndGetPlayer(UUID playerUUID, int joinTick) { - PlayerData newData = new PlayerData(playerUUID, joinTick); - playerDataMap.put(playerUUID, newData); + public PlayerData registerAndGetPlayer(UUID playerUUID, int joinTick, int selfEntityID, SelfEntityCreator selfEntityCreator) { + PlayerData newData = new PlayerData(playerUUID, false, joinTick, selfEntityID, selfEntityCreator); + PlayerData old = playerDataMap.put(playerUUID, newData); + if (old != null) old.markDisconnected(); return newData; } - public PlayerData registerAndGetPlayerIfAbsent(UUID playerUUID, boolean hasBypassPermission, int joinTick) { - PlayerData newData = new PlayerData(playerUUID, hasBypassPermission, joinTick); - PlayerData existingData = playerDataMap.putIfAbsent(playerUUID, newData); - return existingData != null ? existingData : newData; - } - public void unregisterPlayer(UUID playerUUID) { PlayerData unregisteredPlayer = playerDataMap.remove(playerUUID); if (unregisteredPlayer == null) { return; } - unregisteredPlayer.blockView().clear(); - unregisteredPlayer.entityView().clear(); - unregisteredPlayer.playerView().clear(); - unregisteredPlayer.nettyData().clear(); + unregisteredPlayer.markDisconnected(); } public PlayerData getPlayerData(UUID playerUUID) { From 8afb1d10e9205982536a5f5984f700523680856e Mon Sep 17 00:00:00 2001 From: Cubicake <74905@stpeters.sa.edu.au> Date: Sun, 28 Jun 2026 14:05:50 +0930 Subject: [PATCH 12/21] Bump minimum supported version and use PlayerClientLoadedWorldEvent as it is after the play-phase join packet. This also requires guarding for null world in engine --- .../games/cubi/raycastedantiesp/core/engine/SimpleEngine.java | 2 +- platform-paper/build.gradle.kts | 2 +- .../java/games/cubi/raycastedantiesp/paper/EventListener.java | 3 ++- platform-paper/src/main/resources/plugin.yml | 2 +- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/core/src/main/java/games/cubi/raycastedantiesp/core/engine/SimpleEngine.java b/core/src/main/java/games/cubi/raycastedantiesp/core/engine/SimpleEngine.java index 7f5e9482..4230c934 100644 --- a/core/src/main/java/games/cubi/raycastedantiesp/core/engine/SimpleEngine.java +++ b/core/src/main/java/games/cubi/raycastedantiesp/core/engine/SimpleEngine.java @@ -336,7 +336,7 @@ private void processTickForPlayers(List playerDataList, EntityConfig BlockView blockView = playerData.blockView(); Locatable playerLocation = playerData.ownLocation(); - if (playerLocation == null) { + if (playerLocation == null || playerLocation.world() == null) { timings.incrementNullLocationSkippedPlayers(); continue; } diff --git a/platform-paper/build.gradle.kts b/platform-paper/build.gradle.kts index 3f0b2370..2cb1f2d9 100644 --- a/platform-paper/build.gradle.kts +++ b/platform-paper/build.gradle.kts @@ -24,7 +24,7 @@ repositories { } dependencies { - paperweight.paperDevBundle("1.21-R0.1-SNAPSHOT") + paperweight.paperDevBundle("1.21.4-R0.1-SNAPSHOT") //paperweight.paperDevBundle("26.1.2.build.+") compileOnly("com.github.retrooper:packetevents-spigot:2.12.0") compileOnly("org.spongepowered:configurate-core:4.2.0") diff --git a/platform-paper/src/main/java/games/cubi/raycastedantiesp/paper/EventListener.java b/platform-paper/src/main/java/games/cubi/raycastedantiesp/paper/EventListener.java index 0cee91fc..c5993d54 100644 --- a/platform-paper/src/main/java/games/cubi/raycastedantiesp/paper/EventListener.java +++ b/platform-paper/src/main/java/games/cubi/raycastedantiesp/paper/EventListener.java @@ -7,6 +7,7 @@ import games.cubi.raycastedantiesp.paper.engine.PaperSimpleEngine; import games.cubi.raycastedantiesp.core.players.PlayerData; import games.cubi.raycastedantiesp.paper.utils.PaperListener; +import io.papermc.paper.event.player.PlayerClientLoadedWorldEvent; import net.kyori.adventure.text.minimessage.MiniMessage; import org.bukkit.Bukkit; import org.bukkit.Location; @@ -43,7 +44,7 @@ public static EventListener initialise(RaycastedAntiESP plugin, PaperSimpleEngin return instance; } @EventHandler(priority = EventPriority.LOWEST) //Runs first - public void onPlayerJoin(PlayerJoinEvent e) { + public void onPlayerJoin(PlayerClientLoadedWorldEvent e) { Player player = e.getPlayer(); PlayerData playerData = PlayerRegistry.getInstance().getPlayerData(player.getUniqueId()); diff --git a/platform-paper/src/main/resources/plugin.yml b/platform-paper/src/main/resources/plugin.yml index e9bd1eea..2563a76a 100644 --- a/platform-paper/src/main/resources/plugin.yml +++ b/platform-paper/src/main/resources/plugin.yml @@ -1,7 +1,7 @@ name: RaycastedAntiESP version: ${version} main: games.cubi.raycastedantiesp.paper.RaycastedAntiESP -api-version: '1.21.0' +api-version: '1.21.4' prefix: RaycastedAntiESP authors: [ _Cubicake ] load: STARTUP From d35adc6f5f0ca27183123bff41c867afaa33a20c Mon Sep 17 00:00:00 2001 From: Cubicake <74905@stpeters.sa.edu.au> Date: Sun, 28 Jun 2026 15:22:46 +0930 Subject: [PATCH 13/21] Don't do unnecessary lookups --- .../core/engine/SimpleEngine.java | 32 ++++++++----------- .../core/locatables/EntityLocatable.java | 3 ++ .../core/locatables/NettyEntityLocatable.java | 7 ++++ .../core/view/EntityView.java | 7 ++++ .../view/PacketEventsEntityView.java | 13 ++++++++ 5 files changed, 44 insertions(+), 18 deletions(-) diff --git a/core/src/main/java/games/cubi/raycastedantiesp/core/engine/SimpleEngine.java b/core/src/main/java/games/cubi/raycastedantiesp/core/engine/SimpleEngine.java index 4230c934..cfec4188 100644 --- a/core/src/main/java/games/cubi/raycastedantiesp/core/engine/SimpleEngine.java +++ b/core/src/main/java/games/cubi/raycastedantiesp/core/engine/SimpleEngine.java @@ -1,5 +1,6 @@ package games.cubi.raycastedantiesp.core.engine; +import games.cubi.locatables.ImmutableLocatable; import games.cubi.locatables.Locatable; import games.cubi.logs.Logger; import games.cubi.raycastedantiesp.core.config.ConfigManager; @@ -363,24 +364,23 @@ private void processTickForPlayers(List playerDataList, EntityConfig private void checkEntities(PlayerData player, Locatable playerLocation, EntityConfig entityConfig, boolean debugParticles, BlockView blockView, int currentTick, TickTimingBatch timings) { EntityView entityView = player.entityView(); - int checked = entityView.forEachNeedingRecheck(entityConfig.getVisibleRecheckIntervalTicks(), currentTick, entityUUID -> { - boolean wasVisible = entityView.isVisible(entityUUID); - //todo Instead of passing by UUID reference everywhere (attachedToSelf and getLocation) have forEachNeedingRecheck pass the entity reference itself. - if (attachedToSelf(player, entityView, entityUUID, currentTick)) { + int checked = entityView.forEachNeedingRecheckEntity(entityConfig.getVisibleRecheckIntervalTicks(), currentTick, entity -> { + boolean wasVisible = entity.visible(); + if (attachedToSelf(player, entityView, entity, currentTick)) { return; } - Locatable entityLocation = entityView.getLocation(entityUUID); + ImmutableLocatable entityLocation = entity.getOffsetEntityLocation(); if (entityLocation == null) { timings.incrementEntityNullTargets(); Logger.debug("SimpleEngine.checkEntities skipped-null-location viewer=" + player.getPlayerUUID() - + " target=" + entityUUID + + " target=" + entity.entityUUID() + " wasVisible=" + wasVisible + " tick=" + currentTick); return; } timings.incrementEntityRaycasts(); boolean canSee = RaycastUtil.raycast(player, playerLocation, entityLocation, entityConfig.getMaxOccludingCount(), entityConfig.getAlwaysShowRadius(), entityConfig.getRaycastRadius(), debugParticles, blockView, 1, particleSpawner); - entityView.setVisibility(entityUUID, canSee, currentTick); + entityView.setVisibility(entity, canSee, currentTick); }); timings.addEntityChecked(checked); } @@ -388,32 +388,28 @@ private void checkEntities(PlayerData player, Locatable playerLocation, EntityCo private void checkPlayers(PlayerData player, Locatable playerLocation, PlayerConfig playerConfig, boolean debugParticles, BlockView blockView, int currentTick, TickTimingBatch timings) { EntityView playerView = player.playerView(); - int checked = playerView.forEachNeedingRecheck(playerConfig.getVisibleRecheckIntervalTicks(), currentTick, otherPlayerUUID -> { - boolean wasVisible = playerView.isVisible(otherPlayerUUID); - if (attachedToSelf(player, playerView, otherPlayerUUID, currentTick)) { + int checked = playerView.forEachNeedingRecheckEntity(playerConfig.getVisibleRecheckIntervalTicks(), currentTick, otherPlayer -> { + boolean wasVisible = otherPlayer.visible(); + if (attachedToSelf(player, playerView, otherPlayer, currentTick)) { return; } - Locatable otherPlayerLocation = playerView.getLocation(otherPlayerUUID); + ImmutableLocatable otherPlayerLocation = otherPlayer.getOffsetEntityLocation(); if (otherPlayerLocation == null) { timings.incrementPlayerNullTargets(); Logger.debug("SimpleEngine.checkPlayers skipped-null-location viewer=" + player.getPlayerUUID() - + " target=" + otherPlayerUUID + + " target=" + otherPlayer.entityUUID() + " wasVisible=" + wasVisible + " tick=" + currentTick); return; } timings.incrementPlayerRaycasts(); boolean canSee = RaycastUtil.raycast(player, playerLocation, otherPlayerLocation, playerConfig.getMaxOccludingCount(), playerConfig.getAlwaysShowRadius(), playerConfig.getRaycastRadius(), debugParticles, blockView, 1, particleSpawner); - playerView.setVisibility(otherPlayerUUID, canSee, currentTick); + playerView.setVisibility(otherPlayer, canSee, currentTick); }); timings.addPlayerChecked(checked); } - private boolean attachedToSelf(PlayerData player, EntityView view, UUID entityUUID, int currentTick) { - if (!(view.getEntity(entityUUID) instanceof NettyEntityLocatable entity)) { - Logger.error(new RuntimeException("EntityView returned a non-NettyEntityLocatable for UUID=" + entityUUID + " when checking for self-attachment. This should never happen."), 1, SimpleEngine.class); - return false; - } + private boolean attachedToSelf(PlayerData player, EntityView view, NettyEntityLocatable entity, int currentTick) { int selfEntityID = player.nettyData().getSelfEntityID(); if (!player.nettyData().isSelfEntityID(entity.leashingEntity()) && !player.nettyData().isSelfEntityID(entity.vehicleID()) diff --git a/core/src/main/java/games/cubi/raycastedantiesp/core/locatables/EntityLocatable.java b/core/src/main/java/games/cubi/raycastedantiesp/core/locatables/EntityLocatable.java index 3c2d27c9..3cee8b07 100644 --- a/core/src/main/java/games/cubi/raycastedantiesp/core/locatables/EntityLocatable.java +++ b/core/src/main/java/games/cubi/raycastedantiesp/core/locatables/EntityLocatable.java @@ -1,5 +1,6 @@ package games.cubi.raycastedantiesp.core.locatables; +import games.cubi.locatables.ImmutableLocatable; import games.cubi.locatables.MutableLocatable; import org.jetbrains.annotations.Nullable; @@ -62,6 +63,8 @@ public interface EntityLocatable extends MutableLo PacketReplayData packetReplayData(); EntityLocatable setPacketReplayData(PacketReplayData packetReplayData); + ImmutableLocatable getOffsetEntityLocation(); + /** * For use when the player disconnects, clears all data. */ diff --git a/core/src/main/java/games/cubi/raycastedantiesp/core/locatables/NettyEntityLocatable.java b/core/src/main/java/games/cubi/raycastedantiesp/core/locatables/NettyEntityLocatable.java index 88fd57d5..1bad380a 100644 --- a/core/src/main/java/games/cubi/raycastedantiesp/core/locatables/NettyEntityLocatable.java +++ b/core/src/main/java/games/cubi/raycastedantiesp/core/locatables/NettyEntityLocatable.java @@ -1,6 +1,8 @@ package games.cubi.raycastedantiesp.core.locatables; +import games.cubi.locatables.ImmutableLocatable; import games.cubi.locatables.MutableLocatable; +import games.cubi.locatables.implementations.ImmutableLocatableImpl; import games.cubi.locatables.implementations.MutableLocatableImpl; import games.cubi.raycastedantiesp.core.players.PlayerData; import games.cubi.raycastedantiesp.core.utils.Clearable; @@ -286,6 +288,11 @@ public PacketReplayData packetReplayData() { return this; } + @Override + public ImmutableLocatable getOffsetEntityLocation() { + return new ImmutableLocatableImpl(world, x, y + 0.5, z); //todo: move away from hardcoded offset + } + @Override public double x() { return x; diff --git a/core/src/main/java/games/cubi/raycastedantiesp/core/view/EntityView.java b/core/src/main/java/games/cubi/raycastedantiesp/core/view/EntityView.java index 662d74ca..9bac4a25 100644 --- a/core/src/main/java/games/cubi/raycastedantiesp/core/view/EntityView.java +++ b/core/src/main/java/games/cubi/raycastedantiesp/core/view/EntityView.java @@ -53,6 +53,13 @@ public interface EntityView> extends Clearable */ int forEachNeedingRecheck(int recheckTicks, int currentTick, Consumer action); + /** + * Iterates currently tracked entities that should be visibility-checked. + * + * @return number of entities passed to {@code action}. + */ + int forEachNeedingRecheckEntity(int recheckTicks, int currentTick, Consumer> action); + boolean hasPendingTransitions(); List drainTransitions(); diff --git a/packetevents/src/main/java/games/cubi/raycastedantiesp/packetevents/view/PacketEventsEntityView.java b/packetevents/src/main/java/games/cubi/raycastedantiesp/packetevents/view/PacketEventsEntityView.java index 01b5add4..a1c0e32d 100644 --- a/packetevents/src/main/java/games/cubi/raycastedantiesp/packetevents/view/PacketEventsEntityView.java +++ b/packetevents/src/main/java/games/cubi/raycastedantiesp/packetevents/view/PacketEventsEntityView.java @@ -197,6 +197,19 @@ public int forEachNeedingRecheck(int recheckTicks, int currentTick, Consumer> action) { + int processed = 0; + for (PacketEventsEntity entity : entitiesByUUID.values()) { + if (entity.visible() && (recheckTicks < 0 || currentTick - entity.lastChecked() < recheckTicks)) { + continue; + } + action.accept(entity); + processed++; + } + return processed; + } + @Override public boolean hasPendingTransitions() { return !transitions.isEmpty(); From 331310ddd17feb1a232ec9e7d98d06ab9cbb6453 Mon Sep 17 00:00:00 2001 From: Cubicake <74905@stpeters.sa.edu.au> Date: Sun, 28 Jun 2026 16:02:55 +0930 Subject: [PATCH 14/21] Don't do unnecessary lookups --- .../core/engine/SimpleEngine.java | 9 +++--- .../core/view/AbstractBlockView.java | 32 +++++++++++++++---- .../raycastedantiesp/core/view/BlockView.java | 17 ++++++++++ 3 files changed, 47 insertions(+), 11 deletions(-) diff --git a/core/src/main/java/games/cubi/raycastedantiesp/core/engine/SimpleEngine.java b/core/src/main/java/games/cubi/raycastedantiesp/core/engine/SimpleEngine.java index cfec4188..6139bda2 100644 --- a/core/src/main/java/games/cubi/raycastedantiesp/core/engine/SimpleEngine.java +++ b/core/src/main/java/games/cubi/raycastedantiesp/core/engine/SimpleEngine.java @@ -421,20 +421,19 @@ private boolean attachedToSelf(PlayerData player, EntityView view, NettyEntit } private void checkTileEntities(PlayerData player, Locatable playerLocation, TileEntityConfig tileEntityConfig, boolean debugParticles, BlockView blockView, int currentTick, TickTimingBatch timings) { - int checked = blockView.forEachNeedingRecheck(tileEntityConfig.getVisibleRecheckIntervalTicks(), currentTick, tileEntityLocation -> { + int checked = blockView.updateVisibilityForEachNeedingRecheck(tileEntityConfig.getVisibleRecheckIntervalTicks(), currentTick, tileEntityLocation -> { if (tileEntityLocation.world() == null || !tileEntityLocation.world().equals(playerLocation.world())) { timings.incrementTileWorldSkipped(); - return; + return BlockView.VisibilityResolver.SKIPPED; } if (playerLocation.distanceSquared(tileEntityLocation) > (double) tileEntityConfig.getRaycastRadius() * tileEntityConfig.getRaycastRadius()) { timings.incrementTileRadiusSkipped(); - blockView.setVisibility(tileEntityLocation, false, currentTick); - return; + return BlockView.VisibilityResolver.HIDE; } timings.incrementTileRaycasts(); boolean canSee = RaycastUtil.raycast(player, playerLocation, tileEntityLocation, tileEntityConfig.getMaxOccludingCount() + 1, tileEntityConfig.getAlwaysShowRadius(), tileEntityConfig.getRaycastRadius(), debugParticles, blockView, 1, particleSpawner); - blockView.setVisibility(tileEntityLocation, canSee, currentTick); + return canSee ? BlockView.VisibilityResolver.SHOW : BlockView.VisibilityResolver.HIDE; }); timings.addTileChecked(checked); } diff --git a/core/src/main/java/games/cubi/raycastedantiesp/core/view/AbstractBlockView.java b/core/src/main/java/games/cubi/raycastedantiesp/core/view/AbstractBlockView.java index db02577d..58f7630d 100644 --- a/core/src/main/java/games/cubi/raycastedantiesp/core/view/AbstractBlockView.java +++ b/core/src/main/java/games/cubi/raycastedantiesp/core/view/AbstractBlockView.java @@ -12,7 +12,6 @@ import java.util.*; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentLinkedQueue; import java.util.function.Consumer; public abstract class AbstractBlockView> implements BlockView { @@ -90,14 +89,18 @@ public void setVisibility(BlockLocatable location, boolean visible, int currentT if (existing == null) { return; } - if (existing.visible() != visible) { + setVisibility(existing, existing.visible(), visible, currentTick); + } + + public void setVisibility(T tileEntity, boolean currentVisibility, boolean shouldBeVisible, int currentTick) { + if (currentVisibility != shouldBeVisible) { transitions.add(new BlockViewTransition( - visible ? BlockViewTransition.Type.SHOW : BlockViewTransition.Type.HIDE, - location + shouldBeVisible ? BlockViewTransition.Type.SHOW : BlockViewTransition.Type.HIDE, + tileEntity )); } - existing.setVisible(visible); - existing.setLastChecked(currentTick); + tileEntity.setVisible(shouldBeVisible); + tileEntity.setLastChecked(currentTick); } @Override @@ -118,6 +121,23 @@ public int forEachNeedingRecheck(int recheckTicks, int currentTick, Consumer action); + @FunctionalInterface + interface VisibilityResolver { + byte SKIPPED = 78; + byte HIDE = -23; + byte SHOW = 42; + + byte setVisible(BlockLocatable location); + } + + /** + * Iterates currently tracked tile entities that should be visibility-checked. + * + * @return number of tile entities passed to {@code action}. + */ + int updateVisibilityForEachNeedingRecheck(int recheckTicks, int currentTick, VisibilityResolver action); + boolean hasPendingTransitions(); List drainTransitions(); From 344f259f95cf507fd946458d62a004d388181e60 Mon Sep 17 00:00:00 2001 From: Cubicake <74905@stpeters.sa.edu.au> Date: Sun, 28 Jun 2026 16:05:13 +0930 Subject: [PATCH 15/21] Remove unused import --- .../java/games/cubi/raycastedantiesp/core/view/BlockView.java | 1 - 1 file changed, 1 deletion(-) diff --git a/core/src/main/java/games/cubi/raycastedantiesp/core/view/BlockView.java b/core/src/main/java/games/cubi/raycastedantiesp/core/view/BlockView.java index 0319ab6a..f5d9dce8 100644 --- a/core/src/main/java/games/cubi/raycastedantiesp/core/view/BlockView.java +++ b/core/src/main/java/games/cubi/raycastedantiesp/core/view/BlockView.java @@ -1,6 +1,5 @@ package games.cubi.raycastedantiesp.core.view; -import ca.spottedleaf.common.function.BiBooleanObjectConsumer; import games.cubi.locatables.BlockLocatable; import games.cubi.locatables.implementations.ImmutableBlockLocatable; import games.cubi.raycastedantiesp.core.locatables.TileEntityLocatable; From b5540006e3a15c025e7454685044167eccf332c3 Mon Sep 17 00:00:00 2001 From: Cubicake <74905@stpeters.sa.edu.au> Date: Sun, 28 Jun 2026 16:08:58 +0930 Subject: [PATCH 16/21] Move null state guard here --- .../viewcontrollers/PacketEventsBlockViewController.java | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packetevents/src/main/java/games/cubi/raycastedantiesp/packetevents/viewcontrollers/PacketEventsBlockViewController.java b/packetevents/src/main/java/games/cubi/raycastedantiesp/packetevents/viewcontrollers/PacketEventsBlockViewController.java index 56aa117f..8b9ff514 100644 --- a/packetevents/src/main/java/games/cubi/raycastedantiesp/packetevents/viewcontrollers/PacketEventsBlockViewController.java +++ b/packetevents/src/main/java/games/cubi/raycastedantiesp/packetevents/viewcontrollers/PacketEventsBlockViewController.java @@ -154,16 +154,15 @@ private void processTileEntityTransitions(User viewer, BlockView blockView) { for (BlockViewTransition transition : blockView.drainTransitions()) { BlockLocatable location = transition.location(); TileEntityLocatable state = getTrackedTileEntity(blockView, location); - + if (state == null || state.blockID() == 0) { + continue; + } switch (transition.type()) { case HIDE -> viewer.writePacketSilently(new WrapperPlayServerBlockChange( new Vector3i(location.blockX(), location.blockY(), location.blockZ()), getHiddenBlockId(location.blockY()) )); case SHOW -> { - if (state == null || state.blockID() == 0) { - continue; - } viewer.writePacketSilently(new WrapperPlayServerBlockChange( new Vector3i(location.blockX(), location.blockY(), location.blockZ()), state.blockID() From 02253fd48754f3495996c14296dec0faaaf90119 Mon Sep 17 00:00:00 2001 From: Cubicake <74905@stpeters.sa.edu.au> Date: Sun, 28 Jun 2026 17:21:33 +0930 Subject: [PATCH 17/21] Turns out ConcurrentHashMap is better; Also entityUUIDsByID is accessed from netty thread only so it can be a fastutil map --- .../view/PacketEventsEntityView.java | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/packetevents/src/main/java/games/cubi/raycastedantiesp/packetevents/view/PacketEventsEntityView.java b/packetevents/src/main/java/games/cubi/raycastedantiesp/packetevents/view/PacketEventsEntityView.java index a1c0e32d..ff122d3d 100644 --- a/packetevents/src/main/java/games/cubi/raycastedantiesp/packetevents/view/PacketEventsEntityView.java +++ b/packetevents/src/main/java/games/cubi/raycastedantiesp/packetevents/view/PacketEventsEntityView.java @@ -1,8 +1,6 @@ package games.cubi.raycastedantiesp.packetevents.view; import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue; -import ca.spottedleaf.concurrentutil.map.SWMRHashTable; -import ca.spottedleaf.concurrentutil.map.SWMRInt2ObjectHashTable; import games.cubi.locatables.Locatable; import games.cubi.logs.Logger; import games.cubi.raycastedantiesp.core.locatables.NettyEntityLocatable; @@ -10,14 +8,16 @@ import games.cubi.raycastedantiesp.core.view.EntityView; import games.cubi.raycastedantiesp.core.view.EntityViewTransition; import games.cubi.raycastedantiesp.packetevents.locatables.PacketEventsEntity; +import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; import org.jetbrains.annotations.NotNull; import java.util.*; +import java.util.concurrent.ConcurrentHashMap; import java.util.function.Consumer; public class PacketEventsEntityView extends SingleThreadedGuard implements EntityView { - private final Map entitiesByUUID = new SWMRHashTable<>(); - private final SWMRInt2ObjectHashTable entityUUIDsByID = new SWMRInt2ObjectHashTable<>(); + private final Map entitiesByUUID = new ConcurrentHashMap<>(); + private final Int2ObjectOpenHashMap entityUUIDsByID = new Int2ObjectOpenHashMap<>(); private final MultiThreadedQueue transitions = new MultiThreadedQueue<>(); private final boolean isPlayerView; @@ -36,7 +36,6 @@ public static PacketEventsEntityView createEntityView() { @Override public void insertEntity(PacketEventsEntity entity) { - guardThread(); if (entity == null || entity.entityUUID() == null) { Logger.error(new RuntimeException("Attempted to insert null entity or entity with null UUID into EntityView"), 2, PacketEventsEntityView.class); return; @@ -44,6 +43,7 @@ public void insertEntity(PacketEventsEntity entity) { UUID entityUUID = entity.entityUUID(); int entityID = entity.entityID(); + guardThread(); UUID previousUUIDForID = entityUUIDsByID.put(entityID, entityUUID); if (previousUUIDForID != null && !previousUUIDForID.equals(entityUUID)) { PacketEventsEntity previousEntityForID = entitiesByUUID.get(previousUUIDForID); @@ -82,7 +82,6 @@ public void removeEntity(int entityID) { @Override public void removeEntity(UUID entityUUID, int currentTick) { - guardThread(); int entityID = getEntityID(entityUUID); removeEntity(entityID, currentTick); @@ -105,6 +104,7 @@ public boolean exists(UUID entityUUID) { @Override public boolean exists(int entityID) { + guardThread(); return entityUUIDsByID.containsKey(entityID); } @@ -173,6 +173,7 @@ public Collection getKnownEntities() { @Override public int[] getKnownEntityIDs() { + /* guardThread(); // This is only called while clearing on the Netty thread, so size() // is stable and can be used as the exact output array length. @@ -180,8 +181,10 @@ public int[] getKnownEntityIDs() { int[] count = new int[1]; entityUUIDsByID.forEachKey(entityID -> { entityIDs[count[0]++] = entityID; - }); - return entityIDs; + });*/ + guardThread(); + return entityUUIDsByID.keySet().toIntArray(); + //return entityIDs; } @Override @@ -239,6 +242,7 @@ public void clear() { } private PacketEventsEntity getTrackedEntity(int entityID) { + guardThread(); UUID entityUUID = entityUUIDsByID.get(entityID); return entityUUID == null ? null : entitiesByUUID.get(entityUUID); } @@ -246,6 +250,7 @@ private PacketEventsEntity getTrackedEntity(int entityID) { public String getStringDataForDebugging() { StringBuilder builder = new StringBuilder(); builder.append("EntityView isPlayerView=").append(isPlayerView).append("\n"); + guardThread(); entityUUIDsByID.forEach((entityID, entityUUID) -> { PacketEventsEntity entity = entitiesByUUID.get(entityUUID); builder.append("EntityID=").append(entityID) From ce8ce72151c3b41bdb492ed1283967b71e1e9fc8 Mon Sep 17 00:00:00 2001 From: Cubicake <74905@stpeters.sa.edu.au> Date: Sun, 28 Jun 2026 17:22:21 +0930 Subject: [PATCH 18/21] Bump version --- core/build.gradle.kts | 2 +- platform-paper/build.gradle.kts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/core/build.gradle.kts b/core/build.gradle.kts index ee283694..84199da5 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -35,7 +35,7 @@ dependencies { testImplementation("it.unimi.dsi:fastutil:8.5.18") } -val coreVersion = "0.5.2-SNAPSHOT" +val coreVersion = "0.5.3-SNAPSHOT" val isRelease = gradle.startParameter.taskNames.any { it.contains("buildRelease") diff --git a/platform-paper/build.gradle.kts b/platform-paper/build.gradle.kts index 2cb1f2d9..86cd0225 100644 --- a/platform-paper/build.gradle.kts +++ b/platform-paper/build.gradle.kts @@ -50,7 +50,7 @@ val javaToolchainService = project.extensions.getByType(JavaToolchainService::cl group = "games.cubi.raycastedantiesp.paper" -val platformPaperVersion: String = "0.6.1-SNAPSHOT" +val platformPaperVersion: String = "0.7.0-SNAPSHOT" val coreVersion = project(":core").version.toString() val commitShort = providers.exec { From f7d21a716d5a7e845164201c070acf4b9a67360a Mon Sep 17 00:00:00 2001 From: Cubicake <74905@stpeters.sa.edu.au> Date: Mon, 29 Jun 2026 19:56:16 +0930 Subject: [PATCH 19/21] Use SWMRHashTable for lock-free work --- .../core/engine/SimpleEngine.java | 4 +-- .../core/view/EntityView.java | 4 +-- leafpile | 2 +- .../view/PacketEventsEntityView.java | 29 ++++++++++++------- 4 files changed, 24 insertions(+), 15 deletions(-) diff --git a/core/src/main/java/games/cubi/raycastedantiesp/core/engine/SimpleEngine.java b/core/src/main/java/games/cubi/raycastedantiesp/core/engine/SimpleEngine.java index 6139bda2..03be93fe 100644 --- a/core/src/main/java/games/cubi/raycastedantiesp/core/engine/SimpleEngine.java +++ b/core/src/main/java/games/cubi/raycastedantiesp/core/engine/SimpleEngine.java @@ -364,7 +364,7 @@ private void processTickForPlayers(List playerDataList, EntityConfig private void checkEntities(PlayerData player, Locatable playerLocation, EntityConfig entityConfig, boolean debugParticles, BlockView blockView, int currentTick, TickTimingBatch timings) { EntityView entityView = player.entityView(); - int checked = entityView.forEachNeedingRecheckEntity(entityConfig.getVisibleRecheckIntervalTicks(), currentTick, entity -> { + int checked = entityView.forEachNeedingRecheckEntity(entityConfig.getVisibleRecheckIntervalTicks(), currentTick, !(timings instanceof TickTimingBatchNoOp), entity -> { boolean wasVisible = entity.visible(); if (attachedToSelf(player, entityView, entity, currentTick)) { return; @@ -388,7 +388,7 @@ private void checkEntities(PlayerData player, Locatable playerLocation, EntityCo private void checkPlayers(PlayerData player, Locatable playerLocation, PlayerConfig playerConfig, boolean debugParticles, BlockView blockView, int currentTick, TickTimingBatch timings) { EntityView playerView = player.playerView(); - int checked = playerView.forEachNeedingRecheckEntity(playerConfig.getVisibleRecheckIntervalTicks(), currentTick, otherPlayer -> { + int checked = playerView.forEachNeedingRecheckEntity(playerConfig.getVisibleRecheckIntervalTicks(), currentTick, !(timings instanceof TickTimingBatchNoOp), otherPlayer -> { boolean wasVisible = otherPlayer.visible(); if (attachedToSelf(player, playerView, otherPlayer, currentTick)) { return; diff --git a/core/src/main/java/games/cubi/raycastedantiesp/core/view/EntityView.java b/core/src/main/java/games/cubi/raycastedantiesp/core/view/EntityView.java index 9bac4a25..c4311abd 100644 --- a/core/src/main/java/games/cubi/raycastedantiesp/core/view/EntityView.java +++ b/core/src/main/java/games/cubi/raycastedantiesp/core/view/EntityView.java @@ -56,9 +56,9 @@ public interface EntityView> extends Clearable /** * Iterates currently tracked entities that should be visibility-checked. * - * @return number of entities passed to {@code action}. + * @return number of entities passed to {@code action}, or 0 if {@code countingActuallyNeeded} is false. */ - int forEachNeedingRecheckEntity(int recheckTicks, int currentTick, Consumer> action); + int forEachNeedingRecheckEntity(int recheckTicks, int currentTick, boolean countingActuallyNeeded, Consumer> action); boolean hasPendingTransitions(); diff --git a/leafpile b/leafpile index 899341f9..aac6df93 160000 --- a/leafpile +++ b/leafpile @@ -1 +1 @@ -Subproject commit 899341f9976a2f224a1a8914999c99baa98dc0c4 +Subproject commit aac6df93ad1a4caad36ece099bf924a35b00c902 diff --git a/packetevents/src/main/java/games/cubi/raycastedantiesp/packetevents/view/PacketEventsEntityView.java b/packetevents/src/main/java/games/cubi/raycastedantiesp/packetevents/view/PacketEventsEntityView.java index ff122d3d..682b3d31 100644 --- a/packetevents/src/main/java/games/cubi/raycastedantiesp/packetevents/view/PacketEventsEntityView.java +++ b/packetevents/src/main/java/games/cubi/raycastedantiesp/packetevents/view/PacketEventsEntityView.java @@ -1,6 +1,7 @@ package games.cubi.raycastedantiesp.packetevents.view; import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue; +import ca.spottedleaf.concurrentutil.map.SWMRHashTable; import games.cubi.locatables.Locatable; import games.cubi.logs.Logger; import games.cubi.raycastedantiesp.core.locatables.NettyEntityLocatable; @@ -12,11 +13,10 @@ import org.jetbrains.annotations.NotNull; import java.util.*; -import java.util.concurrent.ConcurrentHashMap; import java.util.function.Consumer; public class PacketEventsEntityView extends SingleThreadedGuard implements EntityView { - private final Map entitiesByUUID = new ConcurrentHashMap<>(); + private final SWMRHashTable entitiesByUUID = new SWMRHashTable<>(); private final Int2ObjectOpenHashMap entityUUIDsByID = new Int2ObjectOpenHashMap<>(); private final MultiThreadedQueue transitions = new MultiThreadedQueue<>(); private final boolean isPlayerView; @@ -168,7 +168,9 @@ public void setVisibility(@NotNull NettyEntityLocatable entity, boolean vis @Override public Collection getKnownEntities() { - return List.copyOf(entitiesByUUID.keySet()); + List known = new ArrayList<>(entitiesByUUID.size()); + entitiesByUUID.forEachKey(known::add); + return known; } @Override @@ -201,16 +203,23 @@ public int forEachNeedingRecheck(int recheckTicks, int currentTick, Consumer> action) { - int processed = 0; - for (PacketEventsEntity entity : entitiesByUUID.values()) { + public int forEachNeedingRecheckEntity(int recheckTicks, int currentTick, boolean countingActuallyNeeded, Consumer> action) { + if (countingActuallyNeeded) { + return entitiesByUUID.forEachValueCounted( (entity) -> { + if (entity.visible() && (recheckTicks < 0 || currentTick - entity.lastChecked() < recheckTicks)) { + return false; + } + action.accept(entity); + return true; + }); + } + entitiesByUUID.forEachValue( (entity) -> { if (entity.visible() && (recheckTicks < 0 || currentTick - entity.lastChecked() < recheckTicks)) { - continue; + return; } action.accept(entity); - processed++; - } - return processed; + }); + return 0; } @Override From 50b259ed5b685bc4cce22538b21a50435612a140 Mon Sep 17 00:00:00 2001 From: Cubicake <74905@stpeters.sa.edu.au> Date: Wed, 1 Jul 2026 21:57:59 +0930 Subject: [PATCH 20/21] Make sure test can't be JIT optimised away --- .../paper/commands/RaycastedAntiESPCommand.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/platform-paper/src/main/java/games/cubi/raycastedantiesp/paper/commands/RaycastedAntiESPCommand.java b/platform-paper/src/main/java/games/cubi/raycastedantiesp/paper/commands/RaycastedAntiESPCommand.java index 47c1559e..d65a5d73 100644 --- a/platform-paper/src/main/java/games/cubi/raycastedantiesp/paper/commands/RaycastedAntiESPCommand.java +++ b/platform-paper/src/main/java/games/cubi/raycastedantiesp/paper/commands/RaycastedAntiESPCommand.java @@ -170,16 +170,19 @@ void debugCommand(Player player) throws CommandSyntaxException { locatables[i] = playerLocatable.clonePlainAndCentreIfBlockLocation().add(unitDirection); } Bukkit.getAsyncScheduler().runNow(RaycastedAntiESP.get(), (ignored) -> { + int successfulRays = 0; long startTime = System.nanoTime(); for (Locatable locatable : locatables) { - RaycastUtil.raycast(playerData, playerLocatable, locatable, 3, 0, 100, false, playerData.blockView(), 1, null); + if (RaycastUtil.raycast(playerData, playerLocatable, locatable, 3, 0, 100, false, playerData.blockView(), 1, null)) successfulRays++; } long endTime = System.nanoTime(); long duration = endTime - startTime; double averageTime = duration / (double) locatables.length; + final int successfulRaysFinal = successfulRays; PaperScheduler.runForAudience(RaycastedAntiESP.get(), player, () -> { player.sendRichMessage("Average raycast time: " + averageTime + " nanoseconds"); player.sendRichMessage("Total raycast time: " + duration + " nanoseconds"); + player.sendRichMessage("Successful rays: " + successfulRaysFinal + "/" + locatables.length); }); }); } From c814e1e0b6e7b0857e2f1ada73a564f0fbd20009 Mon Sep 17 00:00:00 2001 From: Cubicake <74905@stpeters.sa.edu.au> Date: Fri, 3 Jul 2026 17:29:49 +0930 Subject: [PATCH 21/21] Bump version --- core/build.gradle.kts | 2 +- platform-paper/build.gradle.kts | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 84199da5..ae5ee5d2 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -35,7 +35,7 @@ dependencies { testImplementation("it.unimi.dsi:fastutil:8.5.18") } -val coreVersion = "0.5.3-SNAPSHOT" +val coreVersion = "0.5.4-SNAPSHOT" val isRelease = gradle.startParameter.taskNames.any { it.contains("buildRelease") diff --git a/platform-paper/build.gradle.kts b/platform-paper/build.gradle.kts index 86cd0225..efabe950 100644 --- a/platform-paper/build.gradle.kts +++ b/platform-paper/build.gradle.kts @@ -50,7 +50,7 @@ val javaToolchainService = project.extensions.getByType(JavaToolchainService::cl group = "games.cubi.raycastedantiesp.paper" -val platformPaperVersion: String = "0.7.0-SNAPSHOT" +val platformPaperVersion: String = "0.7.1-SNAPSHOT" val coreVersion = project(":core").version.toString() val commitShort = providers.exec { @@ -134,6 +134,7 @@ tasks.shadowJar { "ca.spottedleaf", "games.cubi.libs.raycastedantiesp.spottedleaf" ) + minimize {} // get rid of leafpile bloat archiveBaseName.set("RaycastedAntiESP") archiveClassifier.set("") }