commit - d0df860db553d49ad9577921e485218cc97f2c8f
commit + ea7b69c40058e45f428ebffe17534c7caca55fb3
blob - 70c6d5dbf3fc487305f4751c46fe6fc31b3bb542
blob + a4e3057ff19c17f5bd13c8875e0cfd7edc7870d1
--- README.md
+++ README.md
-# 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
*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.
## 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
jar {
exclude 'dev/ryanhcode/**'
exclude 'me/srrapero720/**'
+ exclude 'com/github/alexthe666/**'
}
var generateModMetadata = tasks.register("generateModMetadata", ProcessResources) {
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
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
+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}.
+ *
+ * <p>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.
+ *
+ * <p>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
+package com.github.alexthe666.citadel.server.entity.pathfinding.raycoms;
+
+/**
+ * COMPILE-TIME STUB of
+ * {@code com.github.alexthe666.citadel.server.entity.pathfinding.raycoms.AdvancedPathNavigate}.
+ *
+ * <p>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.
+ *
+ * <p>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
+package com.github.alexthe666.citadel.server.entity.pathfinding.raycoms;
+
+/**
+ * COMPILE-TIME STUB of
+ * {@code com.github.alexthe666.citadel.server.entity.pathfinding.raycoms.PathResult}.
+ *
+ * <p>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<J> {
+}
blob - b0c9c249d56ddbe451aaa3c0bdb02ed2905b28df
blob + 0767b5fca0ee1a1e17b034b21949dd8212888137
--- 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.Level;
import org.slf4j.Logger;
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) {
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) {
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).");
}
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.
+ *
+ * <p>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
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<String, String[]> 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
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
+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.
+ *
+ * <p>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.
+ *
+ * <p>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.
+ *
+ * <p><b>The membership-aware check.</b> 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}):
+ *
+ * <ul>
+ * <li><b>Mob in a sub-level</b> → it's standing on a contraption. Allow
+ * any target — the destination is most likely something else on the
+ * same ship, and rejecting would freeze the mob in place.</li>
+ * <li><b>Mob in world space</b> → fall back to the distance check. Any
+ * path target more than {@link #AERONAUTICSCOMPAT$MAX_REASONABLE_DISTANCE_SQ}
+ * blocks² away is the bleed-through pattern; reject and let the goal
+ * pick something else next tick.</li>
+ * </ul>
+ */
+@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<PathResult> 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
"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
versionRange = "${waterframes_version_range}"
ordering = "AFTER"
side = "BOTH"
+
+[[dependencies.${mod_id}]]
+modId = "citadel"
+type = "optional"
+versionRange = "${citadel_version_range}"
+ordering = "AFTER"
+side = "BOTH"