@@ -54,17 +54,18 @@ public void tick() {
5454
5555 // For upgrading logic
5656 if (this instanceof ClientBlueprintManager clientBlueprintManager && clientBlueprintManager .isUpgrading ()) {
57- checkIntersectUpgradingBuilding (); // This might set cantBuild = true (via intersectsUpgradingBuilding)
58- if (!intersectsUpgradingBuilding ) {
57+ checkIntersectUpgradingBuilding (); // This sets the ' intersectsUpgradingBuilding' field
58+ if (!intersectsUpgradingBuilding ) { // If checkIntersectUpgradingBuilding resulted in false for an upgrade
5959 cantBuild = true ;
6060 }
61- if (cantBuild ) return ; // Early exit if upgrade intersection fails
61+ if (cantBuild ) return ; // Early exit if upgrade intersection fails or other reasons
6262 } else {
63- intersectsUpgradingBuilding = true ; // Default to true if not upgrading
63+ // If not upgrading, intersectsUpgradingBuilding is generally true (no failure due to intersection).
64+ // This prevents "doesn't intersect" warnings/logic when not in upgrade mode.
65+ intersectsUpgradingBuilding = true ;
6466 }
6567
66-
67- checkPlacementValidity (); // Combined check for leaves, surface collision, and sub-floor validity
68+ checkPlacementValidity (); // Combined check for leaves, surface collision, sub-floor validity, and air under floor
6869 }
6970 protected BlockPos getStructureBuildPos () {
7071 return structureBuildPos ;
@@ -91,96 +92,147 @@ private void checkNotEnoughResources() {
9192 }
9293
9394 private void checkPlacementValidity () {
94- if (cantBuild ) return ; // If already marked as can't build (e.g. due to resources), skip further checks
95+ if (cantBuild ) return ; // If already marked as can't build (e.g., due to resources), skip further checks
96+ if (this .client .world == null || structureBuildPos == null ) { // Essential objects check
97+ cantBuild = true ;
98+ return ;
99+ }
95100
96101 final IStructureBlockData blockData = getBlockData ();
97- final Set <BlockPos > blueprintDataPositions = blockData .getLayer (BlueprintDataLayer .GENERAL )
102+ final BlueprintMetadata selectedStructure = getSelectedStructure ();
103+ if (selectedStructure == null ) { // Should be caught by isSelecting or resource check, but for safety
104+ cantBuild = true ;
105+ return ;
106+ }
107+
108+ // Get all non-air block positions from the blueprint in its local coordinate system
109+ final Set <BlockPos > blueprintNonAirLocalPositions = blockData .getLayer (BlueprintDataLayer .GENERAL )
98110 .entrySet ()
99111 .stream ()
100- .filter (entry -> !entry .getValue ().isAir ()) // Only consider non-air blocks from the blueprint
112+ .filter (entry -> !entry .getValue ().isAir ())
101113 .map (Map .Entry ::getKey )
102114 .collect (Collectors .toSet ());
103115
104- final int floorLevel = getSelectedStructure () .getFloorLevel ();
105- final Vec3i size = blockData .getSize (); // For leaves check
116+ final int floorLevel = selectedStructure .getFloorLevel ();
117+ final Vec3i blueprintSize = blockData .getSize ();
106118
107119 // 1. Check for leaves directly under the blueprint's entire footprint
108- final int groundYInWorld = structureBuildPos .getY () - floorLevel - 1 ;
109- for (int dx = 0 ; dx < size .getX (); dx ++) {
110- for (int dz = 0 ; dz < size .getZ (); dz ++) {
111- BlockPos currentGroundPos = new BlockPos (
120+ // The footprint ground level is 1 Y-level below the blueprint's origin (Y=0).
121+ // World Y for blueprint's Y=0 is structureBuildPos.getY() - floorLevel.
122+ // So, ground check Y is structureBuildPos.getY() - floorLevel - 1.
123+ final int groundYInWorldForLeavesCheck = structureBuildPos .getY () - floorLevel - 1 ;
124+ // Iterate over the footprint of the blueprint in world coordinates
125+ for (int dx = 0 ; dx < blueprintSize .getX (); dx ++) {
126+ for (int dz = 0 ; dz < blueprintSize .getZ (); dz ++) {
127+ // structureBuildPos.getX/Z() is minX/Z of structure in world.
128+ BlockPos currentPosUnderFootprint = new BlockPos (
112129 structureBuildPos .getX () + dx ,
113- groundYInWorld ,
130+ groundYInWorldForLeavesCheck ,
114131 structureBuildPos .getZ () + dz
115132 );
116- if (this .client .world != null ) {
117- BlockState stateBeneath = this .client .world .getBlockState (currentGroundPos );
118- if (stateBeneath .isIn (BlockTags .LEAVES )) {
119- cantBuild = true ;
120- return ;
121- }
133+ // client.world.getBlockState handles out-of-bounds Y by returning air/void_air, which are not leaves.
134+ BlockState stateBeneath = this .client .world .getBlockState (currentPosUnderFootprint );
135+ if (stateBeneath .isIn (BlockTags .LEAVES )) {
136+ cantBuild = true ;
137+ return ;
122138 }
123139 }
124140 }
125141
126- // 2. Check blocks AT OR ABOVE the blueprint's floor level
142+ // 2. NEW: Check for air directly under the blueprint's "floor" blocks.
143+ // "Floor" blocks are non-air blueprint blocks at localY == floorLevel.
144+ // Their world position is structureBuildPos.add(localX, 0, localZ)
145+ // because structureBuildPos.getY() is already the world Y-level of the floor.
146+ final boolean airUnderneathFloorBlocks = blueprintNonAirLocalPositions .stream ()
147+ .filter (bpLocalPos -> bpLocalPos .getY () == floorLevel ) // Filter for blueprint's floor surface blocks
148+ .map (bpFloorLocalPos -> {
149+ // Calculate world position of this blueprint floor block
150+ BlockPos worldFloorBlockPos = structureBuildPos .add (
151+ bpFloorLocalPos .getX (),
152+ 0 , // Y-offset is 0 because structureBuildPos.getY() is the world Y for floorLevel
153+ bpFloorLocalPos .getZ ()
154+ );
155+ return worldFloorBlockPos .down (); // Get the position directly under it in the world
156+ })
157+ .anyMatch (posUnderFloor -> {
158+ // client.world.getBlockState handles Y bounds (returns AIR or VOID_AIR, both .isAir())
159+ return BuildingHelper .canPlaceBlock (this .client .world , posUnderFloor );
160+ });
161+
162+ if (airUnderneathFloorBlocks ) {
163+ cantBuild = true ;
164+ return ;
165+ }
166+
167+ // 3. Check blocks AT OR ABOVE the blueprint's floor level for collisions
127168 final boolean isUpgrading = (this instanceof ClientBlueprintManager cbm ) && cbm .isUpgrading ();
128169 final BlockBox upgradingBox = isUpgrading ? ((ClientBlueprintManager ) this ).getUpgradingBuildingBox () : null ;
129170
130- final boolean partAtOrAboveFloorCollides = blueprintDataPositions .stream ()
131- .filter (bpLocalPos -> bpLocalPos .getY () >= floorLevel )
132- .map (bpLocalPos -> structureBuildPos .add (bpLocalPos .getX (), bpLocalPos .getY () - floorLevel , bpLocalPos .getZ ()))
171+ final boolean partAtOrAboveFloorCollides = blueprintNonAirLocalPositions .stream ()
172+ .filter (bpLocalPos -> bpLocalPos .getY () >= floorLevel ) // Consider blueprint blocks at or above its floor
173+ .map (bpLocalPos -> structureBuildPos .add (bpLocalPos .getX (), bpLocalPos .getY () - floorLevel , bpLocalPos .getZ ())) // Map to world pos
133174 .anyMatch (worldPos -> {
134- boolean currentCanBuild = BuildingHelper .canPlaceBlock (client .world , worldPos );
135- if (isUpgrading && upgradingBox != null ) {
136- currentCanBuild = currentCanBuild || upgradingBox .contains (worldPos );
175+ boolean currentCanPlace = BuildingHelper .canPlaceBlock (client .world , worldPos );
176+ if (isUpgrading && upgradingBox != null && upgradingBox .contains (worldPos )) {
177+ // If upgrading, and this block is part of the existing structure, it's "placeable" in context.
178+ currentCanPlace = true ;
137179 }
138- return !currentCanBuild ; // if !canPlace (and not part of upgrade), then it collides
180+ return !currentCanPlace ; // Collision if cannot place (and not an allowed overlap during upgrade)
139181 });
140182
141183 if (partAtOrAboveFloorCollides ) {
142184 cantBuild = true ;
143185 return ;
144186 }
145187
146- // 3. Check blocks BELOW the blueprint's floor level
147- final boolean partBelowFloorInvalid = blueprintDataPositions .stream ()
148- .filter (bpLocalPos -> bpLocalPos .getY () < floorLevel )
149- .map (bpLocalPos -> structureBuildPos .add (bpLocalPos .getX (), bpLocalPos .getY () - floorLevel , bpLocalPos .getZ ()))
150- .anyMatch (worldPos -> BuildingHelper .canPlaceBlock (client .world , worldPos ));
188+ // 4. Check blocks BELOW the blueprint's floor level (foundation/sub-structure parts)
189+ // Original logic: These parts must NOT be placeable into air/replaceable blocks,
190+ // i.e., they must be placed into solid, non-replaceable ground.
191+ final boolean partBelowFloorInvalid = blueprintNonAirLocalPositions .stream ()
192+ .filter (bpLocalPos -> bpLocalPos .getY () < floorLevel ) // Consider blueprint blocks below its floor
193+ .map (bpLocalPos -> structureBuildPos .add (bpLocalPos .getX (), bpLocalPos .getY () - floorLevel , bpLocalPos .getZ ())) // Map to world pos
194+ .anyMatch (worldPos -> BuildingHelper .canPlaceBlock (client .world , worldPos )); // Invalid if it *can* be placed (i.e., targets air/replaceable)
151195
152196 if (partBelowFloorInvalid ) {
153197 cantBuild = true ;
154- // No return here, as we've already checked other conditions that might set cantBuild
198+ // Original code does not return here, allowing cantBuild to be set and fall through.
199+ // This is fine as canBuild() will reflect the final state of cantBuild.
155200 }
156201 }
157202
158203
159- private void checkIntersectUpgradingBuilding () { // This method is only relevant if isUpgrading is true
204+ private void checkIntersectUpgradingBuilding () {
160205 if (!(this instanceof ClientBlueprintManager cbm ) || !cbm .isUpgrading ()) {
161- intersectsUpgradingBuilding = true ; // Not upgrading, so no intersection issue by default
206+ intersectsUpgradingBuilding = true ;
162207 return ;
163208 }
164209
165- if (!enoughResources ) { // If not enough resources, intersection check is moot for buildability
166- intersectsUpgradingBuilding = false ;
210+ if (!enoughResources ) {
211+ intersectsUpgradingBuilding = false ; // As per original logic, if not enough resources, it's marked as not intersecting for UI/logic consistency.
167212 return ;
168213 }
169214
170215 final IStructureBlockData blockData = getBlockData ();
216+ final BlueprintMetadata selectedStructure = getSelectedStructure ();
217+ if (selectedStructure == null ) { // Safety check
218+ intersectsUpgradingBuilding = false ;
219+ return ;
220+ }
171221 final Set <BlockPos > blueprintDataPositions = blockData .getLayer (BlueprintDataLayer .GENERAL )
172222 .entrySet ()
173223 .stream ()
174224 .filter (entry -> !entry .getValue ().isAir ())
175225 .map (Map .Entry ::getKey )
176226 .collect (Collectors .toSet ());
177- final int floorLevel = getSelectedStructure () .getFloorLevel ();
227+ final int floorLevel = selectedStructure .getFloorLevel ();
178228 final BlockBox upgradingBuildingBox = cbm .getUpgradingBuildingBox ();
229+ if (upgradingBuildingBox == null ) { // Should ideally not happen if cbm.isUpgrading() is true and a valid building is targeted
230+ intersectsUpgradingBuilding = false ;
231+ return ;
232+ }
179233
180- // An upgrade is valid if at least one block of the new blueprint (at or above its floor)
181- // intersects with the existing building's box.
182234 intersectsUpgradingBuilding = blueprintDataPositions .stream ()
183- .filter (bpLocalPos -> bpLocalPos .getY () >= floorLevel ) // Consider blocks at or above the blueprint's designated floor for intersection
235+ .filter (bpLocalPos -> bpLocalPos .getY () >= floorLevel )
184236 .map (bpLocalPos -> structureBuildPos .add (bpLocalPos .getX (), bpLocalPos .getY () - floorLevel , bpLocalPos .getZ ()))
185237 .anyMatch (upgradingBuildingBox ::contains );
186238 }
@@ -198,18 +250,17 @@ private IStructureBlockData getBlockData() {
198250 private BlockPos getSelectedPos () {
199251 if (client .crosshairTarget instanceof final BlockHitResult crosshairTarget && this instanceof ClientBlueprintManager cbm ) {
200252 final BlockPos originalPos = crosshairTarget .getBlockPos ();
201- if (client .world == null || originalPos == null ) return null ; // Ensure world is not null
253+ if (client .world == null || originalPos == null ) return null ;
202254
203255 final BlockState blockState = client .world .getBlockState (originalPos );
204- if (blockState .isAir () && !cbm .isUpgrading ()) return null ; // Can't start placement on air unless upgrading
256+ if (blockState .isAir () && !cbm .isUpgrading ()) return null ;
205257
206258 var movedPos = originalPos ;
207- final BlockBox upgradingBuildingBox = cbm .getUpgradingBuildingBox (); // Can be null if not upgrading
259+ final BlockBox upgradingBuildingBox = cbm .isUpgrading () ? cbm . getUpgradingBuildingBox () : null ;
208260 final boolean intersectingWithTheBuilding = cbm .isUpgrading () && upgradingBuildingBox != null &&
209261 (upgradingBuildingBox .contains (originalPos ) || blockState .isOf (FortressBlocks .FORTRESS_BUILDING ));
210262
211263 if (intersectingWithTheBuilding ) {
212- // Align Y with the bottom of the existing building box if upgrading
213264 movedPos = new BlockPos (movedPos .getX (), upgradingBuildingBox .getMinY (), movedPos .getZ ());
214265 }
215266
@@ -240,6 +291,8 @@ public final Optional<BlockPos> getStructureRenderPos() {
240291 final var selected = getSelectedStructure ();
241292 if (selected == null ) return Optional .empty ();
242293 final var floorLevel = selected .getFloorLevel ();
294+ // structureBuildPos.getY() is world Y for blueprint's floorLevel.
295+ // structureBuildPos.down(floorLevel) results in world Y for blueprint's Y=0.
243296 return Optional .ofNullable (structureBuildPos ).map (it -> it .down (floorLevel ));
244297 }
245298
0 commit comments