Skip to content

Fix legacy client crashes from unsupported 26.2 particles#7

Merged
ohnodev merged 4 commits intomainfrom
fix/legacylink-particle-remap-26_1
Apr 16, 2026
Merged

Fix legacy client crashes from unsupported 26.2 particles#7
ohnodev merged 4 commits intomainfrom
fix/legacylink-particle-remap-26_1

Conversation

@ohnodev
Copy link
Copy Markdown
Owner

@ohnodev ohnodev commented Apr 15, 2026

Summary

  • Add explicit 26.2-only particle constants for legacy compatibility filtering.
  • Rewrite clientbound level_particles payloads that carry unsupported particle types to safe legacy fallback particles, preventing decode disconnects on 26.1 clients.
  • Refresh prebuilt/legacylink-0.1.1.jar from the updated build output.

Test plan

  • cd legacylink && ./gradlew build
  • cd legacylink-client-tests && ./.venv/bin/python run_e2e.py --skip-stability --skip-server-verify
  • Confirm no disconnect on join from clientbound/minecraft:level_particles during smoke run.

Made with Cursor

Summary by CodeRabbit

  • Bug Fixes

    • Improved legacy client compatibility by stripping unsupported particle types from registry syncs and converting incompatible particle packets to safe fallbacks, reducing visual errors.
  • New Features

    • Added automatic remapping of particle data for older clients and a startup validation that every unsupported particle has a configured fallback.

Filter 26.2-only particle registry entries and rewrite level_particles payloads to safe fallbacks so 26.1 clients no longer disconnect on unknown particle ids, then refresh the prebuilt jar artifact.

Made-with: Cursor
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 15, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f6d0e3ff-d77b-472d-9053-60368d781039

📥 Commits

Reviewing files that changed from the base of the PR and between b10e662 and 2936d10.

⛔ Files ignored due to path filters (1)
  • prebuilt/legacylink-0.1.1.jar is excluded by !**/*.jar
📒 Files selected for processing (1)
  • src/main/java/dev/ohno/legacylink/handler/LegacyPacketHandler.java
✅ Files skipped from review due to trivial changes (1)
  • src/main/java/dev/ohno/legacylink/handler/LegacyPacketHandler.java

📝 Walkthrough

Walkthrough

Adds a constant listing 26.2-only particle IDs; strips those IDs from particle-type registry sync for legacy clients; and implements outbound ClientboundLevelParticlesPacket remapping with option rewrites and fallback substitution plus startup validation of fallback coverage.

Changes

Cohort / File(s) Summary
Particle Constants
src/main/java/dev/ohno/legacylink/LegacyLinkConstants.java
Added public static final Set<String> LEGACY_UNSUPPORTED_PARTICLE_IDS listing four 26.2-only particle registry IDs and documentation noting they are absent on 26.1 clients.
Registry Filtering & Packet Remapping
src/main/java/dev/ohno/legacylink/handler/LegacyPacketHandler.java
Extended play-phase routing to intercept ClientboundLevelParticlesPacket; added strip26_2OnlyParticle handling in filterRegistryData; introduced LEGACY_PARTICLE_FALLBACKS + startup validateParticleFallbackCoverage; implemented remapLevelParticles, remapParticleOptionsForLegacy, and copyParticlePacket to rewrite block/item particle options and substitute unsupported particle types with configured fallbacks.

Sequence Diagram

sequenceDiagram
    participant Client as Legacy Client
    participant Handler as LegacyPacketHandler
    participant Registry as Registry Sync
    participant Producer as Particle Producer

    rect rgba(0, 150, 200, 0.5)
    note over Handler,Registry: Registry Filtering Phase
    Handler->>Registry: filterRegistryData()
    Handler->>Handler: compute strip26_2OnlyParticle from LEGACY_UNSUPPORTED_PARTICLE_IDS
    Handler->>Registry: remove unsupported particle entries
    Registry-->>Client: Filtered registry (no 26.2 particles)
    end

    rect rgba(200, 150, 0, 0.5)
    note over Producer,Handler: Packet Remapping Phase
    Producer->>Handler: ClientboundLevelParticlesPacket (26.2 particle)
    Handler->>Handler: routePlayPacket() detects particle packet
    Handler->>Handler: remapLevelParticles()
    Handler->>Handler: remapParticleOptionsForLegacy() (block/item state rewrites)
    Handler->>Handler: check particle type against LEGACY_UNSUPPORTED_PARTICLE_IDS
    alt Unsupported
        Handler->>Handler: substitute with LEGACY_PARTICLE_FALLBACKS
        Handler->>Client: rewritten ClientboundLevelParticlesPacket via copyParticlePacket()
    else Supported
        Handler-->>Client: original or option-rewritten packet
    end
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Poem

🐰 I hop through registries, a careful little sprite,
I tuck four new sparks out of legacy sight.
I remap blocks and items, soften flashes that gleam,
So old clients still shimmer in a gentler dream.
Nibble, translate, and onward we leap!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 14.29% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main change: fixing legacy client crashes caused by unsupported particles from version 26.2.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/legacylink-particle-remap-26_1

Comment @coderabbitai help to get the list of available commands and usage tips.

Remap block-state ids and item templates inside level_particles options before fallback handling so common impact particles (such as deepslate crumble) cannot desync legacy decoding.

Made-with: Cursor
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/main/java/dev/ohno/legacylink/handler/LegacyPacketHandler.java (1)

102-107: Consider adding a coverage guard between unsupported IDs and fallback mappings.

This prevents silent drift if a new unsupported particle ID is added later without an explicit fallback choice.

Suggested guard
 private static final Map<String, SimpleParticleType> LEGACY_PARTICLE_FALLBACKS = Map.of(
         "minecraft:noxious_gas", ParticleTypes.SMOKE,
         "minecraft:noxious_gas_cloud", ParticleTypes.CLOUD,
         "minecraft:sulfur_bubbles", ParticleTypes.BUBBLE,
         "minecraft:sulfur_cube_goo", ParticleTypes.ITEM_SLIME
 );
+
+static {
+    Set<String> missing = new HashSet<>(LegacyLinkConstants.LEGACY_UNSUPPORTED_PARTICLE_IDS);
+    missing.removeAll(LEGACY_PARTICLE_FALLBACKS.keySet());
+    if (!missing.isEmpty()) {
+        LegacyLinkMod.LOGGER.warn(
+                "[LegacyLink] Missing explicit legacy particle fallback(s): {}. Defaulting to smoke.",
+                missing
+        );
+    }
+}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/dev/ohno/legacylink/handler/LegacyPacketHandler.java` around
lines 102 - 107, Add a coverage guard to ensure the LEGACY_PARTICLE_FALLBACKS
map covers exactly the known unsupported particle IDs: implement a static
validation (in the LegacyPacketHandler class static initializer or the method
that accesses LEGACY_PARTICLE_FALLBACKS) that computes the set difference
between the authoritative unsupported ID set and
LEGACY_PARTICLE_FALLBACKS.keySet() and fails fast (throw IllegalStateException
or log+exit) if any unsupported IDs are missing or if there are unexpected keys;
reference LEGACY_PARTICLE_FALLBACKS and the authoritative unsupported ID
collection (create one if missing) so future additions cannot silently drift
without an explicit fallback.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/main/java/dev/ohno/legacylink/handler/LegacyPacketHandler.java`:
- Around line 102-107: Add a coverage guard to ensure the
LEGACY_PARTICLE_FALLBACKS map covers exactly the known unsupported particle IDs:
implement a static validation (in the LegacyPacketHandler class static
initializer or the method that accesses LEGACY_PARTICLE_FALLBACKS) that computes
the set difference between the authoritative unsupported ID set and
LEGACY_PARTICLE_FALLBACKS.keySet() and fails fast (throw IllegalStateException
or log+exit) if any unsupported IDs are missing or if there are unexpected keys;
reference LEGACY_PARTICLE_FALLBACKS and the authoritative unsupported ID
collection (create one if missing) so future additions cannot silently drift
without an explicit fallback.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 703aaf97-e253-47cb-aeff-1aa0ba16a8cd

📥 Commits

Reviewing files that changed from the base of the PR and between 5307535 and d55387e.

⛔ Files ignored due to path filters (1)
  • prebuilt/legacylink-0.1.1.jar is excluded by !**/*.jar
📒 Files selected for processing (2)
  • src/main/java/dev/ohno/legacylink/LegacyLinkConstants.java
  • src/main/java/dev/ohno/legacylink/handler/LegacyPacketHandler.java

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/main/java/dev/ohno/legacylink/handler/LegacyPacketHandler.java (1)

107-112: Add startup validation so unsupported-particle coverage cannot silently drift.

Line 632 defaults missing mappings to SMOKE; that is safe, but it can hide newly added unsupported IDs. Consider validating that every ID in LEGACY_UNSUPPORTED_PARTICLE_IDS has an explicit fallback.

♻️ Proposed hardening
 private static final Map<String, SimpleParticleType> LEGACY_PARTICLE_FALLBACKS = Map.of(
         "minecraft:noxious_gas", ParticleTypes.SMOKE,
         "minecraft:noxious_gas_cloud", ParticleTypes.CLOUD,
         "minecraft:sulfur_bubbles", ParticleTypes.BUBBLE,
         "minecraft:sulfur_cube_goo", ParticleTypes.ITEM_SLIME
 );
+
+static {
+    if (!LEGACY_PARTICLE_FALLBACKS.keySet().containsAll(LegacyLinkConstants.LEGACY_UNSUPPORTED_PARTICLE_IDS)) {
+        Set<String> missing = new HashSet<>(LegacyLinkConstants.LEGACY_UNSUPPORTED_PARTICLE_IDS);
+        missing.removeAll(LEGACY_PARTICLE_FALLBACKS.keySet());
+        throw new ExceptionInInitializerError("[LegacyLink] Missing particle fallback mappings: " + missing);
+    }
+}

Also applies to: 632-632

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/dev/ohno/legacylink/handler/LegacyPacketHandler.java` around
lines 107 - 112, Add a startup validation that ensures every ID in
LEGACY_UNSUPPORTED_PARTICLE_IDS has an explicit entry in
LEGACY_PARTICLE_FALLBACKS so new unsupported IDs cannot silently default to
SMOKE; implement this check in a static initializer or a startup method in
LegacyPacketHandler (e.g., validateFallbacksOnStartup) and have it log and/or
throw an error listing any missing keys (referencing LEGACY_PARTICLE_FALLBACKS
and LEGACY_UNSUPPORTED_PARTICLE_IDS) so the build fails or operator is alerted
when coverage drifts.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/main/java/dev/ohno/legacylink/handler/LegacyPacketHandler.java`:
- Around line 107-112: Add a startup validation that ensures every ID in
LEGACY_UNSUPPORTED_PARTICLE_IDS has an explicit entry in
LEGACY_PARTICLE_FALLBACKS so new unsupported IDs cannot silently default to
SMOKE; implement this check in a static initializer or a startup method in
LegacyPacketHandler (e.g., validateFallbacksOnStartup) and have it log and/or
throw an error listing any missing keys (referencing LEGACY_PARTICLE_FALLBACKS
and LEGACY_UNSUPPORTED_PARTICLE_IDS) so the build fails or operator is alerted
when coverage drifts.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5223d559-493f-4cac-afc4-081e63eb891b

📥 Commits

Reviewing files that changed from the base of the PR and between d55387e and 35e5d19.

⛔ Files ignored due to path filters (1)
  • prebuilt/legacylink-0.1.1.jar is excluded by !**/*.jar
📒 Files selected for processing (1)
  • src/main/java/dev/ohno/legacylink/handler/LegacyPacketHandler.java

Fail fast when unsupported particle IDs and fallback mappings drift, and require explicit fallback entries instead of silent SMOKE defaults.

Made-with: Cursor
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/main/java/dev/ohno/legacylink/handler/LegacyPacketHandler.java (1)

571-600: Keep particle filtering tied to the explicit unsupported-id set.

stripSulfurOrModEntry still applies to Registries.PARTICLE_TYPE, so particle filtering can drift away from LEGACY_UNSUPPORTED_PARTICLE_IDS/LEGACY_PARTICLE_FALLBACKS again. A future sulfur-named particle would be removed from registry sync without tripping validateParticleFallbackCoverage() or getting a runtime fallback here.

Possible simplification
-            boolean stripSulfurOrModEntry = !entityTypeRegistry
+            boolean stripSulfurOrModEntry = !entityTypeRegistry
+                    && !particleRegistry
                     && (LegacyLinkConstants.SULFUR_BLOCK_IDS.contains(entryId)
                     || LegacyLinkConstants.SULFUR_ITEM_IDS.contains(entryId)
                     || entryId.equals(LegacyLinkConstants.SULFUR_CAVES_BIOME_ID)
                     || entryId.equals(LegacyLinkConstants.SULFUR_CUBE_ENTITY_ID)
                     || entryId.contains("sulfur"));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/dev/ohno/legacylink/handler/LegacyPacketHandler.java` around
lines 571 - 600, The code allows stripSulfurOrModEntry to affect particle
registries (particleRegistry / Registries.PARTICLE_TYPE), which can remove
sulfur-named particles without using the explicit unsupported set
(LegacyLinkConstants.LEGACY_UNSUPPORTED_PARTICLE_IDS /
LEGACY_PARTICLE_FALLBACKS) and bypass validateParticleFallbackCoverage(); fix by
preventing stripSulfurOrModEntry from applying when particleRegistry is true
(e.g. change the stripSulfurOrModEntry boolean or the enclosing if to exclude
particleRegistry), ensuring particle removal decisions rely only on
strip26_2OnlyParticle and the LEGACY_UNSUPPORTED_PARTICLE_IDS/fallback logic so
validateParticleFallbackCoverage() and runtime fallbacks remain effective.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/main/java/dev/ohno/legacylink/handler/LegacyPacketHandler.java`:
- Around line 662-672: In remapParticleOptionsForLegacy, calling
RegistryRemapper.remapBlockState(currentStateId) can throw and currently aborts
translation; wrap that call (and the subsequent lookup via
Block.BLOCK_STATE_REGISTRY.byId) in a try/catch that handles any runtime
exception by returning the original BlockParticleOption (or null/marker that
triggers the existing unsupported-particle fallback) so a single bad/missing
state does not fail the entire outbound translation; update the method around
the RegistryRemapper.remapBlockState and Block.BLOCK_STATE_REGISTRY.byId usage
to catch exceptions and degrade gracefully.

---

Nitpick comments:
In `@src/main/java/dev/ohno/legacylink/handler/LegacyPacketHandler.java`:
- Around line 571-600: The code allows stripSulfurOrModEntry to affect particle
registries (particleRegistry / Registries.PARTICLE_TYPE), which can remove
sulfur-named particles without using the explicit unsupported set
(LegacyLinkConstants.LEGACY_UNSUPPORTED_PARTICLE_IDS /
LEGACY_PARTICLE_FALLBACKS) and bypass validateParticleFallbackCoverage(); fix by
preventing stripSulfurOrModEntry from applying when particleRegistry is true
(e.g. change the stripSulfurOrModEntry boolean or the enclosing if to exclude
particleRegistry), ensuring particle removal decisions rely only on
strip26_2OnlyParticle and the LEGACY_UNSUPPORTED_PARTICLE_IDS/fallback logic so
validateParticleFallbackCoverage() and runtime fallbacks remain effective.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b7ea1c7a-11a3-4e29-94fb-1423309d37fd

📥 Commits

Reviewing files that changed from the base of the PR and between 35e5d19 and b10e662.

⛔ Files ignored due to path filters (1)
  • prebuilt/legacylink-0.1.1.jar is excluded by !**/*.jar
📒 Files selected for processing (1)
  • src/main/java/dev/ohno/legacylink/handler/LegacyPacketHandler.java

Comment thread src/main/java/dev/ohno/legacylink/handler/LegacyPacketHandler.java
Avoid aborting outbound translation when block particle state remaps fail and ensure sulfur keyword filtering does not apply to particle registries so explicit unsupported particle handling stays authoritative.

Made-with: Cursor
@ohnodev ohnodev merged commit 6b999c2 into main Apr 16, 2026
2 checks passed
@ohnodev ohnodev deleted the fix/legacylink-particle-remap-26_1 branch April 16, 2026 06:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant