[BLEEDING] Sketch one approach for on-ground with past states.

Still incomplete, could contain bugs (endless loops, perhaps).
Invalidation
mechanics may need to be refined.

Not covered:
* Exemption for moves resulting from horizontal push/pull.

*
This commit is contained in:
asofold 2016-12-06 00:14:29 +01:00
parent 3cd7625516
commit e6673de09e
9 changed files with 530 additions and 28 deletions

View File

@ -751,7 +751,7 @@ public class MovingListener extends CheckListener implements TickListener, IRemo
else if (checkCf) {
// CreativeFly
if (newTo == null) {
newTo = creativeFly.check(player, pFrom, pTo, data, cc, time);
newTo = creativeFly.check(player, pFrom, pTo, data, cc, time, useBlockChangeTracker);
}
data.sfHoverTicks = -1;
data.sfLowJump = false;

View File

@ -27,6 +27,8 @@ import fr.neatmonster.nocheatplus.checks.moving.model.PlayerMoveData;
import fr.neatmonster.nocheatplus.checks.moving.player.Passable;
import fr.neatmonster.nocheatplus.checks.moving.util.MovingUtil;
import fr.neatmonster.nocheatplus.compat.MCAccess;
import fr.neatmonster.nocheatplus.compat.blocks.changetracker.BlockChangeTracker;
import fr.neatmonster.nocheatplus.utilities.TickTask;
import fr.neatmonster.nocheatplus.utilities.location.PlayerLocation;
import fr.neatmonster.nocheatplus.utilities.map.BlockCache;
import fr.neatmonster.nocheatplus.utilities.map.BlockProperties;
@ -49,9 +51,13 @@ public class LostGround {
* @param sprinting
* @param data
* @param cc
* @param useBlockChangeTracker
* @return If touching the ground was lost.
*/
public static boolean lostGround(final Player player, final PlayerLocation from, final PlayerLocation to, final double hDistance, final double yDistance, final boolean sprinting, final PlayerMoveData lastMove, final MovingData data, final MovingConfig cc, final Collection<String> tags) {
public static boolean lostGround(final Player player, final PlayerLocation from, final PlayerLocation to,
final double hDistance, final double yDistance, final boolean sprinting,
final PlayerMoveData lastMove, final MovingData data, final MovingConfig cc,
final BlockChangeTracker blockChangeTracker, final Collection<String> tags) {
// TODO: Regroup with appropriate conditions (toOnGround first?).
// TODO: Some workarounds allow step height (0.6 on MC 1.8).
// TODO: yDistance limit does not seem to be appropriate.
@ -79,6 +85,23 @@ public class LostGround {
}
}
}
// Block change tracker (kept extra for now).
if (blockChangeTracker != null && lostGroundPastState(player, from, to, data, cc, blockChangeTracker, tags)) {
return true;
}
return false;
}
private static boolean lostGroundPastState(final Player player,
final PlayerLocation from, final PlayerLocation to,
final MovingData data, final MovingConfig cc, final BlockChangeTracker blockChangeTracker, final Collection<String> tags) {
// TODO: Heuristics.
// TODO: full y-move at from-xz (!).
final int tick = TickTask.getTick();
if (from.isOnGroundOpportune(cc.yOnGround, 0L, blockChangeTracker, data.blockChangeRef, tick)) {
// TODO: Not sure with setBackSafe here (could set back a hundred blocks on parkour).
return applyLostGround(player, from, false, data.playerMoves.getCurrentMove(), data, "past", tags);
}
return false;
}
@ -297,6 +320,7 @@ public class LostGround {
// TODO: Full bounds check (!).
final Location ref = from.getLocation();
ref.setY(to.getY());
// TODO: passable test is obsolete with PassableAxisTracing.
if (Passable.isPassable(from.getLocation(), ref)) {
// TODO: Needs new model (store detailed on-ground properties).
return applyLostGround(player, from, false, thisMove, data, "vcollide", tags);

View File

@ -23,6 +23,7 @@ import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.entity.Player;
import fr.neatmonster.nocheatplus.NCPAPIProvider;
import fr.neatmonster.nocheatplus.actions.ParameterName;
import fr.neatmonster.nocheatplus.checks.Check;
import fr.neatmonster.nocheatplus.checks.CheckType;
@ -37,6 +38,7 @@ import fr.neatmonster.nocheatplus.checks.moving.model.PlayerMoveData;
import fr.neatmonster.nocheatplus.checks.moving.util.MovingUtil;
import fr.neatmonster.nocheatplus.compat.Bridge1_9;
import fr.neatmonster.nocheatplus.compat.BridgeMisc;
import fr.neatmonster.nocheatplus.compat.blocks.changetracker.BlockChangeTracker;
import fr.neatmonster.nocheatplus.utilities.StringUtil;
import fr.neatmonster.nocheatplus.utilities.location.PlayerLocation;
import fr.neatmonster.nocheatplus.utilities.location.TrigUtil;
@ -48,12 +50,14 @@ import fr.neatmonster.nocheatplus.utilities.location.TrigUtil;
public class CreativeFly extends Check {
private final List<String> tags = new LinkedList<String>();
private final BlockChangeTracker blockChangeTracker;
/**
* Instantiates a new creative fly check.
*/
public CreativeFly() {
super(CheckType.MOVING_CREATIVEFLY);
blockChangeTracker = NCPAPIProvider.getNoCheatPlusAPI().getBlockChangeTracker();
}
/**
@ -66,7 +70,8 @@ public class CreativeFly extends Check {
* @param time Milliseconds.
* @return
*/
public Location check(final Player player, final PlayerLocation from, final PlayerLocation to, final MovingData data, final MovingConfig cc, final long time) {
public Location check(final Player player, final PlayerLocation from, final PlayerLocation to,
final MovingData data, final MovingConfig cc, final long time, final boolean useBlockChangeTracker) {
// Reset tags, just in case.
tags.clear();
@ -99,7 +104,8 @@ public class CreativeFly extends Check {
// Nothing to do.
}
}
else if (LostGround.lostGround(player, from, to, hDistance, yDistance, sprinting, lastMove, data, cc, tags)) {
else if (LostGround.lostGround(player, from, to, hDistance, yDistance, sprinting, lastMove,
data, cc, useBlockChangeTracker ? blockChangeTracker : null, tags)) {
// Nothing to do.
}
}

View File

@ -153,7 +153,8 @@ public class SurvivalFly extends Check {
// Set some flags.
final boolean fromOnGround = thisMove.from.onGround;
final boolean toOnGround = thisMove.to.onGround;
final boolean resetTo = toOnGround || to.isResetCond();
final boolean resetTo = toOnGround || to.isResetCond()
|| useBlockChangeTracker && toOnGroundPastStates(from, to, thisMove, tick, data, cc);
// Determine if the player is actually sprinting.
final boolean sprinting;
@ -211,7 +212,13 @@ public class SurvivalFly extends Check {
// TODO: Extra workarounds for toOnGround (step-up is a case with to on ground)?
else if (isSamePos) {
// TODO: This isn't correct, needs redesign.
if (lastMove.toIsValid && lastMove.hDistance > 0.0 && lastMove.yDistance < -0.3) {
if (useBlockChangeTracker && from.isOnGroundOpportune(cc.yOnGround, 0L, blockChangeTracker,
data.blockChangeRef, tick)) {
// TODO: Quick addition. Reconsider entry points etc.
resetFrom = true;
tags.add("pastground_from");
}
else if (lastMove.toIsValid && lastMove.hDistance > 0.0 && lastMove.yDistance < -0.3) {
// Note that to is not on ground either.
resetFrom = LostGround.lostGroundStill(player, from, to, hDistance, yDistance, sprinting, lastMove, data, cc, tags);
} else {
@ -223,7 +230,8 @@ public class SurvivalFly extends Check {
// TODO: More refined conditions possible ?
// TODO: Consider if (!resetTo) ?
// Check lost-ground workarounds.
resetFrom = LostGround.lostGround(player, from, to, hDistance, yDistance, sprinting, lastMove, data, cc, tags);
resetFrom = LostGround.lostGround(player, from, to, hDistance, yDistance, sprinting, lastMove, data, cc,
useBlockChangeTracker ? blockChangeTracker : null, tags);
// Note: if not setting resetFrom, other places have to check assumeGround...
}
@ -630,6 +638,18 @@ public class SurvivalFly extends Check {
return null;
}
private boolean toOnGroundPastStates(final PlayerLocation from, final PlayerLocation to,
final PlayerMoveData thisMove, int tick, final MovingData data, final MovingConfig cc) {
// TODO: Heuristics / more / which? (too short move, typical step up moves, typical levels, ...)
if (to.isOnGroundOpportune(cc.yOnGround, 0L, blockChangeTracker, data.blockChangeRef, tick)) {
tags.add("pastground_to");
return true;
}
else {
return false;
}
}
/**
* Check for push/pull by pistons, alter data appropriately (blockChangeId).
*

View File

@ -29,6 +29,7 @@ public class BlockChangeReference {
* checking, update with updateFinal.
*/
public BlockChangeEntry lastUsedEntry = null;
// TODO: Consider to store the tick of when lastUsedEntry had been used, to allow invalidation.
/**
* Indicate if the timing of the last entry is still regarded as valid.

View File

@ -42,6 +42,7 @@ import fr.neatmonster.nocheatplus.utilities.ds.map.LinkedCoordHashMap;
import fr.neatmonster.nocheatplus.utilities.ds.map.LinkedCoordHashMap.MoveOrder;
import fr.neatmonster.nocheatplus.utilities.map.BlockCache;
import fr.neatmonster.nocheatplus.utilities.map.BlockCache.IBlockCacheNode;
import fr.neatmonster.nocheatplus.utilities.map.BlockProperties;
/**
* Keep track of block changes, to allow mitigation of false positives. Think of
@ -266,6 +267,8 @@ public class BlockChangeTracker {
/** Ensure to set from extern. */
private IGenericInstanceHandle<BlockCache> blockCacheHandle = null;
private final OnGroundReference onGroundReference = new OnGroundReference();
/*
* TODO: Consider tracking regions of player activity (chunk sections, with
* a margin around the player) and filter.
@ -569,6 +572,88 @@ public class BlockChangeTracker {
return null;
}
/**
* Determine if a past state can be found where the given bounds would have
* been on ground. The span of the given BlockChangeReference instance is
* only updated on success (pass a copy or store span/data otherwise for
* checking multiple blocks). This method will only check a position, if at
* least one stored node can be found. If no stored node exist for the
* world+coordinates, false will be returned (assumes that you have already
* checked with BlockProperties.isOnGround or similar).
*
* @param blockCache
* @param ref
* @param tick
* @param worldId
* @param minX
* @param minY
* @param minZ
* @param maxX
* @param maxY
* @param maxZ
* @param ignoreFlags
* @return
*/
public boolean isOnGround(final BlockCache blockCache,
final BlockChangeReference ref, final int tick, final UUID worldId,
final double minX, final double minY, final double minZ,
final double maxX, final double maxY, final double maxZ,
final long ignoreFlags) {
// (The method has been put here for efficiency. Alternative: put specific stuff into OnGroundReference.)
// TODO: Keep the outer iteration code in line with BlockProperties.isOnGround.
final WorldNode worldNode = getValidWorldNode(tick, worldId);
if (worldNode == null) {
return false;
}
final int maxBlockY = blockCache.getMaxBlockY();
final int iMinX = Location.locToBlock(minX);
final int iMaxX = Location.locToBlock(maxX);
final int iMinY = Location.locToBlock(minY - 0.5626);
if (iMinY > maxBlockY) {
return false;
}
final int iMaxY = Math.min(Location.locToBlock(maxY), maxBlockY);
final int iMinZ = Location.locToBlock(minZ);
final int iMaxZ = Location.locToBlock(maxZ);
onGroundReference.init(blockCache, ref, ignoreFlags);
for (int x = iMinX; x <= iMaxX; x++) {
for (int z = iMinZ; z <= iMaxZ; z++) {
onGroundReference.setEntriesAbove(getValidBlockChangeEntries(tick, worldNode, x, iMaxY + 1, z));
for (int y = iMaxY; y >= iMinY; y --) {
onGroundReference.setEntries(getValidBlockChangeEntries(tick, worldNode, x, y, z));
if (!onGroundReference.hasAnyEntries() || !onGroundReference.initEntries(x, y, z)) {
// Don't break here.
continue;
}
boolean shouldBreak = true; // Indicate no better than abort-y-iteration found.
do {
switch(BlockProperties.isOnGround(blockCache, minX, minY, minZ, maxX, maxY, maxZ,
ignoreFlags, x, y, z,
onGroundReference.getNode(), onGroundReference.getNodeAbove())) {
case YES:
onGroundReference.updateSpan();
onGroundReference.clear();
return true;
case MAYBE:
shouldBreak = false;
case NO:
break;
}
} while (onGroundReference.advance());
// (End of y-loop.)
if (shouldBreak) {
break; // case NO for all, end y-iteration.
}
else {
onGroundReference.moveDown();
}
}
}
}
onGroundReference.clear();
return false;
}
/**
* Get a WorldNode instance, after lazy expiration. If no node is there, or
* the node expired, null is returned.

View File

@ -0,0 +1,366 @@
package fr.neatmonster.nocheatplus.compat.blocks.changetracker;
import java.util.LinkedList;
import java.util.ListIterator;
import fr.neatmonster.nocheatplus.compat.blocks.changetracker.BlockChangeTracker.BlockChangeEntry;
import fr.neatmonster.nocheatplus.utilities.map.BlockCache;
import fr.neatmonster.nocheatplus.utilities.map.BlockCache.IBlockCacheNode;
import fr.neatmonster.nocheatplus.utilities.map.BlockProperties;
/**
* Maintain consistent past states for a block and the block above.
*
* @author asofold
*
*/
public class OnGroundReference {
/*
* TODO: Make this a reference for use within the BlockChangeTracker at
* first. isOnGround is moved back there, but the reference carries entry,
* entryAbove, and the overlap logic + split to methods + nice to use access
* methods. Essentially this is an iterator for somehow consistent
* dual-block states.
*/
// TODO: More simplified opportunistic variant?
// TODO: Can other edge cases be excluded?
private BlockCache blockCache = null;
private BlockChangeReference ref = null;
private long ignoreFlags = 0L;
private LinkedList<BlockChangeEntry> entries = null;
private ListIterator<BlockChangeEntry> itEntries = null;
private BlockChangeEntry entry = null;
private IBlockCacheNode node = null;
private LinkedList<BlockChangeEntry> entriesAbove = null;
private ListIterator<BlockChangeEntry> itEntriesAbove = null;
private BlockChangeEntry entryAbove = null;
private IBlockCacheNode nodeAbove = null;
private int entriesAboveLockIndex = 0;
public void init(BlockCache blockCache, BlockChangeReference ref, long ignoreFlags) {
this.blockCache = blockCache;
this.ref = ref;
this.ignoreFlags = ignoreFlags;
}
public void setEntries(LinkedList<BlockChangeEntry> entries) {
this.entries = entries;
this.itEntries = null;
this.entry = null;
this.node = null;
}
public void setEntriesAbove(LinkedList<BlockChangeEntry> entriesAbove) {
this.entriesAbove = entriesAbove;
this.itEntriesAbove = null;
this.entryAbove = null;
this.nodeAbove = null;
}
public void moveDown() {
entriesAbove = entries;
itEntriesAbove = null; // Gets overridden if not null.
entryAbove = null;
nodeAbove = node; // Gets overridden, if entriesAbove are not null.
// TODO: Consider to set other fields to null.
}
public void updateSpan() {
if (entry != null) {
ref.updateSpan(entry);
}
if (entryAbove != null) {
ref.updateSpan(entryAbove);
}
}
/**
* Detach all.
*/
public void clear() {
ignoreFlags = 0L;
ref = null;
entries = entriesAbove = null;
itEntries = itEntriesAbove = null;
entry = entryAbove = null;
node = nodeAbove = null;
}
public boolean hasAnyEntries() {
return entries != null || entriesAbove != null;
}
public IBlockCacheNode getNode() {
return node;
}
public IBlockCacheNode getNodeAbove() {
return nodeAbove;
}
/**
* Initialize basics, nodes, etc. Only call after having ensured, that there
* are any entries (hasAnyEntries).
*
* @return Returns true, if a usable entry or pair of entries has been set.
* If no suitable entries exist from start, false is returned.
*/
public boolean initEntries(final int x, final int y, final int z) {
// TODO: Much of cleanup + tests + delegate to auxiliary methods (shrink code size in this method).
// TODO: Evaluate which is the most likely most often called part(s) ?
itEntries = entries == null ? null : entries.listIterator();
itEntriesAbove = entriesAbove == null ? null : entriesAbove.listIterator();
entriesAboveLockIndex = 0;
// First align to the minimum time, according to ref(!).
if (itEntries != null) {
while(itEntries.hasNext()) {
entry = itEntries.next();
if (ref != null && !ref.canUpdateWith(entry)
|| !BlockProperties.isGround(entry.previousState.getId(), ignoreFlags)) {
entry = null;
}
else {
// Start with this entry.
break;
}
}
}
// Only fetch nodes once, if no entries are there.
if (entry == null) {
node = blockCache.getOrCreateBlockCacheNode(x, y, z, false);
// Fast exclusion check right here.
if (!BlockProperties.isGround(node.getId(), ignoreFlags)) {
entriesAbove = entries;
return false;
}
}
else {
node = null;
}
itEntriesAbove = entriesAbove == null ? null : entriesAbove.listIterator();
if (itEntriesAbove != null) {
while(itEntriesAbove.hasNext()) {
entryAbove = itEntriesAbove.next();
if (ref != null && !ref.canUpdateWith(entryAbove)) {
entryAbove = null;
}
else {
// Start with this entry.
break;
}
}
}
if (entry == null && entryAbove == null) {
// Skip these.
entriesAbove = entries;
return false; // Next y.
}
if (entry != null && entryAbove != null
&& !entry.overlapsIntervalOfValidity(entryAbove)) {
// Wind the "older one" of the iterators forward until first match.
if (entryAbove.nextEntryTick >= 0 && entry.tick > entryAbove.nextEntryTick) {
entryAbove = null;
while (itEntriesAbove.hasNext()) {
entryAbove = itEntriesAbove.next();
if (entryAbove.nextEntryTick >= 0 && entry.tick > entryAbove.nextEntryTick) {
entryAbove = null;
}
else {
break;
}
}
}
else if (entry.nextEntryTick >= 0 && entryAbove.tick > entry.nextEntryTick) {
entry = null;
while(itEntries.hasNext()) {
entry = itEntries.next();
if (entry.nextEntryTick >= 0 && entryAbove.tick > entry.nextEntryTick
|| !BlockProperties.isGround(entry.previousState.getId(), ignoreFlags)) {
entry = null;
}
else {
break;
}
}
}
else {
throw new IllegalStateException("Unintended pun.");
}
if (entry == null && entryAbove == null) {
return false;
}
if (entry == null) {
node = blockCache.getOrCreateBlockCacheNode(x, y, z, false);
// Fast exclusion check right here.
if (!BlockProperties.isGround(node.getId(), ignoreFlags)) {
return false;
}
}
}
if (nodeAbove == null) {
if (entryAbove == null) {
// Use the current state.
nodeAbove = blockCache.getOrCreateBlockCacheNode(x, y + 1, z, false);
}
else {
nodeAbove = entryAbove.previousState;
}
}
if (node == null) {
if (entry == null) {
// Use the current state.
node= blockCache.getOrCreateBlockCacheNode(x, y , z, false);
}
else {
node = entry.previousState;
}
}
return true;
}
/**
* Advance pair-iteration state.
*
* @return Returns true, if a usable entry or pair of entries has been set.
* If no suitable entry could be found, false is returned.
*/
public boolean advance() {
// TODO: Evaluate which is the most likely most often called part(s) ?
if (entries == null) { // TODO: Which to test for: entries or entry?
if (itEntriesAbove.hasNext()) {
entryAbove = itEntriesAbove.next();
nodeAbove = entryAbove.previousState;
return true;
}
else {
// No more entries to check.
return false;
}
}
else if (entriesAbove == null) {
entry = null;
while (itEntries.hasNext()) {
entry = itEntries.next();
node = entry.previousState;
if (BlockProperties.isGround(node.getId(), ignoreFlags)) {
// TODO: If nodeAbove is ground too, could exclude cases here.
return true;
}
else {
entry = null;
node = null;
}
}
if (entry == null){
// No more entries to check.
return false;
}
else {
return true;
}
}
else {
// Both not null (that case has been excluded above).
return advanceDualEntries();
}
}
private boolean advanceDualEntries() {
// Re-iterate, if necessary.
while (true) {
// Try to iterate aboveEntries first.
entryAbove = null;
nodeAbove = null;
while (itEntriesAbove.hasNext()) {
entryAbove = itEntriesAbove.next();
nodeAbove = entryAbove.previousState;
if (entry.nextEntryTick >= 0 && entryAbove.tick > entry.nextEntryTick) {
entryAbove = null;
nodeAbove = null;
break;
}
else if (entry.overlapsIntervalOfValidity(entryAbove)) {
// Good to use.
return true;
}
else {
entryAbove = null;
nodeAbove = null;
// Check the next one (not out of range yet).
}
}
// (entryAbove == null)
// Rewind entryAbove, advance entry.
entry = null;
node = null;
if (itEntries.hasNext()) {
entry = itEntries.next();
node = entry.previousState;
// TODO: Skip if not ground (!).
// Rewind.
while (itEntriesAbove.nextIndex() > entriesAboveLockIndex) {
entryAbove = itEntriesAbove.previous();
nodeAbove = entryAbove.previousState;
// TODO: Consider optimized break here.
}
// Advance towards next overlap.
if (entryAbove != null) {
while (!entry.overlapsIntervalOfValidity(entryAbove)) {
if (entry.nextEntryTick >= 0 && entryAbove.tick > entry.nextEntryTick) {
// Try next entry.
entryAbove = null;
nodeAbove = null;
break;
}
if (itEntriesAbove.hasNext()) {
entryAbove = itEntriesAbove.next();
nodeAbove = entryAbove.previousState;
entriesAboveLockIndex = itEntriesAbove.nextIndex();
}
else {
// Try next entry.
entryAbove = null;
nodeAbove = null;
// TODO: Consider allow checking entry + null once.
break;
}
}
if (entryAbove != null) {
return true;
}
}
}
else {
/*
* TODO: Cover current state for entry vs. last of entriesAbove
* for the very last thing? (same for current above state vs.
* last of entries).
*/
entry = entryAbove = null;
node = nodeAbove = null;
return false;
}
if (entry == null && entryAbove == null) {
/*
* TODO: Should be dead code, ensure all cases except for
* "Try next entry." are covered above.
*/
return false;
}
} // (while: Find matching pair to continue with)
}
}

View File

@ -897,6 +897,27 @@ public class RichBoundsLocation implements IGetBukkitLocation, IGetBlockPosition
return onGround;
}
/**
* Check on-ground in a very opportunistic way, in terms of
* fcfs+no-consistency+no-actual-side-condition-checks.
* <hr>
* Assume this gets called after the ordinary isOnGround has returned false.
*
* @param loc
* @param yShift
* @param blockChangetracker
* @param blockChangeRef
* @return
*/
public final boolean isOnGroundOpportune(
final double yOnGround, final long ignoreFlags,
final BlockChangeTracker blockChangeTracker, final BlockChangeReference blockChangeRef,
final int tick) {
// TODO: Consider updating onGround+dist cache.
return blockChangeTracker.isOnGround(blockCache, blockChangeRef, tick, world.getUID(),
minX, minY - yOnGround, minZ, maxX, maxY, maxZ, ignoreFlags);
}
/**
* Check if solid blocks hit the box.
*

View File

@ -43,8 +43,6 @@ import fr.neatmonster.nocheatplus.compat.AlmostBoolean;
import fr.neatmonster.nocheatplus.compat.Bridge1_9;
import fr.neatmonster.nocheatplus.compat.MCAccess;
import fr.neatmonster.nocheatplus.compat.blocks.BlockPropertiesSetup;
import fr.neatmonster.nocheatplus.compat.blocks.changetracker.BlockChangeReference;
import fr.neatmonster.nocheatplus.compat.blocks.changetracker.BlockChangeTracker;
import fr.neatmonster.nocheatplus.compat.blocks.init.vanilla.VanillaBlocksFactory;
import fr.neatmonster.nocheatplus.components.registry.event.IHandle;
import fr.neatmonster.nocheatplus.config.ConfPaths;
@ -3839,25 +3837,6 @@ public class BlockProperties {
return AlmostBoolean.MAYBE;
}
/**
* Check on-ground in a very opportunistic way, in terms of
* fcfs+no-consistency+no-actual-side-condition-checks.
* <hr>
* Assume this gets called after the ordinary isOnGround has returned false.
*
* @param loc
* @param yShift
* @param blockChangetracker
* @param blockChangeRef
* @return
*/
public static final boolean isOnGroundInAnOverlyOpportunisticWay(
final PlayerLocation loc, final double yShift, final long ignoreFlags,
final BlockChangeTracker blockChangetracker, final BlockChangeReference blockChangeRef) {
// TODO: Implement in an overly opportunistic way (relay to BlockChangeTracker.isOnGround).
return false;
}
/**
* All dimensions 0 ... 1, no null checks.
*