commit 7559f2684f57cde53d5c8ecf448780a92ef7d269 from: rohanverma2007 date: Thu Apr 23 13:03:20 2026 UTC delete youtube support, try to fix windows 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. + * + *

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 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 proxyUrl; - public final ModConfigSpec.ConfigValue 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. - *

- * Flow: POSTs to {@code /api/resolve} with JSON {@code {"url": "..."}} - * and bearer auth. The service runs yt-dlp, returns metadata plus an - * {@code audio_url} pointing at {@code /api/stream/.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 HOST_SUFFIXES = List.of( - "youtube.com", - "youtu.be", - "youtube-nocookie.com", - "music.youtube.com" - ); - - private final Map 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 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 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 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 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 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 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. - * - *

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; *

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 sub-level-local + * coordinates — often tens of millions of blocks from any world-space mob + * that sees them. * - *

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. + *

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 world-space start position and + * the sub-level-space end position: * - *

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}): + *

+ *   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.
+ * 
* - * + * 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. + * + *

The span-based guard. We measure the span 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. + * + *

Crucially this guard is symmetric: 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 every music disc (vanilla, - * datapack, Etched YouTube) as long as Etched is installed. + * datapack, Etched) as long as Etched is installed. * *

Root cause. 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" -} +{}