commit e79e68ab0bad1b80f54a5648509db813f4bd9b8d from: rohanverma2007 date: Sat Apr 25 21:56:00 2026 UTC add off-ship thick air support commit - 46b086be928761b713c634a5db75fc3fda819e22 commit + e79e68ab0bad1b80f54a5648509db813f4bd9b8d blob - 430cc322f76a5eb04796c2942241546b38fdc2ac blob + 7f0e190859d2a5b42b63ab721238ff5e60f05bf1 --- src/main/java/sh/rsap/aeronauticscompat/compat/SableBridge.java +++ src/main/java/sh/rsap/aeronauticscompat/compat/SableBridge.java @@ -5,10 +5,13 @@ import net.minecraft.core.Position; import net.minecraft.world.entity.Entity; import net.minecraft.world.level.ChunkPos; import net.minecraft.world.level.Level; +import net.minecraft.world.phys.Vec3; import org.slf4j.Logger; +import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Method; +import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -42,6 +45,12 @@ public final class SableBridge { /** Reflective handles for the bounding-box accessors. */ private static volatile Method BB_MIN_X, BB_MIN_Z, BB_MAX_X, BB_MAX_Z; + /** Reflective handle on {@code ActiveSableCompanion.getAllIntersecting(Level, BoundingBox3dc)}. */ + private static volatile Method GET_ALL_INTERSECTING_METHOD; + + /** {@code BoundingBox3d(double, double, double, double, double, double)} ctor handle. */ + private static volatile Constructor BB3D_CTOR; + static { Object inst = null; Method dist = null; @@ -116,50 +125,7 @@ public final class SableBridge { Object subLevel = GET_TRACKING_OR_VEHICLE_METHOD.invoke(INSTANCE, entity); if (subLevel == null) return null; - // Lazily resolve SubLevel.getPlot() and LevelPlot.getBoundingBox() - // off the first non-null sub-level we see — works regardless of - // whether Sable's class names change between versions, as long as - // the API methods stay the same. - Method getPlot = GET_PLOT_METHOD; - if (getPlot == null) { - getPlot = subLevel.getClass().getMethod("getPlot"); - getPlot.setAccessible(true); - GET_PLOT_METHOD = getPlot; - } - Object plot = getPlot.invoke(subLevel); - if (plot == null) return null; - - Method getBb = GET_PLOT_BOUNDING_BOX_METHOD; - if (getBb == null) { - getBb = plot.getClass().getMethod("getBoundingBox"); - getBb.setAccessible(true); - GET_PLOT_BOUNDING_BOX_METHOD = getBb; - } - Object bb = getBb.invoke(plot); - if (bb == null) return null; - - // BoundingBox3ic exposes minX/minZ/maxX/maxZ in BLOCK coords. - // Resolve once. - Method minX = BB_MIN_X, minZ = BB_MIN_Z, maxX = BB_MAX_X, maxZ = BB_MAX_Z; - if (minX == null) { - minX = bb.getClass().getMethod("minX"); - minZ = bb.getClass().getMethod("minZ"); - maxX = bb.getClass().getMethod("maxX"); - maxZ = bb.getClass().getMethod("maxZ"); - BB_MIN_X = minX; BB_MIN_Z = minZ; BB_MAX_X = maxX; BB_MAX_Z = maxZ; - } - int blockMinX = (Integer) minX.invoke(bb); - int blockMinZ = (Integer) minZ.invoke(bb); - int blockMaxX = (Integer) maxX.invoke(bb); - int blockMaxZ = (Integer) maxZ.invoke(bb); - - // Block coords -> chunk coords (>> 4 each) - return new int[] { - blockMinX >> 4, - blockMinZ >> 4, - blockMaxX >> 4, - blockMaxZ >> 4 - }; + return resolvePlotChunkBoundsForSubLevel(subLevel); } catch (Throwable t) { return null; } @@ -168,7 +134,7 @@ public final class SableBridge { public static List getTrackingPlotChunks(Entity entity) { int[] bounds = getTrackingPlotChunkBounds(entity); if (bounds == null) return Collections.emptyList(); - java.util.ArrayList out = new java.util.ArrayList<>(); + ArrayList out = new ArrayList<>(); for (int x = bounds[0]; x <= bounds[2]; x++) { for (int z = bounds[1]; z <= bounds[3]; z++) { out.add(new ChunkPos(x, z)); @@ -176,4 +142,103 @@ public final class SableBridge { } return out; } + + public static List getNearbyPlotChunks(Level level, Vec3 center, double searchRadius) { + if (INSTANCE == null) return Collections.emptyList(); + + try { + Method getAll = GET_ALL_INTERSECTING_METHOD; + if (getAll == null) { + // Resolve once. The signature is getAllIntersecting(Level, BoundingBox3dc). + for (Method m : INSTANCE.getClass().getMethods()) { + if ("getAllIntersecting".equals(m.getName()) + && m.getParameterCount() == 2 + && m.getParameterTypes()[0] == Level.class) { + getAll = m; + getAll.setAccessible(true); + GET_ALL_INTERSECTING_METHOD = getAll; + break; + } + } + if (getAll == null) return Collections.emptyList(); + } + + // Build a BoundingBox3d covering [center - r, center + r]. + // The interface BoundingBox3dc has a concrete impl BoundingBox3d + // in the same package; walk the name to find it. + Constructor ctor = BB3D_CTOR; + if (ctor == null) { + Class bbCls = getAll.getParameterTypes()[1]; + String name = bbCls.getName(); + if (name.endsWith("c")) name = name.substring(0, name.length() - 1); + Class concrete = Class.forName(name); + ctor = concrete.getConstructor(double.class, double.class, double.class, + double.class, double.class, double.class); + ctor.setAccessible(true); + BB3D_CTOR = ctor; + } + + Object bb = ctor.newInstance( + center.x - searchRadius, center.y - searchRadius, center.z - searchRadius, + center.x + searchRadius, center.y + searchRadius, center.z + searchRadius + ); + + Object iterable = getAll.invoke(INSTANCE, level, bb); + if (!(iterable instanceof Iterable)) return Collections.emptyList(); + + ArrayList out = new ArrayList<>(); + for (Object subLevel : (Iterable) iterable) { + int[] bounds = resolvePlotChunkBoundsForSubLevel(subLevel); + if (bounds == null) continue; + for (int cx = bounds[0]; cx <= bounds[2]; cx++) { + for (int cz = bounds[1]; cz <= bounds[3]; cz++) { + out.add(new ChunkPos(cx, cz)); + } + } + } + return out; + } catch (Throwable t) { + return Collections.emptyList(); + } + } + + private static int[] resolvePlotChunkBoundsForSubLevel(Object subLevel) throws Exception { + Method getPlot = GET_PLOT_METHOD; + if (getPlot == null) { + getPlot = subLevel.getClass().getMethod("getPlot"); + getPlot.setAccessible(true); + GET_PLOT_METHOD = getPlot; + } + Object plot = getPlot.invoke(subLevel); + if (plot == null) return null; + + Method getBb = GET_PLOT_BOUNDING_BOX_METHOD; + if (getBb == null) { + getBb = plot.getClass().getMethod("getBoundingBox"); + getBb.setAccessible(true); + GET_PLOT_BOUNDING_BOX_METHOD = getBb; + } + Object bb = getBb.invoke(plot); + if (bb == null) return null; + + Method minX = BB_MIN_X, minZ = BB_MIN_Z, maxX = BB_MAX_X, maxZ = BB_MAX_Z; + if (minX == null) { + minX = bb.getClass().getMethod("minX"); + minZ = bb.getClass().getMethod("minZ"); + maxX = bb.getClass().getMethod("maxX"); + maxZ = bb.getClass().getMethod("maxZ"); + BB_MIN_X = minX; BB_MIN_Z = minZ; BB_MAX_X = maxX; BB_MAX_Z = maxZ; + } + int blockMinX = (Integer) minX.invoke(bb); + int blockMinZ = (Integer) minZ.invoke(bb); + int blockMaxX = (Integer) maxX.invoke(bb); + int blockMaxZ = (Integer) maxZ.invoke(bb); + + return new int[] { + blockMinX >> 4, + blockMinZ >> 4, + blockMaxX >> 4, + blockMaxZ >> 4 + }; + } } blob - 6b64a0c52a0bdb6f32d7209c4c0e06c6356be9b2 blob + 5669135e9462f9fb43f5eab087e03b64fbdab5f7 --- src/main/java/sh/rsap/aeronauticscompat/mixin/thickair/ThickAirAirQualityHelperMixin.java +++ src/main/java/sh/rsap/aeronauticscompat/mixin/thickair/ThickAirAirQualityHelperMixin.java @@ -23,6 +23,7 @@ import java.util.Map; @Pseudo @Mixin(value = AirQualityHelper.class, remap = false) public abstract class ThickAirAirQualityHelperMixin { + private static final double SUB_LEVEL_SEARCH_RADIUS = 64.0; @Inject( method = "getAirQualityAtLocation(Lnet/minecraft/world/entity/LivingEntity;)Lcom/leclowndu93150/thick_air/api/AirQualityLevel;", @@ -30,31 +31,30 @@ public abstract class ThickAirAirQualityHelperMixin { cancellable = true, remap = false ) - private static void aeronauticscompat$searchContraptionPlotChunks( + private static void aeronauticscompat$searchNearbyPlotChunks( LivingEntity entity, CallbackInfoReturnable cir) { if (entity.level().isClientSide) return; - // Only intervene when the entity is actually tied to a contraption. - // Off-contraption players see vanilla Thick Air behavior unchanged. - List plotChunks = SableBridge.getTrackingPlotChunks(entity); - if (plotChunks.isEmpty()) return; - Level level = entity.level(); Vec3 eyePos = entity.getEyePosition(); - // Compute the world-space result first by recursing into the - // (Level, Vec3) overload. This handles "is the player's eye block - // itself a fluid / bubble column" plus the world-space chunk scan. + // Find every sub-level near the player (not just the one they're on). + // Empty when no contraptions are within range — common case, fast path. + List nearbyPlotChunks = + SableBridge.getNearbyPlotChunks(level, eyePos, SUB_LEVEL_SEARCH_RADIUS); + + if (nearbyPlotChunks.isEmpty()) return; // no contraptions nearby; vanilla path is correct + + // Compute the world-space (vanilla Thick Air) result first so we can + // merge with sub-level results. AirQualityLevel worldResult = AirQualityHelper.getAirQualityAtLocation(level, eyePos); + if (worldResult == AirQualityLevel.GREEN) return; // can't be improved upon - // GREEN is the best possible — short-circuit, no need to scan plot chunks. - if (worldResult == AirQualityLevel.GREEN) return; - AirQualityLevel best = worldResult; - for (ChunkPos cp : plotChunks) { + for (ChunkPos cp : nearbyPlotChunks) { LevelChunk chunk = level.getChunkSource().getChunkNow(cp.x, cp.z); if (chunk == null) continue; @@ -66,23 +66,15 @@ public abstract class ThickAirAirQualityHelperMixin { BlockPos pos = entry.getKey(); AirQualityLevel quality = entry.getValue(); if (quality == null) continue; - - // Skip if not better than what we already have. if (best != null && !quality.isBetterThan(best)) continue; double radius = quality.getAirProviderRadius(); double radiusSq = radius * radius; - // distanceSquaredWithSubLevels projects both endpoints out of - // their respective sub-levels before measuring — gives the - // correct visual world-space distance even though `pos` is - // in plot-space coords (~20M out) and `eyePos` is in - // world-space. Vec3 bubbleVec = new Vec3(pos.getX() + 0.5, pos.getY() + 0.5, pos.getZ() + 0.5); double distSq = SableBridge.distanceSquaredWithSubLevels(level, eyePos, bubbleVec); + if (Double.isNaN(distSq)) continue; - if (Double.isNaN(distSq)) continue; // bridge unavailable - if (distSq < radiusSq) { best = quality; if (best == AirQualityLevel.GREEN) { @@ -96,7 +88,7 @@ public abstract class ThickAirAirQualityHelperMixin { if (best != null && best != worldResult) { cir.setReturnValue(best); } - // else fall through — original method will run, return worldResult-equivalent. - // (Note: HEAD inject without setReturnValue means the original body executes.) + // Otherwise fall through and let original method run — it will return + // worldResult-equivalent (we computed it above just to compare). } }