Rohan Verma



Commit Diff


commit - 614e15ac0f93a9c3b7c8769357e3312aa6851a6d
commit + 46b086be928761b713c634a5db75fc3fda819e22
blob - ed5c08b5915e9fd5f7edc7f2828a57c48ba7fec9
blob + fb01827e3186ba84d898350b60fcf18869380bf9
--- build.gradle
+++ build.gradle
@@ -88,6 +88,7 @@ jar {
     exclude 'me/srrapero720/**'
     exclude 'com/github/alexthe666/**'
     exclude 'com/starfish_studios/**'
+    exclude 'com/leclowndu93150/**'
 }
 
 var generateModMetadata = tasks.register("generateModMetadata", ProcessResources) {
blob - 7788e0742c6b2c30bbf5159324935551c7e82fdb
blob + c53d8b772252b1b4fd2cd0b92e319090d3500aa8
--- gradle.properties
+++ gradle.properties
@@ -25,7 +25,7 @@ citadel_version_range=[2.0.0,3.0.0)
 mod_id=aeronauticscompat
 mod_name=AeronauticsCompat
 mod_license=MIT
-mod_version=1.0.8
+mod_version=1.0.9
 mod_authors=rohan
 mod_description=Compatibility fixes for Create: Aeronautics / Sable sub-levels. Per-mod patches apply only when the target mod is installed.
 mod_group_id=sh.rsap.aeronauticscompat
blob - /dev/null
blob + 41ee2edf44a013b8acfe22ca01ff4e7ee023621f (mode 644)
--- /dev/null
+++ src/main/java/com/leclowndu93150/thick_air/ModRegistry.java
@@ -0,0 +1,13 @@
+package com.leclowndu93150.thick_air;
+
+import com.leclowndu93150.thick_air.capability.AirBubblePositions;
+import net.minecraft.world.level.chunk.ChunkAccess;
+import net.neoforged.neoforge.attachment.AttachmentType;
+
+import java.util.function.Supplier;
+
+public final class ModRegistry {
+    private ModRegistry() {}
+
+    public static final Supplier<AttachmentType<AirBubblePositions>> AIR_BUBBLE_POSITIONS = () -> null;
+}
blob - /dev/null
blob + 44489118ed25f7881e059b73525ad0f275a9a064 (mode 644)
--- /dev/null
+++ src/main/java/com/leclowndu93150/thick_air/api/AirQualityHelper.java
@@ -0,0 +1,17 @@
+package com.leclowndu93150.thick_air.api;
+
+import net.minecraft.world.entity.LivingEntity;
+import net.minecraft.world.level.Level;
+import net.minecraft.world.phys.Vec3;
+
+public final class AirQualityHelper {
+    private AirQualityHelper() {}
+
+    public static AirQualityLevel getAirQualityAtLocation(LivingEntity entity) {
+        return null;
+    }
+
+    public static AirQualityLevel getAirQualityAtLocation(Level level, Vec3 location) {
+        return null;
+    }
+}
blob - /dev/null
blob + 99200d6b8b80f35d9611073bb9501aca9bd18fe5 (mode 644)
--- /dev/null
+++ src/main/java/com/leclowndu93150/thick_air/api/AirQualityLevel.java
@@ -0,0 +1,13 @@
+package com.leclowndu93150.thick_air.api;
+
+public enum AirQualityLevel {
+    GREEN, BLUE, YELLOW, RED;
+
+    public boolean isBetterThan(AirQualityLevel other) {
+        return this.ordinal() < other.ordinal();
+    }
+
+    public double getAirProviderRadius() {
+        return 0.0;
+    }
+}
blob - /dev/null
blob + 5515161976b036f81050373588093a60c308d307 (mode 644)
--- /dev/null
+++ src/main/java/com/leclowndu93150/thick_air/capability/AirBubblePositions.java
@@ -0,0 +1,13 @@
+package com.leclowndu93150.thick_air.capability;
+
+import com.leclowndu93150.thick_air.api.AirQualityLevel;
+import net.minecraft.core.BlockPos;
+
+import java.util.Collections;
+import java.util.Map;
+
+public class AirBubblePositions {
+    public Map<BlockPos, AirQualityLevel> getAirBubblePositionsView() {
+        return Collections.emptyMap();
+    }
+}
blob - 02389948c4dae6e1a03518a1dcb919b2dbd9b323
blob + 430cc322f76a5eb04796c2942241546b38fdc2ac
--- src/main/java/sh/rsap/aeronauticscompat/compat/SableBridge.java
+++ src/main/java/sh/rsap/aeronauticscompat/compat/SableBridge.java
@@ -3,11 +3,14 @@ package sh.rsap.aeronauticscompat.compat;
 import com.mojang.logging.LogUtils;
 import net.minecraft.core.Position;
 import net.minecraft.world.entity.Entity;
+import net.minecraft.world.level.ChunkPos;
 import net.minecraft.world.level.Level;
 import org.slf4j.Logger;
 
 import java.lang.reflect.Field;
 import java.lang.reflect.Method;
+import java.util.Collections;
+import java.util.List;
 
 public final class SableBridge {
 
@@ -21,15 +24,29 @@ public final class SableBridge {
     };
 
     private static final Object INSTANCE;
+
     /** {@code double distanceSquaredWithSubLevels(Level, Position, Position)} */
     private static final Method DIST_SQ_METHOD;
-    /** {@code SubLevel getContaining(Entity)} */
+
+    /** {@code SubLevel getContaining(Entity)} — chunkPos-based lookup; null for entities at world-space coords. */
     private static final Method GET_CONTAINING_ENTITY_METHOD;
 
+    private static final Method GET_TRACKING_OR_VEHICLE_METHOD;
+
+    /** Reflective handle on {@code SubLevel.getPlot()} resolved lazily on first call. */
+    private static volatile Method GET_PLOT_METHOD;
+
+    /** Reflective handle on {@code LevelPlot.getBoundingBox()} resolved lazily on first call. */
+    private static volatile Method GET_PLOT_BOUNDING_BOX_METHOD;
+
+    /** Reflective handles for the bounding-box accessors. */
+    private static volatile Method BB_MIN_X, BB_MIN_Z, BB_MAX_X, BB_MAX_Z;
+
     static {
         Object inst = null;
         Method dist = null;
         Method getContainingEntity = null;
+        Method getTrackingOrVehicle = null;
         String foundFqn = null;
 
         for (String fqn : CANDIDATE_FQNS) {
@@ -41,29 +58,29 @@ public final class SableBridge {
                 dist = cls.getMethod("distanceSquaredWithSubLevels",
                         Level.class, Position.class, Position.class);
                 dist.setAccessible(true);
-                // getContaining(Entity) lives on ActiveSableCompanion (the runtime
-                // subclass of SableCompanion). Best-effort lookup against the
-                // resolved instance class.
                 try {
                     getContainingEntity = inst.getClass().getMethod("getContaining", Entity.class);
                     getContainingEntity.setAccessible(true);
-                } catch (NoSuchMethodException ignored) {
-                    // older Sable without this overload — leave as null
-                }
+                } catch (NoSuchMethodException ignored) {}
+                try {
+                    getTrackingOrVehicle = inst.getClass().getMethod("getTrackingOrVehicleSubLevel", Entity.class);
+                    getTrackingOrVehicle.setAccessible(true);
+                } catch (NoSuchMethodException ignored) {}
                 foundFqn = fqn;
                 break;
-            } catch (Throwable ignored) {
-                // try next FQN
-            }
+            } catch (Throwable ignored) {}
         }
 
         INSTANCE = inst;
         DIST_SQ_METHOD = dist;
         GET_CONTAINING_ENTITY_METHOD = getContainingEntity;
+        GET_TRACKING_OR_VEHICLE_METHOD = getTrackingOrVehicle;
 
         if (INSTANCE != null && DIST_SQ_METHOD != null) {
-            LOGGER.info("[AeronauticsCompat] SableBridge bound to {} (membership-check={})",
-                    foundFqn, GET_CONTAINING_ENTITY_METHOD != null);
+            LOGGER.info("[AeronauticsCompat] SableBridge bound to {} (membership-check={}, tracking={})",
+                    foundFqn,
+                    GET_CONTAINING_ENTITY_METHOD != null,
+                    GET_TRACKING_OR_VEHICLE_METHOD != null);
         } else {
             LOGGER.info("[AeronauticsCompat] SableBridge inactive (Sable not resolved).");
         }
@@ -90,4 +107,73 @@ public final class SableBridge {
             return false;
         }
     }
+
+    public static int[] getTrackingPlotChunkBounds(Entity entity) {
+        if (entity == null || INSTANCE == null) return null;
+        if (GET_TRACKING_OR_VEHICLE_METHOD == null) return null;
+
+        try {
+            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
+            };
+        } catch (Throwable t) {
+            return null;
+        }
+    }
+
+    public static List<ChunkPos> getTrackingPlotChunks(Entity entity) {
+        int[] bounds = getTrackingPlotChunkBounds(entity);
+        if (bounds == null) return Collections.emptyList();
+        java.util.ArrayList<ChunkPos> out = new java.util.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));
+            }
+        }
+        return out;
+    }
 }
blob - 3327d62863fc62ffcd7bff214307980a6fbf2f10
blob + 24c93dfb70c5f956300ffa0580d4fa5e4921a8bf
--- src/main/java/sh/rsap/aeronauticscompat/mixin/AeronauticsCompatMixinPlugin.java
+++ src/main/java/sh/rsap/aeronauticscompat/mixin/AeronauticsCompatMixinPlugin.java
@@ -14,7 +14,7 @@ public class AeronauticsCompatMixinPlugin implements I
     private static final String ETCHED = "gg.moonflower.etched.api.sound.StopListeningSound";
     private static final String WATERFRAMES = "me.srrapero720.waterframes.WaterFrames";
     private static final String CITADEL = "com.github.alexthe666.citadel.server.entity.pathfinding.raycoms.AdvancedPathNavigate";
-    private static final String ANOTHER_FURNITURE = "com.starfish_studios.another_furniture.entity.SeatEntity";
+    private static final String THICK_AIR = "com.leclowndu93150.thick_air.api.AirQualityHelper";
 
     /** Per-mixin marker-class requirements (all must resolve to apply). */
     private static final Map<String, String[]> REQUIREMENTS = Map.of(
@@ -23,7 +23,9 @@ public class AeronauticsCompatMixinPlugin implements I
             "sh.rsap.aeronauticscompat.mixin.waterframes.WaterFramesSableDistanceMixin",
             new String[]{SABLE, WATERFRAMES},
             "sh.rsap.aeronauticscompat.mixin.citadel.CitadelAdvancedPathNavigateMixin",
-            new String[]{SABLE, CITADEL}
+            new String[]{SABLE, CITADEL},
+            "sh.rsap.aeronauticscompat.mixin.thickair.ThickAirAirQualityHelperMixin",
+            new String[]{SABLE, THICK_AIR}
     );
 
     @Override
@@ -33,7 +35,8 @@ public class AeronauticsCompatMixinPlugin implements I
                 + "sable=" + resolves(SABLE, cl)
                 + " etched=" + resolves(ETCHED, cl)
                 + " waterframes=" + resolves(WATERFRAMES, cl)
-                + " citadel=" + resolves(CITADEL, cl));
+                + " citadel=" + resolves(CITADEL, cl)
+                + " thick_air=" + resolves(THICK_AIR, cl));
     }
 
     @Override
blob - /dev/null
blob + 6b64a0c52a0bdb6f32d7209c4c0e06c6356be9b2 (mode 644)
--- /dev/null
+++ src/main/java/sh/rsap/aeronauticscompat/mixin/thickair/ThickAirAirQualityHelperMixin.java
@@ -0,0 +1,102 @@
+package sh.rsap.aeronauticscompat.mixin.thickair;
+
+import com.leclowndu93150.thick_air.ModRegistry;
+import com.leclowndu93150.thick_air.api.AirQualityHelper;
+import com.leclowndu93150.thick_air.api.AirQualityLevel;
+import com.leclowndu93150.thick_air.capability.AirBubblePositions;
+import net.minecraft.core.BlockPos;
+import net.minecraft.world.entity.LivingEntity;
+import net.minecraft.world.level.ChunkPos;
+import net.minecraft.world.level.Level;
+import net.minecraft.world.level.chunk.LevelChunk;
+import net.minecraft.world.phys.Vec3;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.Pseudo;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Inject;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
+import sh.rsap.aeronauticscompat.compat.SableBridge;
+
+import java.util.List;
+import java.util.Map;
+
+@Pseudo
+@Mixin(value = AirQualityHelper.class, remap = false)
+public abstract class ThickAirAirQualityHelperMixin {
+
+    @Inject(
+            method = "getAirQualityAtLocation(Lnet/minecraft/world/entity/LivingEntity;)Lcom/leclowndu93150/thick_air/api/AirQualityLevel;",
+            at = @At("HEAD"),
+            cancellable = true,
+            remap = false
+    )
+    private static void aeronauticscompat$searchContraptionPlotChunks(
+            LivingEntity entity,
+            CallbackInfoReturnable<AirQualityLevel> 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<ChunkPos> 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.
+        AirQualityLevel worldResult = AirQualityHelper.getAirQualityAtLocation(level, eyePos);
+
+        // 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) {
+            LevelChunk chunk = level.getChunkSource().getChunkNow(cp.x, cp.z);
+            if (chunk == null) continue;
+
+            AirBubblePositions caps = chunk.getData(ModRegistry.AIR_BUBBLE_POSITIONS);
+            Map<BlockPos, AirQualityLevel> bubbles = caps.getAirBubblePositionsView();
+            if (bubbles.isEmpty()) continue;
+
+            for (Map.Entry<BlockPos, AirQualityLevel> entry : bubbles.entrySet()) {
+                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; // bridge unavailable
+
+                if (distSq < radiusSq) {
+                    best = quality;
+                    if (best == AirQualityLevel.GREEN) {
+                        cir.setReturnValue(AirQualityLevel.GREEN);
+                        return;
+                    }
+                }
+            }
+        }
+
+        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.)
+    }
+}
blob - fbf6e07de129ff57517907bad472eb2852d548d7
blob + d91b3c5e01393e9b9955c2db55684b7274b08924
--- src/main/resources/aeronauticscompat.mixins.json
+++ src/main/resources/aeronauticscompat.mixins.json
@@ -5,7 +5,8 @@
   "compatibilityLevel": "JAVA_21",
   "minVersion": "0.8",
   "mixins": [
-    "citadel.CitadelAdvancedPathNavigateMixin"
+    "citadel.CitadelAdvancedPathNavigateMixin",
+    "thickair.ThickAirAirQualityHelperMixin"
   ],
   "client": [
     "etched.EtchedStopListeningSoundMixin",