Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,32 @@
*/
package fr.neatmonster.nocheatplus.compat.bukkit;

import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;

import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.World;
import org.bukkit.entity.Entity;
import org.bukkit.entity.EntityType;

import fr.neatmonster.nocheatplus.compat.Bridge1_13;
import fr.neatmonster.nocheatplus.compat.SchedulerHelper;
import fr.neatmonster.nocheatplus.utilities.CheckUtils;
import fr.neatmonster.nocheatplus.utilities.map.BlockCache;
import fr.neatmonster.nocheatplus.utilities.map.BlockFlags;
import fr.neatmonster.nocheatplus.utilities.map.BlockProperties;
import fr.neatmonster.nocheatplus.utilities.map.MaterialUtil;

public class BlockCacheBukkit extends BlockCache {

private static final long FOLIA_FALLBACK_LOG_INTERVAL_MS = 10000L;
private static final int FOLIA_FALLBACK_CONSOLE_LOG_MIN_COUNT = 2;
private static final AtomicInteger foliaFallbackCount = new AtomicInteger();
private static final AtomicInteger foliaFallbackResolvedCount = new AtomicInteger();
private static final AtomicLong nextFoliaFallbackLog = new AtomicLong();

protected World world;

/** Temporary use. Use LocUtil.clone before passing on. Call setWorld(null) after use. */
Expand All @@ -50,16 +62,92 @@ public BlockCache setAccess(World world) {
@Override
public Material fetchTypeId(final int x, final int y, final int z) {
// TODO: consider setting type id and data at once.
return world.getBlockAt(x, y, z).getType();
if (world == null) {
return Material.AIR;
}
if (SchedulerHelper.isOwnedByCurrentRegion(world, x, z)) {
return world.getBlockAt(x, y, z).getType();
}
final Material fallbackType = fetchTypeIdWithLocationFallback(x, y, z);
return fallbackType == null ? Material.AIR : fallbackType;
}

@SuppressWarnings("deprecation")
@Override
public int fetchData(final int x, final int y, final int z) {
// TODO: consider setting type id and data at once.
return Bridge1_13.hasIsSwimming() ? 0 : world.getBlockAt(x, y, z).getData();
if (world == null) {
return 0;
}
if (SchedulerHelper.isOwnedByCurrentRegion(world, x, z)) {
return Bridge1_13.hasIsSwimming() ? 0 : world.getBlockAt(x, y, z).getData();
}
final Integer fallbackData = fetchDataWithLocationFallback(x, y, z);
return fallbackData == null ? 0 : fallbackData.intValue();
}

private Material fetchTypeIdWithLocationFallback(final int x, final int y, final int z) {
// Folia compatibility: if the chunk-level ownership check cannot prove safety, retry the exact location.
// If that also fails, keep AIR as the safe final fallback instead of touching another region's block state.
boolean resolved = false;
try {
if (SchedulerHelper.isOwnedByCurrentRegion(new Location(world, x, y, z))) {
resolved = true;
return world.getBlockAt(x, y, z).getType();
}
}
catch (Throwable t) {
// Keep the final AIR fallback for servers that throw while checking ownership or block state.
}
finally {
logFoliaBlockFallback("type", resolved);
}
return null;
}

@SuppressWarnings("deprecation")
private Integer fetchDataWithLocationFallback(final int x, final int y, final int z) {
// Folia compatibility: retry exact-location ownership before returning legacy data 0 as the final fallback.
boolean resolved = false;
try {
if (SchedulerHelper.isOwnedByCurrentRegion(new Location(world, x, y, z))) {
resolved = true;
return Integer.valueOf(Bridge1_13.hasIsSwimming() ? 0 : world.getBlockAt(x, y, z).getData());
}
}
catch (Throwable t) {
// Keep the final data 0 fallback for servers that throw while checking ownership or block state.
}
finally {
logFoliaBlockFallback("data", resolved);
}
return null;
}

private static void logFoliaBlockFallback(final String kind, final boolean resolved) {
if (!CheckUtils.shouldLogDebugToConsole()) {
return;
}
final int count = foliaFallbackCount.incrementAndGet();
if (resolved) {
foliaFallbackResolvedCount.incrementAndGet();
}
final long now = System.currentTimeMillis();
final long next = nextFoliaFallbackLog.get();
if (now < next || !nextFoliaFallbackLog.compareAndSet(next, now + FOLIA_FALLBACK_LOG_INTERVAL_MS)) {
return;
}
final int resolvedCount = foliaFallbackResolvedCount.getAndSet(0);
final int periodCount = foliaFallbackCount.getAndSet(0);
// Folia diagnostic: a single safe fallback during teleport/chunk handoff is expected and not useful console noise.
if (periodCount < FOLIA_FALLBACK_CONSOLE_LOG_MIN_COUNT) {
return;
}
Bukkit.getLogger().info("[NCP][Folia][BlockCache] safe " + kind + " block fallback used "
+ periodCount + " times in the last 10s, resolved=" + resolvedCount
+ ", finalSafeFallback=" + (periodCount - resolvedCount) + ".");
}

@Override
public double[] fetchBounds(final int x, final int y, final int z){
Material mat = getType(x, y, z);
Expand All @@ -74,11 +162,17 @@ public double[] fetchBounds(final int x, final int y, final int z){

@Override
public boolean standsOnEntity(final Entity entity, final double minX, final double minY, final double minZ, final double maxX, final double maxY, final double maxZ){
if (!SchedulerHelper.isOwnedByCurrentRegion(entity)) {
return false;
}
try{
// TODO: Probably check other ids too before doing this ?
for (final Entity other : entity.getNearbyEntities(2.0, 2.0, 2.0)){
if (!SchedulerHelper.isOwnedByCurrentRegion(other)) {
continue;
}
final EntityType type = other.getType();
if (!MaterialUtil.isBoat(type) && type != EntityType.SHULKER){ // && !(other instanceof Minecart))
if (!MaterialUtil.isBoat(type) && type != EntityType.SHULKER){ // && !(other instanceof Minecart))
continue;
}
final double locY = entity.getLocation(useLoc).getY();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,15 @@

import java.util.Map;

import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.World;
import org.bukkit.block.data.BlockData;
import org.bukkit.block.data.Waterlogged;
import org.bukkit.entity.Entity;
import org.bukkit.entity.EntityType;

import fr.neatmonster.nocheatplus.compat.SchedulerHelper;
import fr.neatmonster.nocheatplus.compat.bukkit.model.BukkitShapeModel;
import fr.neatmonster.nocheatplus.compat.versions.ClientVersion;
import fr.neatmonster.nocheatplus.players.IPlayerData;
Expand Down Expand Up @@ -61,6 +63,37 @@ public int fetchData(int x, int y, int z) {
}
return super.fetchData(x, y, z);
}

@Override
public long fetchExtendedData(final int x, final int y, final int z) {
if (world == null) {
return 0L;
}
// Folia support: only query live Bukkit block data from the owning region thread.
if (SchedulerHelper.isOwnedByCurrentRegion(world, x, z)) {
return fetchWaterloggedFlags(x, y, z);
}
try {
if (SchedulerHelper.isOwnedByCurrentRegion(new Location(world, x, y, z))) {
return fetchWaterloggedFlags(x, y, z);
}
} catch (Throwable ignored) {
// Keep the cache fallback conservative if the scheduler API is unavailable.
}
return 0L;
}

private long fetchWaterloggedFlags(final int x, final int y, final int z) {
final IPlayerData pData = getPlayerData();
if (pData != null && pData.getClientVersion().isLowerThan(ClientVersion.V_1_13)) {
return 0L;
}
final BlockData data = world.getBlockAt(x, y, z).getBlockData();
if (data instanceof Waterlogged && ((Waterlogged) data).isWaterlogged()) {
return BlockFlags.F_WATER | BlockFlags.F_LIQUID | BlockFlags.F_WATERLOGGED;
}
return 0L;
}

@Override
public double[] fetchBounds(int x, int y, int z) {
Expand Down Expand Up @@ -96,9 +129,15 @@ public boolean isCollisionSameVisual(int x, int y, int z) {

@Override
public boolean standsOnEntity(final Entity entity, final double minX, final double minY, final double minZ, final double maxX, final double maxY, final double maxZ) {
if (!SchedulerHelper.isOwnedByCurrentRegion(entity)) {
return false;
}
try {
// TODO: Probably check vehicle ids too before doing this ?
for (final Entity vehicle : entity.getNearbyEntities(0.1, 2.0, 0.1)) {
if (!SchedulerHelper.isOwnedByCurrentRegion(vehicle)) {
continue;
}
final EntityType type = vehicle.getType();
if (!MaterialUtil.isBoat(type) && type != EntityType.SHULKER) { // && !(vehicle instanceof Minecart))
continue;
Expand All @@ -114,17 +153,4 @@ public boolean standsOnEntity(final Entity entity, final double minX, final doub
}
return false;
}

@Override
public long fetchExtendedData(int x, int y, int z) {
BlockData bd = world.getBlockAt(x, y, z).getBlockData();
if (bd instanceof Waterlogged && ((Waterlogged)bd).isWaterlogged()) {
final IPlayerData pData = getPlayerData();
if (pData != null) {
if (pData.getClientVersion().isLowerThan(ClientVersion.V_1_13)) return 0;
}
return BlockFlags.F_WATER | BlockFlags.F_LIQUID | BlockFlags.F_WATERLOGGED;
}
return 0;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ private int operationToInt(final Operation operation) {
* @param id
* @return
*/
@SuppressWarnings({"deprecation", "removal"})
private AttributeModifier getModifier(final AttributeInstance attrInst, final UUID id) {
for (final AttributeModifier mod : attrInst.getModifiers()) {
if (id.equals(mod.getUniqueId())) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,13 @@
import org.bukkit.block.data.type.Snow;

import fr.neatmonster.nocheatplus.utilities.map.BlockCache;
import fr.neatmonster.nocheatplus.utilities.map.BlockCache.IBlockCacheNode;

public class BukkitSnow implements BukkitShapeModel {

private static final double[][] SNOW_LAYERS = {
null,
private static final int MAX_COLLISION_LAYER = 7;
private static final double[][] SNOW_COLLISION_LAYERS = {
{0.0, 0.0, 0.0, 1.0, 0.0, 1.0},
{0.0, 0.0, 0.0, 1.0, 0.125, 1.0},
{0.0, 0.0, 0.0, 1.0, 0.250, 1.0},
{0.0, 0.0, 0.0, 1.0, 0.375, 1.0},
Expand All @@ -38,14 +40,8 @@ public class BukkitSnow implements BukkitShapeModel {

@Override
public double[] getShape(BlockCache blockCache, World world, int x, int y, int z) {
// TODO: Backward Handling
final Block block = world.getBlockAt(x, y, z);
final BlockState state = block.getState();
final BlockData blockData = state.getBlockData();
if (blockData instanceof Snow) {
return SNOW_LAYERS[((Snow)blockData).getLayers() - 1];
}
return new double[] {0.0, 0.0, 0.0, 1.0, 1.0, 1.0};
// Snow model: collision height is one layer lower than visual height; one-layer snow has no collision.
return SNOW_COLLISION_LAYERS[getCollisionLayer(blockCache, world, x, y, z)];
}

@Override
Expand All @@ -60,12 +56,33 @@ public boolean isCollisionSameVisual(BlockCache blockCache, World world, int x,

@Override
public int getFakeData(BlockCache blockCache, World world, int x, int y, int z) {
final Block block = world.getBlockAt(x, y, z);
final BlockState state = block.getState();
final BlockData blockData = state.getBlockData();
if (blockData instanceof Snow) {
return ((Snow)blockData).getLayers() - 1;
return getCollisionLayer(blockCache, world, x, y, z);
}

private int getCollisionLayer(final BlockCache blockCache, final World world, final int x, final int y, final int z) {
if (world != null) {
try {
final Block block = world.getBlockAt(x, y, z);
final BlockState state = block.getState();
final BlockData blockData = state.getBlockData();
if (blockData instanceof Snow) {
return clampCollisionLayer(((Snow) blockData).getLayers() - 1);
}
}
catch (Throwable ignored) {
// Folia/async fallback: use already cached legacy data if it exists, otherwise use no collision.
}
}
if (blockCache != null) {
final IBlockCacheNode node = blockCache.getBlockCacheNode(x, y, z);
if (node != null && node.isDataFetched()) {
return clampCollisionLayer(node.getData());
}
}
return 0;
}
}

private int clampCollisionLayer(final int layer) {
return Math.max(0, Math.min(MAX_COLLISION_LAYER, layer));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ public class KeepAliveAdapter extends BaseAdapter {
private final KeepAliveFrequency frequencyCheck = new KeepAliveFrequency();

public KeepAliveAdapter(Plugin plugin) {
super(plugin, ListenerPriority.LOW, PacketType.Play.Client.KEEP_ALIVE);
super(plugin, ListenerPriority.LOW, PacketType.Play.Client.KEEP_ALIVE, PacketType.Play.Server.KEEP_ALIVE);
this.checkType = CheckType.NET_KEEPALIVEFREQUENCY;
// Add feature tags for checks.
if (NCPAPIProvider.getNoCheatPlusAPI().getWorldDataManager().isActiveAnywhere(CheckType.NET_KEEPALIVEFREQUENCY)) {
Expand All @@ -70,11 +70,12 @@ public void onPacketReceiving(final PacketEvent event) {
final IPlayerData pData = DataManager.getPlayerDataSafe(player);
if (pData == null) return;
final NetData data = pData.getGenericInstance(NetData.class);
recordKeepAlivePacketDetails(event, data, time);
data.lastKeepAliveTime = time;
final NetConfig cc = pData.getGenericInstance(NetConfig.class);

// Run check(s).
// TODO: Match vs. outgoing keep alive requests.
// KeepAlive model: outgoing server ids are recorded in onPacketSending; matching replies are expected.
// TODO: Better modeling of actual packet sequences (flying vs. keep alive vs. request/ping).
// TODO: Better integration with god-mode check / trigger reset ndt.
if (frequencyCheck.isEnabled(player, pData)
Expand All @@ -83,8 +84,25 @@ public void onPacketReceiving(final PacketEvent event) {
}
}

private void recordKeepAlivePacketDetails(final PacketEvent event, final NetData data, final long time) {
final KeepAlivePacketInfo info = KeepAlivePacketInfo.read(event.getPacket());
data.recordKeepAlivePacket(time, info.idAvailable, info.id, info.idType, info.longCount, info.intCount,
event.isAsync(), Thread.currentThread().getName());
}

@Override
public void onPacketSending(PacketEvent event) {
// TODO: Maybe detect if keep alive wasn't asked for + allow cancel.
final Player player = event.getPlayer();
if (player == null) {
return;
}
final IPlayerData pData = DataManager.getPlayerDataSafe(player);
if (pData == null) {
return;
}
final KeepAlivePacketInfo info = KeepAlivePacketInfo.read(event.getPacket());
pData.getGenericInstance(NetData.class).recordOutgoingKeepAlivePacket(System.currentTimeMillis(),
info.idAvailable, info.id, info.idType, info.longCount, info.intCount,
event.isAsync(), Thread.currentThread().getName());
}
}
Loading