Place the fluid of a round that finished its structure in the same round

This commit is contained in:
ABKQPO committed 2026-09-11 09:55:22 +08:00
1 parent d7df0c0b5b
commit 7adaa4841d
5 files changed
+95 -9

No files matched your search

@@ -340,6 +340,10 @@ public class AutoPlaceEnvironment {
if (fluidRoundState != null) fluidRoundState.setGateResult(ready);
}
void clearFluidGateResult() {
if (fluidRoundState != null) fluidRoundState.clearGateResult();
}
/**
* Flag that at least one fluid element wanted to place its fluid, but had to wait for the structure to be built
* first. An autoplace round that did nothing but wait reports zero placed elements instead of reporting that the
@@ -407,6 +411,15 @@ public class AutoPlaceEnvironment {
*/
void setGateResult(boolean ready);
/**
* Forget the answer of the fluid placement gate, so that it is asked again.
* <p>
* The answer describes the world as it was before this round built anything, so it has to be dropped once the
* round has placed something and the fluids that had to wait are retried. An implementation that keeps no
* answer at all can leave this alone.
*/
default void clearGateResult() {}
/**
* Remember that a fluid element had to wait for the structure to be built first.
*/
@@ -63,9 +63,11 @@ public class FluidAutoplace {
*/
LENIENT,
/**
* Only place the fluid once every non fluid element of the structure is satisfied. This is the default, and
* together with the fact that an autoplace round places a limited number of elements it effectively builds the
* structure first and fills it with fluid afterwards.
* Only place the fluid once every non fluid element of the structure is satisfied. This is the default.
* <p>
* The fluid is placed as soon as the structure can hold it, which is normally the round that finishes the
* structure: the fluids that round could not place yet are tried once more when it is done building, so they do
* not have to wait for another round.
*/
STRICT
}
@@ -195,9 +197,9 @@ public class FluidAutoplace {
/**
* Whether a fluid may be placed at this position right now.
* <p>
* With {@link GateMode#STRICT} the answer is computed once per autoplace round and cached on the environment, as it
* costs a walk over the whole structure. That also means a fluid is at worst one round late, which is what keeps
* the walk cheap on big multiblocks.
* With {@link GateMode#STRICT} the answer is computed by a walk over the whole structure and cached for the rest of
* the round, as that walk is expensive. The cache is dropped as soon as the round has built something, so that the
* fluids which waited for it are not held back by an answer that predates those placements.
*
* @param world world to place in
* @param x x coord
@@ -302,15 +304,23 @@ public class FluidAutoplace {
private static boolean placeFluidBlock(World world, int x, int y, int z, FluidBlockRequirement requirement) {
FluidStack fluid = requirement.cost();
Block block = requirement.block();
if (requirement.placer() != null) {
return requirement.placer().place(world, x, y, z, requirement.block(), requirement.meta(), fluid);
if (!requirement.placer().place(world, x, y, z, block, requirement.meta(), fluid)) return false;
} else {
// The notification flags matter here. A fluid that is put into the world silently neither flows nor tells
// its neighbours about itself, so it would sit where it was placed until something else happens to touch
// it, which for water inside a multiblock can be never.
if (!world.setBlock(x, y, z, block, requirement.meta(), 3)) return false;
}
return world.setBlock(x, y, z, requirement.block(), requirement.meta(), 2);
// A fluid that is never ticked does not spread, and a fluid placed as a flowing one would never settle.
world.scheduleBlockUpdate(x, y, z, block, block.tickRate(world));
return true;
}
private static void restore(World world, int x, int y, int z, Block block, int meta) {
if (block == null || block.isAir(world, x, y, z)) world.setBlockToAir(x, y, z);
else world.setBlock(x, y, z, block, meta, 2);
else world.setBlock(x, y, z, block, meta, 3);
}
private static void reportMissingFluid(AutoPlaceEnvironment env, FluidBlockRequirement requirement, int missing) {
@@ -318,6 +318,9 @@ public interface IStructureDefinition<T> {
basePositionC,
walker,
"survivalBuild");
// A fluid position is often visited before the blocks that hold it, so give the fluids that had to wait for
// those blocks a chance to be placed in this round rather than the next one.
walker.finishRound(world);
return walker.getBuilt();
}
@@ -1,10 +1,14 @@
package com.gtnewhorizon.structurelib.structure;
import java.util.ArrayList;
import java.util.List;
import javax.annotation.Nullable;
import net.minecraft.item.ItemStack;
import net.minecraft.world.World;
import com.github.bsideup.jabel.Desugar;
import com.gtnewhorizon.structurelib.StructureLibAPI;
import com.gtnewhorizon.structurelib.alignment.enumerable.ExtendedFacing;
import com.gtnewhorizon.structurelib.structure.IStructureElement.PlaceResult;
@@ -20,6 +24,7 @@ class SurvivalBuildStructureWalker<T> implements IStructureWalker<T>, AutoPlaceE
private Boolean fluidGateResult;
private boolean fluidDeferred;
private final List<DeferredFluid<T>> deferredFluids = new ArrayList<>();
private final AutoPlaceEnvironment env;
@@ -52,6 +57,11 @@ class SurvivalBuildStructureWalker<T> implements IStructureWalker<T>, AutoPlaceE
env.offsetABC[2] = c;
env.setSource(params.getSource());
PlaceResult placeResult = element.survivalPlaceBlock(object, world, x, y, z, trigger, env);
if (placeResult == PlaceResult.REJECT_CONTINUE && element.isFluidElement(object)) {
// The fluid has to wait. Remember where it is, as this round may still build what it is waiting for, and
// then it can be placed before the round is over instead of one round later.
deferredFluids.add(new DeferredFluid<>(element, x, y, z, a, b, c));
}
if (placeResult != PlaceResult.SKIP && placeResult != PlaceResult.REJECT_CONTINUE && built == -1) built = 0;
switch (placeResult) {
case SKIP:
@@ -74,6 +84,44 @@ class SurvivalBuildStructureWalker<T> implements IStructureWalker<T>, AutoPlaceE
}
}
/**
* Place the fluids that had to wait during this round, now that the round has built everything it was going to.
* <p>
* A fluid position is often visited before the blocks that are meant to hold it, so a round that finishes the
* structure would otherwise leave the fluid to the next round even though there is nothing left to wait for. The
* gate is asked again, as the answer it gave was about the world as it was before this round built anything.
* <p>
* Like the walk itself, this places at most as many elements as the round still has budget left, and whatever it
* cannot reach is left to the next round.
*
* @param world the world this round was run in
*/
void finishRound(World world) {
if (deferredFluids.isEmpty()) return;
// A round that built nothing has not changed the answer of the gate either.
if (built <= 0) return;
int budget = elementBudget - built;
env.clearFluidGateResult();
for (DeferredFluid<T> deferred : deferredFluids) {
if (budget <= 0) break;
IStructureElement<T> element = deferred.element();
// The element has to see the same offsets and item source it saw during the walk, as elements may read
// channel data or take items through them.
env.offsetABC[0] = deferred.a();
env.offsetABC[1] = deferred.b();
env.offsetABC[2] = deferred.c();
env.setSource(params.getSource());
PlaceResult placeResult = element
.survivalPlaceBlock(object, world, deferred.x(), deferred.y(), deferred.z(), trigger, env);
if (placeResult != PlaceResult.ACCEPT) continue;
if (check) element.check(object, world, deferred.x(), deferred.y(), deferred.z());
built += 1;
budget -= 1;
}
deferredFluids.clear();
}
/**
* Number of elements this round has placed, or -1 when the structure is done.
* <p>
@@ -95,6 +143,11 @@ class SurvivalBuildStructureWalker<T> implements IStructureWalker<T>, AutoPlaceE
fluidGateResult = ready;
}
@Override
public void clearGateResult() {
fluidGateResult = null;
}
@Override
public void markDeferred() {
fluidDeferred = true;
@@ -104,4 +157,10 @@ class SurvivalBuildStructureWalker<T> implements IStructureWalker<T>, AutoPlaceE
public boolean isDeferred() {
return fluidDeferred;
}
/**
* A fluid position a round had to leave alone, kept so that it can be tried once more when the round is over.
*/
@Desugar
private record DeferredFluid<T> (IStructureElement<T> element, int x, int y, int z, int a, int b, int c) {}
}