commit - 614e15ac0f93a9c3b7c8769357e3312aa6851a6d
commit + 46b086be928761b713c634a5db75fc3fda819e22
blob - ed5c08b5915e9fd5f7edc7f2828a57c48ba7fec9
blob + fb01827e3186ba84d898350b60fcf18869380bf9
--- build.gradle
+++ build.gradle
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
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
+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
+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
+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
+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
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 {
};
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) {
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).");
}
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
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(
"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
+ "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
+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
"compatibilityLevel": "JAVA_21",
"minVersion": "0.8",
"mixins": [
- "citadel.CitadelAdvancedPathNavigateMixin"
+ "citadel.CitadelAdvancedPathNavigateMixin",
+ "thickair.ThickAirAirQualityHelperMixin"
],
"client": [
"etched.EtchedStopListeningSoundMixin",