commit 46b086be928761b713c634a5db75fc3fda819e22 from: rohanverma2007 date: Sat Apr 25 21:42:25 2026 UTC add thick air support 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> 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 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 getTrackingPlotChunks(Entity entity) { + int[] bounds = getTrackingPlotChunkBounds(entity); + if (bounds == null) return Collections.emptyList(); + java.util.ArrayList 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 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 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. + 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 bubbles = caps.getAirBubblePositionsView(); + if (bubbles.isEmpty()) continue; + + for (Map.Entry 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",