commit ea7b69c40058e45f428ebffe17534c7caca55fb3 from: rohanverma2007 date: Wed Apr 22 15:45:29 2026 UTC fix alex mobs/caves commit - d0df860db553d49ad9577921e485218cc97f2c8f commit + ea7b69c40058e45f428ebffe17534c7caca55fb3 blob - 70c6d5dbf3fc487305f4751c46fe6fc31b3bb542 blob + a4e3057ff19c17f5bd13c8875e0cfd7edc7870d1 --- README.md +++ README.md @@ -1,7 +1,9 @@ -# AeronauticsCompat 1.0.0 +# AeronauticsCompat 1.0.4 A compatibility-patch mod for [Create: Aeronautics](https://github.com/Creators-of-Aeronautics/Simulated-Project) / [Sable](https://github.com/ryanhcode/sable) sub-levels (NeoForge 1.21.1). +Tested against Sable 1.0.6 and 1.1.1, Create: Aeronautics 1.0.3 and 1.1.3. All patches target stable behavior that has not changed between these versions. + All patches are **soft dependencies** — each one applies only when its target mod is installed. Uninstalling any of the target mods is safe; AeronauticsCompat stays loaded and continues to patch whatever else is present. ## Patches @@ -24,6 +26,15 @@ All patches are **soft dependencies** — each one app *Activates when: WaterFrames + Sable are both installed.* +### Citadel × Sable: mob pathfinding no longer crashes the server +**Problem.** Server OOMs (`java.lang.OutOfMemoryError: Java heap space`) whenever a Citadel-powered mob ticks near a Sable contraption. Affects everything that depends on Citadel — Alex's Mobs, Alex's Caves, L\_Ender's Cataclysm, etc. Vanilla mobs and other mod mobs are unaffected. + +**Cause.** Sable's `SubLevelInclusiveLevelEntityGetter` wraps `Level#getEntities`, causing entity queries to also enumerate entities inside sub-levels. Those entities carry raw coordinates in sub-level space — tens of millions of blocks from the querying mob's position. When a Citadel mob picks such an entity as a target (or draws a stroll pos near one), its goal calls `AdvancedPathNavigate#moveToXYZ` with those coordinates. Citadel's `AbstractPathJob` sizes a `ChunkCache`'s `LevelChunk[][]` by the start-to-end bounding box; with end coords in the millions, the array is ~30M×30M and JVM heap is exhausted instantly. + +**Fix.** Mixin `AdvancedPathNavigate.moveToXYZ` at HEAD. If the target is more than 1024 blocks from the mob's actual position, return `null` — this looks to the goal exactly like "no path found, try again next tick." No path job is constructed, no `ChunkCache` allocated, no OOM. Legitimate pathing (FOLLOW\_RANGE is usually 16–48, at most ~128) is completely unaffected. + +*Activates when: Citadel + Sable are both installed.* + ## Optional: YouTube source for Etched Also includes the legacy Etchtube YouTube-via-yt-dlp source, gated on Etched being installed. Configure the proxy URL and bearer token under `YoutubeProxy.URL` / `YoutubeProxy.Token` in `config/aeronauticscompat-client.toml`. Leave empty to disable. Not required for the patches above. @@ -31,7 +42,7 @@ Also includes the legacy Etchtube YouTube-via-yt-dlp s ## Dependencies - **Required**: NeoForge 1.21.1 -- **Optional** (each enables one or more patches): Sable, Etched, WaterFrames +- **Optional** (each enables one or more patches): Sable, Etched, WaterFrames, Citadel No target mod is a hard dependency. The mod loads and logs which patches are active on startup. blob - 7b78ca1799a817aa89236cd6351aa7925aeec0a0 blob + 1717c77b0f7afac91b30ba8e469ae5b9416c0a7b --- build.gradle +++ build.gradle @@ -86,6 +86,7 @@ dependencies { jar { exclude 'dev/ryanhcode/**' exclude 'me/srrapero720/**' + exclude 'com/github/alexthe666/**' } var generateModMetadata = tasks.register("generateModMetadata", ProcessResources) { @@ -98,6 +99,7 @@ var generateModMetadata = tasks.register("generateModM etched_version_range : etched_version_range, sable_version_range : sable_version_range, waterframes_version_range: waterframes_version_range, + citadel_version_range : citadel_version_range, mod_id : mod_id, mod_name : mod_name, mod_license : mod_license, blob - a58fe6a8db4b34d2862657f257dc460b65a21bcc blob + 953a6cb36f0415b1e86e19c58db146769eb13cc0 --- gradle.properties +++ gradle.properties @@ -19,12 +19,13 @@ loader_version_range=[1,) etched_version_range=[5.0.0,6.0.0) sable_version_range=[1.0.0,) waterframes_version_range=[2.1.0,3.0.0) +citadel_version_range=[2.0.0,3.0.0) # Mod Properties mod_id=aeronauticscompat mod_name=AeronauticsCompat mod_license=GPLv3 -mod_version=1.0.0 +mod_version=1.0.4 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 + 8f5ecc2c4322359fbb7f1749fbc698b91d3e7bba (mode 644) --- /dev/null +++ src/main/java/com/github/alexthe666/citadel/server/entity/pathfinding/raycoms/AbstractAdvancedPathNavigate.java @@ -0,0 +1,22 @@ +package com.github.alexthe666.citadel.server.entity.pathfinding.raycoms; + +import net.minecraft.world.entity.Mob; + +/** + * COMPILE-TIME STUB of + * {@code com.github.alexthe666.citadel.server.entity.pathfinding.raycoms.AbstractAdvancedPathNavigate}. + * + *

The real class at runtime is provided by Citadel and declares + * {@code protected final Mob ourEntity} and {@code public Mob getOurEntity()}. + * We only need {@code getOurEntity()} for the Citadel mixin to compile; + * the field itself is not referenced from our code. + * + *

Excluded from the output jar via {@code jar { exclude 'com/github/**' }} + * in build.gradle so it can never shadow the real class at runtime. + */ +public abstract class AbstractAdvancedPathNavigate { + + public Mob getOurEntity() { + return null; + } +} blob - /dev/null blob + 42dcdfb8fc946f8a4bea02b6ba9115b95a22103c (mode 644) --- /dev/null +++ src/main/java/com/github/alexthe666/citadel/server/entity/pathfinding/raycoms/AdvancedPathNavigate.java @@ -0,0 +1,22 @@ +package com.github.alexthe666.citadel.server.entity.pathfinding.raycoms; + +/** + * COMPILE-TIME STUB of + * {@code com.github.alexthe666.citadel.server.entity.pathfinding.raycoms.AdvancedPathNavigate}. + * + *

The real class at runtime extends {@code AbstractAdvancedPathNavigate} + * (which holds the {@code ourEntity} field and the {@code getOurEntity()} + * accessor we use). The mixin doesn't need to reference any fields directly + * — it casts {@code this} to the parent at runtime — so this stub only needs + * the inheritance link to be present so {@code (AbstractAdvancedPathNavigate) + * (Object) this} compiles cleanly. + * + *

Excluded from the output jar via {@code jar { exclude 'com/github/**' }} + * in build.gradle so it can never shadow the real class at runtime. + */ +public abstract class AdvancedPathNavigate extends AbstractAdvancedPathNavigate { + + protected PathResult moveToXYZ(final double x, final double y, final double z, final double speedFactor) { + return null; + } +} blob - /dev/null blob + 42992ad16c531461a662b90b737a84de2085c9b8 (mode 644) --- /dev/null +++ src/main/java/com/github/alexthe666/citadel/server/entity/pathfinding/raycoms/PathResult.java @@ -0,0 +1,13 @@ +package com.github.alexthe666.citadel.server.entity.pathfinding.raycoms; + +/** + * COMPILE-TIME STUB of + * {@code com.github.alexthe666.citadel.server.entity.pathfinding.raycoms.PathResult}. + * + *

The real class at runtime is provided by Citadel. This stub only needs + * to exist to satisfy the compiler for AeronauticsCompat's mixin; no fields + * or methods from it are used by the mixin itself. Excluded from the output + * jar via the {@code jar { exclude 'com/github/**' }} rule in build.gradle. + */ +public class PathResult { +} blob - b0c9c249d56ddbe451aaa3c0bdb02ed2905b28df blob + 0767b5fca0ee1a1e17b034b21949dd8212888137 --- src/main/java/sh/rsap/aeronauticscompat/compat/SableBridge.java +++ src/main/java/sh/rsap/aeronauticscompat/compat/SableBridge.java @@ -2,6 +2,7 @@ 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.Level; import org.slf4j.Logger; @@ -27,10 +28,13 @@ 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)} */ + private static final Method GET_CONTAINING_ENTITY_METHOD; static { Object inst = null; Method dist = null; + Method getContainingEntity = null; String foundFqn = null; for (String fqn : CANDIDATE_FQNS) { @@ -42,6 +46,15 @@ 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 + } foundFqn = fqn; break; } catch (Throwable ignored) { @@ -51,9 +64,11 @@ public final class SableBridge { INSTANCE = inst; DIST_SQ_METHOD = dist; + GET_CONTAINING_ENTITY_METHOD = getContainingEntity; if (INSTANCE != null && DIST_SQ_METHOD != null) { - LOGGER.info("[AeronauticsCompat] SableBridge bound to {}", foundFqn); + LOGGER.info("[AeronauticsCompat] SableBridge bound to {} (membership-check={})", + foundFqn, GET_CONTAINING_ENTITY_METHOD != null); } else { LOGGER.info("[AeronauticsCompat] SableBridge inactive (Sable not resolved)."); } @@ -76,4 +91,25 @@ public final class SableBridge { return Double.NaN; } } + + /** + * Whether the given entity is currently inside a Sable sub-level (i.e. + * standing on a contraption / ship). Used by the Citadel pathfinding + * guard to avoid rejecting legitimate paths between two entities that + * are both on the same contraption — those endpoints have huge + * world-space coords by design and would otherwise look identical to + * the sub-level entity bleed-through that the guard exists to catch. + * + *

Returns {@code false} if Sable isn't loaded, the membership lookup + * isn't available, or the call throws — all of which are fail-safe: + * the caller will then fall back to the conservative distance check. + */ + public static boolean isInSubLevel(Entity entity) { + if (entity == null || GET_CONTAINING_ENTITY_METHOD == null) return false; + try { + return GET_CONTAINING_ENTITY_METHOD.invoke(INSTANCE, entity) != null; + } catch (Throwable t) { + return false; + } + } } blob - 12c587e977718538fe9861761f888e5073ea36dc blob + cb534c34f5b64b8a83873d3d7afb2cc161819a6f --- src/main/java/sh/rsap/aeronauticscompat/mixin/AeronauticsCompatMixinPlugin.java +++ src/main/java/sh/rsap/aeronauticscompat/mixin/AeronauticsCompatMixinPlugin.java @@ -20,13 +20,16 @@ public class AeronauticsCompatMixinPlugin implements I private static final String SABLE = "dev.ryanhcode.sable.sound.SoundInstanceDelegated"; 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"; /** Per-mixin marker-class requirements (all must resolve to apply). */ private static final Map REQUIREMENTS = Map.of( "sh.rsap.aeronauticscompat.mixin.etched.EtchedStopListeningSoundMixin", new String[]{SABLE, ETCHED}, "sh.rsap.aeronauticscompat.mixin.waterframes.WaterFramesSableDistanceMixin", - new String[]{SABLE, WATERFRAMES} + new String[]{SABLE, WATERFRAMES}, + "sh.rsap.aeronauticscompat.mixin.citadel.CitadelAdvancedPathNavigateMixin", + new String[]{SABLE, CITADEL} ); @Override @@ -35,7 +38,8 @@ public class AeronauticsCompatMixinPlugin implements I System.out.println("[AeronauticsCompat] Mod detection: " + "sable=" + resolves(SABLE, cl) + " etched=" + resolves(ETCHED, cl) - + " waterframes=" + resolves(WATERFRAMES, cl)); + + " waterframes=" + resolves(WATERFRAMES, cl) + + " citadel=" + resolves(CITADEL, cl)); } @Override blob - /dev/null blob + 64849a8cafc5f34c7729c941d926fcd8b7d5b4b0 (mode 644) --- /dev/null +++ src/main/java/sh/rsap/aeronauticscompat/mixin/citadel/CitadelAdvancedPathNavigateMixin.java @@ -0,0 +1,110 @@ +package sh.rsap.aeronauticscompat.mixin.citadel; + +import com.github.alexthe666.citadel.server.entity.pathfinding.raycoms.AbstractAdvancedPathNavigate; +import com.github.alexthe666.citadel.server.entity.pathfinding.raycoms.AdvancedPathNavigate; +import com.github.alexthe666.citadel.server.entity.pathfinding.raycoms.PathResult; +import net.minecraft.world.entity.Mob; +import net.minecraft.world.phys.Vec3; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Pseudo; +import org.spongepowered.asm.mixin.Unique; +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; + +/** + * Citadel-from-contraption OOM guard. + * + *

When Sable is installed, + * {@code dev.ryanhcode.sable.util.SubLevelInclusiveLevelEntityGetter} causes + * {@code Level#getEntities(AABB, ...)} to also scan entities living in Sable + * sub-levels. Those entities' raw coordinates are in sub-level space — often + * tens of millions of blocks away from the querying mob's own world position. + * + *

When a Citadel-powered mob (anything from Alex's Mobs, Alex's Caves, + * Cataclysm, etc.) sees such an entity and targets it — or picks a random + * stroll pos near it — its goal code calls + * {@link AdvancedPathNavigate#moveToXYZ} with sub-level coordinates. Citadel's + * {@code AbstractPathJob} then sizes a {@code ChunkCache}'s + * {@code LevelChunk[][]} array by the start-to-end bounding box. With end + * coords in the tens of millions, the array allocation exhausts the JVM heap + * and crashes the server. + * + *

The membership-aware check. A naive distance-only reject breaks + * legitimate AI on contraptions: two mobs both standing on the same Sable + * ship are both at huge world coordinates by design, so the distance between + * them looks identical to a sub-level entity bleeding through into a + * world-space mob's awareness range. We disambiguate by asking Sable whether + * the path-issuing mob itself is currently inside a sub-level + * ({@link SableBridge#isInSubLevel}): + * + *

+ */ +@Pseudo +@Mixin(value = AdvancedPathNavigate.class, remap = false) +public abstract class CitadelAdvancedPathNavigateMixin { + + /** + * Max square-distance (blocks²) between a (world-space) mob and a + * prospective path target before AeronauticsCompat rejects the call. + * 1024 blocks is well beyond any reasonable FOLLOW_RANGE; legitimate + * world-space pathing is unaffected. Mobs on contraptions skip this + * check entirely. + */ + @Unique + private static final double AERONAUTICSCOMPAT$MAX_REASONABLE_DISTANCE_SQ = 1024.0 * 1024.0; + + @Inject( + method = "moveToXYZ(DDDD)Lcom/github/alexthe666/citadel/server/entity/pathfinding/raycoms/PathResult;", + at = @At("HEAD"), + cancellable = true, + remap = false + ) + private void aeronauticscompat$rejectSubLevelTargets( + final double x, + final double y, + final double z, + final double speedFactor, + final CallbackInfoReturnable cir + ) { + // AdvancedPathNavigate extends AbstractAdvancedPathNavigate at runtime, + // so the cast is safe. We don't have a static type relationship here + // (the mixin class doesn't extend the target), hence the (Object) bounce. + final AbstractAdvancedPathNavigate self = (AbstractAdvancedPathNavigate) (Object) this; + final Mob m = self.getOurEntity(); + if (m == null) { + return; + } + + // If the mob is on a contraption, give the AI total freedom — the + // destination is almost certainly on the same ship and will look + // far away in world space by design. + if (SableBridge.isInSubLevel(m)) { + return; + } + + final Vec3 pos = m.position(); + final double dx = x - pos.x; + final double dy = y - pos.y; + final double dz = z - pos.z; + final double distSq = dx * dx + dy * dy + dz * dz; + + if (Double.isNaN(distSq) || Double.isInfinite(distSq) + || distSq > AERONAUTICSCOMPAT$MAX_REASONABLE_DISTANCE_SQ) { + // Target is almost certainly a sub-level entity bleeding through + // Sable's SubLevelInclusiveLevelEntityGetter, or a random stroll + // pos drawn from such an entity's vicinity. Refuse the path — + // next tick the goal will pick something else. + cir.setReturnValue(null); + } + } +} blob - 99d5c3e821739f7533be36fd847ab5e6ab4fc242 blob + fbf6e07de129ff57517907bad472eb2852d548d7 --- src/main/resources/aeronauticscompat.mixins.json +++ src/main/resources/aeronauticscompat.mixins.json @@ -4,6 +4,9 @@ "plugin": "sh.rsap.aeronauticscompat.mixin.AeronauticsCompatMixinPlugin", "compatibilityLevel": "JAVA_21", "minVersion": "0.8", + "mixins": [ + "citadel.CitadelAdvancedPathNavigateMixin" + ], "client": [ "etched.EtchedStopListeningSoundMixin", "waterframes.WaterFramesSableDistanceMixin" blob - de8d417aece25c66fea44e38663dda633460d94f blob + 7943a9e4cf2c5760b086fbe8f1e0245e20553aa8 --- src/main/templates/META-INF/neoforge.mods.toml +++ src/main/templates/META-INF/neoforge.mods.toml @@ -49,3 +49,10 @@ type = "optional" versionRange = "${waterframes_version_range}" ordering = "AFTER" side = "BOTH" + +[[dependencies.${mod_id}]] +modId = "citadel" +type = "optional" +versionRange = "${citadel_version_range}" +ordering = "AFTER" +side = "BOTH"