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 @@ -2,6 +2,7 @@

import com.ferreusveritas.dynamictrees.DynamicTrees;
import com.ferreusveritas.dynamictrees.api.TreeHelper;
import com.ferreusveritas.dynamictrees.util.RotCycleGuard;
import com.ferreusveritas.dynamictrees.api.cell.Cell;
import com.ferreusveritas.dynamictrees.api.cell.CellNull;
import com.ferreusveritas.dynamictrees.api.network.MapSignal;
Expand Down Expand Up @@ -159,19 +160,28 @@ public boolean checkForRot(LevelAccessor level, BlockPos pos, Species species, i
}
}

boolean didRot = species.rot(level, pos, neigh & 0x0F, radius, fertility, rand, rapid, fertility > 0); // Unreinforced branches are destroyed.

if (rapid && didRot) {// Speedily postRot back dead branches if this block rotted
for (Direction dir : Direction.values()) {// The logic here is that if this block rotted then
BlockPos neighPos = pos.relative(dir);// the neighbors might be rotted too.
BlockState neighState = level.getBlockState(neighPos);
if (neighState.getBlock() == this) { // Only check blocks logs that are the same as this one
this.checkForRot(level, neighPos, species, fertility, getRadius(neighState), rand, 1.0f, true);
if (RotCycleGuard.cut(this, species, level, pos, radius, fertility, rand, rapid, false)) {
return false;
}
try {
boolean didRot = species.rot(level, pos, neigh & 0x0F, radius, fertility, rand, rapid, fertility > 0); // Unreinforced branches are destroyed.

if (rapid && didRot) {// Speedily postRot back dead branches if this block rotted
for (Direction dir : Direction.values()) {// The logic here is that if this block rotted then
BlockPos neighPos = pos.relative(dir);// the neighbors might be rotted too.
BlockState neighState = level.getBlockState(neighPos);
if (neighState.getBlock() == this) { // Only check blocks logs that are the same as this one
this.checkForRot(level, neighPos, species, fertility, getRadius(neighState), rand, 1.0f, true);
}
}
}
}

return didRot;
return didRot;
} finally {
if (rapid) {
RotCycleGuard.exit(this, level, pos);
}
}
}

///////////////////////////////////////////
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.ferreusveritas.dynamictrees.DynamicTrees;
import com.ferreusveritas.dynamictrees.api.TreeHelper;
import com.ferreusveritas.dynamictrees.util.RotCycleGuard;
import com.ferreusveritas.dynamictrees.api.cell.Cell;
import com.ferreusveritas.dynamictrees.api.cell.CellNull;
import com.ferreusveritas.dynamictrees.api.network.MapSignal;
Expand Down Expand Up @@ -485,19 +486,26 @@ public boolean checkForRot(LevelAccessor level, BlockPos pos, Species species, i
}
}

boolean didRot = species.rot(level, pos, neigh & 0x0F, radius, fertility, rand, true, false); // Unreinforced branches are destroyed.

if (rapid && didRot) {// Speedily postRot back dead branches if this block rotted
for (Direction dir : Direction.values()) {// The logic here is that if this block rotted then
BlockPos neighPos = pos.relative(dir);// the neighbors might be rotted too.
BlockState neighState = level.getBlockState(neighPos);
if (neighState.getBlock() == this) { // Only check blocks logs that are the same as this one
this.checkForRot(level, neighPos, species, fertility, getRadius(neighState), rand, 1.0f, true);
if (RotCycleGuard.cut(this, species, level, pos, radius, fertility, rand, true, true)) {
return false;
}
try {
boolean didRot = species.rot(level, pos, neigh & 0x0F, radius, fertility, rand, true, false); // Unreinforced branches are destroyed.

if (rapid && didRot) {// Speedily postRot back dead branches if this block rotted
for (Direction dir : Direction.values()) {// The logic here is that if this block rotted then
BlockPos neighPos = pos.relative(dir);// the neighbors might be rotted too.
BlockState neighState = level.getBlockState(neighPos);
if (neighState.getBlock() == this) { // Only check blocks logs that are the same as this one
this.checkForRot(level, neighPos, species, fertility, getRadius(neighState), rand, 1.0f, true);
}
}
}
}

return didRot;
return didRot;
} finally {
RotCycleGuard.exit(this, level, pos);
}
}

//////////////////////////////
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.ferreusveritas.dynamictrees.block.branch;

import com.ferreusveritas.dynamictrees.util.RotCycleGuard;

import com.ferreusveritas.dynamictrees.DynamicTrees;
import com.ferreusveritas.dynamictrees.api.FutureBreakable;
import com.ferreusveritas.dynamictrees.api.TreeHelper;
Expand Down Expand Up @@ -730,9 +732,9 @@ public void breakDeliberate(LevelAccessor level, BlockPos pos, DynamicTrees.Dest
FluidState state = Optional.ofNullable(CoordUtils.getStateSafe(level, pos))
.orElse(Blocks.AIR.defaultBlockState()).getFluidState();
if (state.isEmpty())
level.removeBlock(pos, false);
RotCycleGuard.wrote(level.removeBlock(pos, false));
else
level.setBlock(pos, state.createLegacyBlock(), 3);
RotCycleGuard.wrote(level.setBlock(pos, state.createLegacyBlock(), 3));
destroyMode = DynamicTrees.DestroyMode.SLOPPY;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.ferreusveritas.dynamictrees.systems.genfeature;

import com.ferreusveritas.dynamictrees.util.RotCycleGuard;

import com.ferreusveritas.dynamictrees.api.configuration.ConfigurationProperty;
import com.ferreusveritas.dynamictrees.systems.genfeature.context.PostRotContext;
import net.minecraft.core.BlockPos;
Expand Down Expand Up @@ -53,7 +55,7 @@ protected boolean postRot(GenFeatureConfiguration configuration, PostRotContext
return false;
}

level.setBlock(pos, mushroom.defaultBlockState(), 3);
RotCycleGuard.wrote(level.setBlock(pos, mushroom.defaultBlockState(), 3));
return true;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.ferreusveritas.dynamictrees.systems.genfeature;

import com.ferreusveritas.dynamictrees.util.RotCycleGuard;

import com.ferreusveritas.dynamictrees.api.TreeHelper;
import com.ferreusveritas.dynamictrees.api.configuration.ConfigurationProperty;
import com.ferreusveritas.dynamictrees.systems.genfeature.context.PostRotContext;
Expand Down Expand Up @@ -43,7 +45,7 @@ protected boolean postRot(GenFeatureConfiguration configuration, PostRotContext
}

// Change rooty dirt to rotted soil.
level.setBlock(belowPos, configuration.get(ROTTEN_SOIL).defaultBlockState(), Block.UPDATE_ALL);
RotCycleGuard.wrote(level.setBlock(belowPos, configuration.get(ROTTEN_SOIL).defaultBlockState(), Block.UPDATE_ALL));
return true;
}

Expand Down
208 changes: 208 additions & 0 deletions src/main/java/com/ferreusveritas/dynamictrees/util/RotCycleGuard.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
package com.ferreusveritas.dynamictrees.util;

import com.ferreusveritas.dynamictrees.block.branch.BasicBranchBlock;
import com.ferreusveritas.dynamictrees.block.branch.BasicRootsBlock;
import com.ferreusveritas.dynamictrees.block.branch.BranchBlock;
import com.ferreusveritas.dynamictrees.systems.genfeature.GenFeature;
import com.ferreusveritas.dynamictrees.systems.genfeature.GenFeatureConfiguration;
import com.ferreusveritas.dynamictrees.systems.genfeature.MushroomRotGenFeature;
import com.ferreusveritas.dynamictrees.systems.genfeature.RotSoilGenFeature;
import com.ferreusveritas.dynamictrees.systems.genfeature.context.GenerationContext;
import com.ferreusveritas.dynamictrees.systems.genfeature.context.PostRotContext;
import com.ferreusveritas.dynamictrees.tree.species.Species;
import com.mojang.logging.LogUtils;
import java.lang.reflect.Method;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicLong;
import net.minecraft.core.BlockPos;
import net.minecraft.server.level.WorldGenRegion;
import net.minecraft.util.RandomSource;
import net.minecraft.world.level.LevelAccessor;
import org.slf4j.Logger;

/**
* Detects rapid-rot recursion that revisits a position without any successful rot write in between.
* Failed WorldGenRegion writes can leave neighbouring branches intact forever. The guard is limited
* to that exact implementation, whose generation write boundary is fixed during the cascade. Successful writes
* allow revisiting a position, preserving cascades that make progress. Unknown branch/species/feature
* implementations retain their original behavior because their side effects cannot be inferred here.
*/
public final class RotCycleGuard {
private static final AtomicLong CUTS = new AtomicLong();
private static final AtomicLong FOREIGN = new AtomicLong();
private static final Logger LOGGER = LogUtils.getLogger();
private static volatile boolean announced, warned;
private static final ThreadLocal<Cascade> CASCADE = ThreadLocal.withInitial(Cascade::new);

private RotCycleGuard() {
}

static final class Cascade {
long writes; // successful Dynamic Trees rot writes on this thread so far (only ever grows)
int depth;
Object level, block, species, rand;
int fertility;
long[] pos = new long[32];
int[] radius = new int[32];
long[] start = new long[32];
}

public static boolean cut(Object block, Species species, LevelAccessor level, BlockPos pos, int radius, int fertility, RandomSource rand,
boolean rotRapid, boolean roots) {
// Branches pass
// their own rapid flag to Species.rot; roots always pass true and are recorded either way (a root call that is not
// rapid never recurses, so its record is only ever the bottom one and is dropped at its return).
return rotRapid && level.getClass() == WorldGenRegion.class
&& cutOrRecord(block, level, pos, species, fertility, radius, rand, roots);
}

static boolean cutOrRecord(Object block, LevelAccessor level, BlockPos pos, Species species, int fertility, int radius,
RandomSource rand, boolean roots) {
Cascade c = CASCADE.get();
int d = c.depth;
long p = pos.asLong();
if (d > 0 && !(c.level == level && c.block == block && c.species == species && c.fertility == fertility && c.rand == rand
&& adjacent(c.pos[d - 1], p))) {
// Not the recursion of the record on top (a call from elsewhere): start afresh.
// Forgetting records only means fewer cuts, never a wrong one.
d = 0;
c.depth = 0;
}
if (d == 0) {
c.level = level;
c.block = block;
c.species = species;
c.fertility = fertility;
c.rand = rand;
} else {
for (int i = d - 1; i >= 0; i--) {
if (c.pos[i] != p) {
continue;
}
// the newest record for this position has seen the fewest writes since
if (c.radius[i] == radius && c.start[i] == c.writes) {
if ((roots || fertility <= 0) && ownCode(block, species)) {
announceCut(pos);
return true;
}
foreign(pos, block, species);
}
break;
}
}
if (d == c.pos.length) {
c.pos = java.util.Arrays.copyOf(c.pos, d * 2);
c.radius = java.util.Arrays.copyOf(c.radius, d * 2);
c.start = java.util.Arrays.copyOf(c.start, d * 2);
}
c.pos[d] = p;
c.radius[d] = radius;
c.start[d] = c.writes;
c.depth = d + 1;
return false;
}

public static void exit(Object block, LevelAccessor level, BlockPos pos) {
Cascade c = CASCADE.get();
int d = c.depth;
if (d > 0 && c.pos[d - 1] == pos.asLong() && c.level == level && c.block == block) {
c.depth = --d;
if (d == 0) {
c.level = c.block = c.species = c.rand = null;
}
}
}

public static boolean wrote(boolean success) {
if (success) {
CASCADE.get().writes++;
}
return success;
}

private static boolean adjacent(long a, long b) {
int dx = Math.abs(BlockPos.getX(a) - BlockPos.getX(b));
int dy = Math.abs(BlockPos.getY(a) - BlockPos.getY(b));
int dz = Math.abs(BlockPos.getZ(a) - BlockPos.getZ(b));
return dx + dy + dz == 1;
}

private static void announceCut(BlockPos pos) {
long n = CUTS.incrementAndGet();
if (!announced) {
announced = true;
LOGGER.info("Dynamic Trees: stopped an endless rapid-rot loop at {} (a branch the "
+ "generation region does not let Dynamic Trees remove); later stops are logged at DEBUG", pos);
} else if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Dynamic Trees: stopped rapid-rot loop #{} at {}", n, pos);
}
}

private static void foreign(BlockPos pos, Object block, Species species) {
FOREIGN.incrementAndGet();
if (!warned) {
warned = true;
LOGGER.warn("Dynamic Trees: the rapid-rot loop at {} ({} / {}) runs code that does not come "
+ "with Dynamic Trees (or a positive fertility); it is left to the original code", pos,
block.getClass().getName(), species.getClass().getName());
}
}

// ---- Is the code between two calls Dynamic Trees's own? (once per class) ----

private static final ClassValue<Boolean> BLOCK_OK = new ClassValue<>() {
@Override
protected Boolean computeValue(Class<?> c) {
Class<?> check = declarer(c, "checkForRot", LevelAccessor.class, BlockPos.class, Species.class, int.class, int.class,
RandomSource.class, float.class, boolean.class);
return (check == BasicBranchBlock.class || check == BasicRootsBlock.class)
&& declarer(c, "rot", LevelAccessor.class, BlockPos.class) == BranchBlock.class
&& declarer(c, "breakDeliberate", LevelAccessor.class, BlockPos.class,
com.ferreusveritas.dynamictrees.DynamicTrees.DestroyMode.class) == BranchBlock.class;
}
};
private static final ClassValue<Boolean> SPECIES_OK = new ClassValue<>() {
@Override
protected Boolean computeValue(Class<?> c) {
return declarer(c, "rot", LevelAccessor.class, BlockPos.class, int.class, int.class, int.class, RandomSource.class,
boolean.class, boolean.class) == Species.class
&& declarer(c, "postRot", PostRotContext.class) == Species.class
&& declarer(c, "getGenFeatures") == Species.class;
}
};
private static final Set<Class<?>> OWN_POST_ROT = Set.of(GenFeature.class, MushroomRotGenFeature.class, RotSoilGenFeature.class);
private static final ClassValue<Boolean> FEATURE_OK = new ClassValue<>() {
@Override
protected Boolean computeValue(Class<?> c) {
return declarer(c, "generate", GenFeatureConfiguration.class, GenFeature.Type.class, GenerationContext.class) == GenFeature.class
&& OWN_POST_ROT.contains(declarer(c, "postRot", GenFeatureConfiguration.class, PostRotContext.class));
}
};

static boolean ownCode(Object block, Species species) {
if (!BLOCK_OK.get(block.getClass()) || !SPECIES_OK.get(species.getClass())) {
return false;
}
List<GenFeatureConfiguration> features = species.getGenFeatures();
for (int i = 0; i < features.size(); i++) {
GenFeatureConfiguration cfg = features.get(i);
if (cfg.getClass() != GenFeatureConfiguration.class || !FEATURE_OK.get(cfg.getGenFeature().getClass())) {
return false;
}
}
return true;
}

static Class<?> declarer(Class<?> c, String name, Class<?>... params) {
for (Class<?> k = c; k != null; k = k.getSuperclass()) {
try {
Method m = k.getDeclaredMethod(name, params);
return m.getDeclaringClass();
} catch (NoSuchMethodException e) {
// keep looking in the superclass
}
}
return null;
}
}