Rohan Verma



Commit Diff


commit - 4dde72f7ef2c407e3be26faa36f808d44a193f27
commit + 7559f2684f57cde53d5c8ecf448780a92ef7d269
blob - a4e3057ff19c17f5bd13c8875e0cfd7edc7870d1
blob + 99db605a70e585ea29a6c61f71c09d1012be530b
--- README.md
+++ README.md
@@ -1,4 +1,4 @@
-# AeronauticsCompat 1.0.4
+# AeronauticsCompat 1.0.6
 
 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).
 
@@ -9,7 +9,7 @@ All patches are **soft dependencies** — each one app
 ## Patches
 
 ### Etched × Sable: music discs on contraptions stop correctly
-**Problem.** On a moving Sable contraption (e.g., Create: Aeronautics ship), music discs never stop playing after the disc is ejected. Affects **every** disc — vanilla, datapack, and Etched YouTube discs — as long as Etched is installed.
+**Problem.** On a moving Sable contraption (e.g., Create: Aeronautics ship), music discs never stop playing after the disc is ejected. Affects **every** disc — vanilla, datapack, and Etched discs — as long as Etched is installed.
 
 **Cause.** Etched's `LevelRendererMixin.modifySoundInstance` wraps every jukebox sound in `StopListeningSound`, which doesn't extend `AbstractSoundInstance`, so Sable's `AbstractSoundInstanceMixin` (which implements `SoundInstanceDelegated`) doesn't apply. On stop, Sable can't unwrap the sound to the `MovingSoundInstanceDelegate` keyed in OpenAL; the stop lookup misses and the sound plays forever.
 
@@ -29,16 +29,12 @@ All patches are **soft dependencies** — each one app
 ### 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.
+**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 allocation exhausts JVM heap 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.
+**Fix.** Mixin `AdvancedPathNavigate.moveToXYZ` at HEAD. If the horizontal span between the mob's actual position and the target exceeds 4096 blocks on either axis, 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. The check is symmetric, so legitimate in-world pathing AND legitimate mob-to-mob pathing between two entities on the same contraption (where both endpoints are in the same sub-level coordinate frame, and therefore close in relative terms) both pass through untouched.
 
 *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
@@ -52,7 +48,7 @@ No target mod is a hard dependency. The mod loads and 
 ./gradlew build
 ```
 
-No external jars required in `libs/`. Compile-time stubs for Sable and WaterFrames live in `src/stubs/java/` and are excluded from the output jar. If you want to cross-compile against a real Etched jar (e.g., to rebuild the YouTube source), drop `etched.jar` in `libs/`.
+No external jars required in `libs/`. Compile-time stubs for Sable and WaterFrames live in `src/stubs/java/` and are excluded from the output jar.
 
 ## License
 
blob - f8c6b5889d17546f887628362dd639d40a4045ac
blob + 201fece08b720c85c74dcbe0b1fc8646db031be4
--- gradle.properties
+++ gradle.properties
@@ -24,8 +24,8 @@ citadel_version_range=[2.0.0,3.0.0)
 # Mod Properties
 mod_id=aeronauticscompat
 mod_name=AeronauticsCompat
-mod_license=MIT
-mod_version=1.0.4
-mod_authors=rsap
-mod_description=Compatibility fixes for Create: Aeronautics / Sable sub-levels
+mod_license=GPLv3
+mod_version=1.0.6
+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 - 880df312550304f2369701161e1fb07708b704f6
blob + 6f30b702572ce3c60dedc454f83b4ce528abb29c
--- src/main/java/sh/rsap/aeronauticscompat/AeronauticsCompat.java
+++ src/main/java/sh/rsap/aeronauticscompat/AeronauticsCompat.java
@@ -5,49 +5,34 @@ import net.neoforged.bus.api.IEventBus;
 import net.neoforged.fml.ModContainer;
 import net.neoforged.fml.ModList;
 import net.neoforged.fml.common.Mod;
-import net.neoforged.fml.config.ModConfig;
 import net.neoforged.fml.event.lifecycle.FMLCommonSetupEvent;
-import net.neoforged.neoforge.common.ModConfigSpec;
-import org.apache.commons.lang3.tuple.Pair;
 import org.slf4j.Logger;
 
+/**
+ * AeronauticsCompat entry point.
+ *
+ * <p>All of this mod's actual behavior lives in its mixins — see
+ * {@code aeronauticscompat.mixins.json} and
+ * {@link sh.rsap.aeronauticscompat.mixin.AeronauticsCompatMixinPlugin} for the
+ * list of patches and their per-mod gates. This class just logs what's on the
+ * classpath at common setup time so crash reports are easier to read.
+ */
 @Mod(AeronauticsCompat.MOD_ID)
 public final class AeronauticsCompat {
 
     public static final String MOD_ID = "aeronauticscompat";
     public static final Logger LOGGER = LogUtils.getLogger();
 
-    public static final AeronauticsCompatConfig CONFIG;
-    private static final ModConfigSpec CONFIG_SPEC;
-
-    static {
-        Pair<AeronauticsCompatConfig, ModConfigSpec> pair =
-                new ModConfigSpec.Builder().configure(AeronauticsCompatConfig::new);
-        CONFIG = pair.getLeft();
-        CONFIG_SPEC = pair.getRight();
-    }
-
     public AeronauticsCompat(IEventBus bus, ModContainer container) {
         bus.addListener(this::commonSetup);
-        container.registerConfig(ModConfig.Type.CLIENT, CONFIG_SPEC);
     }
 
     private void commonSetup(FMLCommonSetupEvent event) {
         ModList mods = ModList.get();
-        if (mods.isLoaded("etched")) {
-            // Loaded reflectively so the class isn't linked when Etched is absent.
-            try {
-                Class.forName("sh.rsap.aeronauticscompat.etched.YoutubeSourceRegistrar")
-                        .getMethod("register")
-                        .invoke(null);
-            } catch (Throwable t) {
-                LOGGER.error("[AeronauticsCompat] Failed to register YouTube source", t);
-            }
-        } else {
-            LOGGER.info("[AeronauticsCompat] Etched not present; YouTube source skipped.");
-        }
-
-        LOGGER.info("[AeronauticsCompat] Sable={} Etched={} WaterFrames={}",
-                mods.isLoaded("sable"), mods.isLoaded("etched"), mods.isLoaded("waterframes"));
+        LOGGER.info("[AeronauticsCompat] Sable={} Etched={} WaterFrames={} Citadel={}",
+                mods.isLoaded("sable"),
+                mods.isLoaded("etched"),
+                mods.isLoaded("waterframes"),
+                mods.isLoaded("citadel"));
     }
 }
blob - eb6be1509926cb22eaaecc25bf125ddd75238757 (mode 644)
blob + /dev/null
--- src/main/java/sh/rsap/aeronauticscompat/AeronauticsCompatConfig.java
+++ /dev/null
@@ -1,45 +0,0 @@
-package sh.rsap.aeronauticscompat;
-
-import net.neoforged.neoforge.common.ModConfigSpec;
-
-public final class AeronauticsCompatConfig {
-
-    public final ModConfigSpec.ConfigValue<String> proxyUrl;
-    public final ModConfigSpec.ConfigValue<String> proxyToken;
-    public final ModConfigSpec.IntValue connectTimeoutSeconds;
-    public final ModConfigSpec.IntValue readTimeoutSeconds;
-
-    AeronauticsCompatConfig(ModConfigSpec.Builder builder) {
-        builder.comment(
-                "Etched YouTube proxy settings.",
-                "Only used when Etched is installed."
-        ).push("YoutubeProxy");
-
-        this.proxyUrl = builder
-                .comment(
-                        "Base URL of a self-hosted yt-dlp proxy (no trailing slash).",
-                        "Leave empty to disable YouTube support.",
-                        "Example: https://yt.example.com"
-                )
-                .define("URL", "");
-
-        this.proxyToken = builder
-                .comment(
-                        "Bearer token for the YouTube proxy.",
-                        "Leave empty to disable YouTube support."
-                )
-                .define("Token", "");
-
-        this.connectTimeoutSeconds = builder
-                .comment("HTTP connect timeout (seconds) when talking to the proxy.")
-                .defineInRange("Connect Timeout", 15, 1, 300);
-
-        this.readTimeoutSeconds = builder
-                .comment(
-                        "HTTP read timeout (seconds). First resolves can take a while."
-                )
-                .defineInRange("Read Timeout", 120, 10, 600);
-
-        builder.pop();
-    }
-}
blob - f0570e37a18aa6d29b8123f10d92cba18aa67669 (mode 644)
blob + /dev/null
--- src/main/java/sh/rsap/aeronauticscompat/etched/YoutubeSource.java
+++ /dev/null
@@ -1,291 +0,0 @@
-package sh.rsap.aeronauticscompat.etched;
-
-import sh.rsap.aeronauticscompat.AeronauticsCompat;
-
-import com.google.gson.JsonObject;
-import com.google.gson.JsonParseException;
-import com.google.gson.JsonParser;
-import gg.moonflower.etched.api.record.TrackData;
-import gg.moonflower.etched.api.sound.download.SoundDownloadSource;
-import gg.moonflower.etched.api.util.DownloadProgressListener;
-import net.minecraft.network.chat.Component;
-import net.minecraft.network.chat.TextColor;
-import net.minecraft.server.packs.resources.ResourceManager;
-import net.minecraft.util.GsonHelper;
-import org.jetbrains.annotations.Nullable;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.io.OutputStream;
-import java.net.HttpURLConnection;
-import java.net.Proxy;
-import java.net.URI;
-import java.net.URISyntaxException;
-import java.net.URL;
-import java.nio.charset.StandardCharsets;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-import java.util.WeakHashMap;
-
-/**
- * Resolves YouTube URLs by delegating to a self-hosted yt-dlp proxy service.
- * <p>
- * Flow: POSTs to {@code <proxy>/api/resolve} with JSON {@code {"url": "..."}}
- * and bearer auth. The service runs yt-dlp, returns metadata plus an
- * {@code audio_url} pointing at {@code <proxy>/api/stream/<sha1>.mp3}. That
- * stream endpoint is unauthenticated — the sha1 id is the capability (like
- * a presigned URL) so the mod's audio pipeline can fetch it without having
- * to attach an Authorization header.
- */
-public final class YoutubeSource implements SoundDownloadSource {
-
-    private static final Component BRAND =
-            Component.translatable("sound_source.aeronauticscompat.brand")
-                    .withStyle(style -> style.withColor(TextColor.fromRgb(0xFF0000)));
-
-    private static final List<String> HOST_SUFFIXES = List.of(
-            "youtube.com",
-            "youtu.be",
-            "youtube-nocookie.com",
-            "music.youtube.com"
-    );
-
-    private final Map<String, Boolean> validCache = new WeakHashMap<>();
-
-    // Short-lived cache of resolved track info so resolveTracks, resolveUrl,
-    // and resolveAlbumCover (all called in close succession when a disc is
-    // inserted) share one HTTP call.
-    private final Map<String, ResolvedTrack> trackCache =
-            Collections.synchronizedMap(new WeakHashMap<>());
-
-    private record ResolvedTrack(String audioUrl, String title, String artist, double duration) {}
-
-    private static String trimEndpoint() {
-        String ep = AeronauticsCompat.CONFIG.proxyUrl.get();
-        if (ep == null) return "";
-        return ep.replaceAll("/+$", "");
-    }
-
-    private static String token() {
-        String t = AeronauticsCompat.CONFIG.proxyToken.get();
-        return t == null ? "" : t;
-    }
-
-    private static boolean configured() {
-        return !trimEndpoint().isEmpty() && !token().isEmpty();
-    }
-
-    private ResolvedTrack resolve(String url,
-                                  @Nullable DownloadProgressListener progressListener,
-                                  Proxy proxy) throws IOException {
-        ResolvedTrack cached = this.trackCache.get(url);
-        if (cached != null) return cached;
-
-        if (!configured()) {
-            throw new IOException(
-                    "AeronauticsCompat YouTube proxy is not configured. " +
-                            "Set 'URL' and 'Token' in config/aeronauticscompat-client.toml."
-            );
-        }
-
-        if (progressListener != null) {
-            progressListener.progressStartRequest(Component.translatable(
-                    "sound_source.etched.requesting", this.getApiName()));
-        }
-
-        String body = "{\"url\":" + jsonString(url) + "}";
-        URL endpointUrl;
-        try {
-            endpointUrl = new URI(trimEndpoint() + "/api/resolve").toURL();
-        } catch (URISyntaxException e) {
-            throw new IOException("Invalid proxy URL: " + trimEndpoint(), e);
-        }
-
-        HttpURLConnection conn = (HttpURLConnection) endpointUrl.openConnection(proxy);
-        try {
-            conn.setRequestMethod("POST");
-            conn.setDoOutput(true);
-            conn.setConnectTimeout(AeronauticsCompat.CONFIG.connectTimeoutSeconds.get() * 1000);
-            conn.setReadTimeout(AeronauticsCompat.CONFIG.readTimeoutSeconds.get() * 1000);
-            conn.setInstanceFollowRedirects(true);
-            for (Map.Entry<String, String> h : SoundDownloadSource.getDownloadHeaders().entrySet()) {
-                conn.setRequestProperty(h.getKey(), h.getValue());
-            }
-            conn.setRequestProperty("Content-Type", "application/json");
-            conn.setRequestProperty("Accept", "application/json");
-            conn.setRequestProperty("Authorization", "Bearer " + token());
-
-            try (OutputStream os = conn.getOutputStream()) {
-                os.write(body.getBytes(StandardCharsets.UTF_8));
-            }
-
-            int code = conn.getResponseCode();
-            if (code != 200) {
-                String err = readErrorMessage(conn);
-                throw new IOException("proxy returned " + code + (err.isEmpty() ? "" : ": " + err));
-            }
-
-            JsonObject json;
-            try (InputStreamReader reader = new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8)) {
-                json = JsonParser.parseReader(reader).getAsJsonObject();
-            }
-
-            String audioUrl = GsonHelper.getAsString(json, "audio_url");
-            String title = GsonHelper.getAsString(json, "title", "");
-            String artist = GsonHelper.getAsString(json, "artist", "");
-            double duration = 0.0;
-            if (json.has("duration") && !json.get("duration").isJsonNull()) {
-                duration = json.get("duration").getAsDouble();
-            }
-
-            ResolvedTrack t = new ResolvedTrack(audioUrl, title, artist, duration);
-            this.trackCache.put(url, t);
-            return t;
-        } finally {
-            conn.disconnect();
-        }
-    }
-
-    @Override
-    public Collection<URL> resolveUrl(String url,
-                                      @Nullable DownloadProgressListener progressListener,
-                                      Proxy proxy) throws IOException, URISyntaxException, JsonParseException {
-        ResolvedTrack t = this.resolve(url, progressListener, proxy);
-        return Collections.singletonList(new URI(t.audioUrl()).toURL());
-    }
-
-    @Override
-    public Collection<TrackData> resolveTracks(String url,
-                                               @Nullable DownloadProgressListener progressListener,
-                                               Proxy proxy) throws IOException, URISyntaxException, JsonParseException {
-        ResolvedTrack t = this.resolve(url, progressListener, proxy);
-        String title = t.title().isEmpty() ? url : t.title();
-        String artist = t.artist().isEmpty() ? "YouTube" : t.artist();
-        return Collections.singletonList(new TrackData(url, artist, Component.literal(title)));
-    }
-
-    @Override
-    public Optional<String> resolveAlbumCover(String url,
-                                              @Nullable DownloadProgressListener progressListener,
-                                              Proxy proxy,
-                                              ResourceManager resourceManager) {
-        String videoId = extractVideoId(url);
-        if (videoId == null) return Optional.empty();
-        return Optional.of("https://i.ytimg.com/vi/" + videoId + "/hqdefault.jpg");
-    }
-
-    @Override
-    public boolean isValidUrl(String url) {
-        return this.validCache.computeIfAbsent(url, key -> {
-            try {
-                String host = new URI(key).getHost();
-                if (host == null) return false;
-                String lower = host.toLowerCase();
-                for (String suffix : HOST_SUFFIXES) {
-                    if (lower.equals(suffix) || lower.endsWith("." + suffix)) return true;
-                }
-                return false;
-            } catch (URISyntaxException e) {
-                return false;
-            }
-        });
-    }
-
-    @Override
-    public boolean isTemporary(String url) {
-        return true;
-    }
-
-    @Override
-    public String getApiName() {
-        return "YouTube";
-    }
-
-    @Override
-    public Optional<Component> getBrandText(String url) {
-        return Optional.of(BRAND);
-    }
-
-    // --- helpers ---
-
-    private static @Nullable String extractVideoId(String url) {
-        try {
-            URI uri = new URI(url);
-            String host = uri.getHost();
-            if (host == null) return null;
-            String lowerHost = host.toLowerCase();
-            String path = uri.getPath() == null ? "" : uri.getPath();
-
-            if (lowerHost.equals("youtu.be") || lowerHost.endsWith(".youtu.be")) {
-                if (path.length() > 1) {
-                    return path.substring(1).split("/")[0];
-                }
-                return null;
-            }
-
-            for (String prefix : new String[]{"/shorts/", "/embed/", "/v/"}) {
-                if (path.startsWith(prefix)) {
-                    String rest = path.substring(prefix.length());
-                    int slash = rest.indexOf('/');
-                    return slash >= 0 ? rest.substring(0, slash) : rest;
-                }
-            }
-
-            String query = uri.getQuery();
-            if (query == null) return null;
-            for (String part : query.split("&")) {
-                int eq = part.indexOf('=');
-                if (eq <= 0) continue;
-                if (part.substring(0, eq).equals("v")) {
-                    return part.substring(eq + 1);
-                }
-            }
-            return null;
-        } catch (URISyntaxException e) {
-            return null;
-        }
-    }
-
-    private static String readErrorMessage(HttpURLConnection conn) {
-        try (InputStream es = conn.getErrorStream()) {
-            if (es == null) {
-                String msg = conn.getResponseMessage();
-                return msg == null ? "" : msg;
-            }
-            String text = new String(es.readAllBytes(), StandardCharsets.UTF_8);
-            try {
-                JsonObject json = JsonParser.parseString(text).getAsJsonObject();
-                if (json.has("error")) return GsonHelper.getAsString(json, "error");
-            } catch (Throwable ignored) {
-            }
-            return text;
-        } catch (IOException e) {
-            return "";
-        }
-    }
-
-    private static String jsonString(String s) {
-        StringBuilder sb = new StringBuilder(s.length() + 2);
-        sb.append('"');
-        for (int i = 0; i < s.length(); i++) {
-            char c = s.charAt(i);
-            switch (c) {
-                case '"' -> sb.append("\\\"");
-                case '\\' -> sb.append("\\\\");
-                case '\n' -> sb.append("\\n");
-                case '\r' -> sb.append("\\r");
-                case '\t' -> sb.append("\\t");
-                default -> {
-                    if (c < 0x20) sb.append(String.format("\\u%04x", (int) c));
-                    else sb.append(c);
-                }
-            }
-        }
-        sb.append('"');
-        return sb.toString();
-    }
-}
blob - b8bf15236cc2db118f44b5466ad06074cbee9131 (mode 644)
blob + /dev/null
--- src/main/java/sh/rsap/aeronauticscompat/etched/YoutubeSourceRegistrar.java
+++ /dev/null
@@ -1,21 +0,0 @@
-package sh.rsap.aeronauticscompat.etched;
-
-import gg.moonflower.etched.api.sound.download.SoundSourceManager;
-import sh.rsap.aeronauticscompat.AeronauticsCompat;
-
-/**
- * Isolated registrar for the Etched YouTube source.
- *
- * <p>Lives in its own class so {@link SoundSourceManager} is only class-
- * loaded when Etched is installed. {@link AeronauticsCompat} calls
- * {@link #register()} reflectively after checking {@code ModList.isLoaded("etched")}.
- */
-public final class YoutubeSourceRegistrar {
-
-    private YoutubeSourceRegistrar() {}
-
-    public static void register() {
-        SoundSourceManager.registerSource(new YoutubeSource());
-        AeronauticsCompat.LOGGER.info("[AeronauticsCompat] Etched YouTube source registered.");
-    }
-}
blob - 64849a8cafc5f34c7729c941d926fcd8b7d5b4b0
blob + 3faec0a4989be5bea853b1659507a0d278745e94
--- src/main/java/sh/rsap/aeronauticscompat/mixin/citadel/CitadelAdvancedPathNavigateMixin.java
+++ src/main/java/sh/rsap/aeronauticscompat/mixin/citadel/CitadelAdvancedPathNavigateMixin.java
@@ -11,7 +11,6 @@ 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.
@@ -19,49 +18,58 @@ import sh.rsap.aeronauticscompat.compat.SableBridge;
  * <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.
+ * sub-levels. Those entities' raw positions live in <i>sub-level-local</i>
+ * coordinates — often tens of millions of blocks from any world-space mob
+ * that sees them.
  *
- * <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>When a Citadel-powered mob (Alex's Mobs, Alex's Caves, Cataclysm, etc.)
+ * calls {@link AdvancedPathNavigate#moveToXYZ} with one of these targets,
+ * Citadel's {@code AbstractPathJob} allocates a {@code ChunkCache} sized by
+ * the bounding box between the mob's <b>world-space</b> start position and
+ * the <b>sub-level-space</b> end position:
  *
- * <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}):
+ * <pre>
+ *   final int minX = Math.min(start.getX(), end.getX()) - range/2;
+ *   final int maxX = Math.max(start.getX(), end.getX()) + range/2;
+ *   // ... same for Z, then a LevelChunk[dx/16 + 1][dz/16 + 1] is allocated.
+ * </pre>
  *
- * <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>
+ * With one coordinate near origin and the other in the tens of millions, the
+ * chunk array is sized in the trillions of entries and the JVM OOMs. This
+ * happens on servers too — the path job runs on the server thread.
+ *
+ * <p><b>The span-based guard.</b> We measure the <i>span</i> between the
+ * mob's current position (what {@code AbstractPathJob.prepareStart} will use
+ * as {@code start}) and the requested target {@code (x, y, z)} (the
+ * {@code end}). If the horizontal span exceeds
+ * {@link #AERONAUTICSCOMPAT$MAX_REASONABLE_SPAN} blocks, the resulting
+ * {@code ChunkCache} would be too large to allocate safely — this is
+ * definitionally the bleed-through pattern, regardless of whether the mob
+ * is on a contraption or in world space. We reject the path and return
+ * {@code null}; next tick the goal will pick a saner target.
+ *
+ * <p>Crucially this guard is <i>symmetric</i>: it doesn't matter whether
+ * start is tiny and end is huge, or vice-versa — we only care about the
+ * span. That means legitimate mob-to-mob paths entirely within a single
+ * sub-level (both endpoints at, say, 20000001 and 20000005) are allowed
+ * through, because the span is small. And legitimate world-space paths
+ * (start and end both near world origin) are also allowed. Only the
+ * pathological cross-domain case is rejected.
  */
 @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.
+     * Maximum horizontal span (blocks, per axis) between a mob's current
+     * position and a prospective path target before the call is rejected.
+     * Chosen so that {@code (span / 16 + 1)²} stays comfortably below
+     * anything that could exhaust a typical server heap — 4096 blocks yields
+     * a ~256×256 chunk array (65k entries), still plenty of headroom for
+     * any real pathing job while catching anything in the millions.
      */
     @Unique
-    private static final double AERONAUTICSCOMPAT$MAX_REASONABLE_DISTANCE_SQ = 1024.0 * 1024.0;
+    private static final double AERONAUTICSCOMPAT$MAX_REASONABLE_SPAN = 4096.0;
 
     @Inject(
         method = "moveToXYZ(DDDD)Lcom/github/alexthe666/citadel/server/entity/pathfinding/raycoms/PathResult;",
@@ -85,25 +93,17 @@ public abstract class CitadelAdvancedPathNavigateMixin
             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;
+        final double dx = Math.abs(x - pos.x);
+        final double dz = Math.abs(z - pos.z);
 
-        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.
+        if (Double.isNaN(dx) || Double.isNaN(dz)
+            || Double.isInfinite(dx) || Double.isInfinite(dz)
+            || dx > AERONAUTICSCOMPAT$MAX_REASONABLE_SPAN
+            || dz > AERONAUTICSCOMPAT$MAX_REASONABLE_SPAN) {
+            // ChunkCache allocation would explode. This is the bleed-through
+            // pattern — reject the path; next tick the goal will pick something
+            // more sensible. Silent; this fires frequently.
             cir.setReturnValue(null);
         }
     }
blob - ed00e90ee8d9dc9d9540525f82a6d0deb1e50ddc
blob + ffaba628e15d07a3e732d46ad4bc0441545f7284
--- src/main/java/sh/rsap/aeronauticscompat/mixin/etched/EtchedStopListeningSoundMixin.java
+++ src/main/java/sh/rsap/aeronauticscompat/mixin/etched/EtchedStopListeningSoundMixin.java
@@ -9,7 +9,7 @@ import org.spongepowered.asm.mixin.Unique;
 /**
  * Fixes the "jukebox music never stops on a Sable/Create: Aeronautics
  * contraption" bug that affects <em>every</em> music disc (vanilla,
- * datapack, Etched YouTube) as long as Etched is installed.
+ * datapack, Etched) as long as Etched is installed.
  *
  * <p><strong>Root cause.</strong> When a jukebox sound is played on a
  * block sitting in a Sable sub-level (i.e. a moving contraption), Sable's
blob - b88b1ede6017051e0a67ac4932122d7a33e80fc5
blob + 0967ef424bce6791893e9a57bb952f80fd536e93
--- src/main/resources/assets/aeronauticscompat/lang/en_us.json
+++ src/main/resources/assets/aeronauticscompat/lang/en_us.json
@@ -1,3 +1 @@
-{
-  "sound_source.aeronauticscompat.brand": "Provided by YouTube"
-}
+{}