Skip to content

Rewrite block entity renderers#22

Draft
BastianBoB wants to merge 27 commits intoDaRealTurtyWurty:mainfrom
BastianBoB:main
Draft

Rewrite block entity renderers#22
BastianBoB wants to merge 27 commits intoDaRealTurtyWurty:mainfrom
BastianBoB:main

Conversation

@BastianBoB
Copy link
Contributor

@BastianBoB BastianBoB commented Aug 17, 2025

Summary by CodeRabbit

  • New Features

    • Rotary Kiln now emits smoke when items collide, adding immersive feedback.
  • Bug Fixes

    • Physics timestep clamped for smoother Rotary Kiln simulation and more stable visuals.
  • Other Changes

    • Rendering pipeline reorganized across many machines for more consistent model, item, and fluid visuals; fluid rendering updated (directional tiling and tank rendering API renamed), which may alter in-world fluid appearance.

BastianBoB and others added 26 commits August 14, 2025 11:14
# Conflicts:
#	src/client/java/dev/turtywurty/industria/renderer/block/CentrifugalConcentratorBlockEntityRenderer.java
Todo: Remove map entry when BE is destroyed
# Conflicts:
#	src/client/java/dev/turtywurty/industria/renderer/block/CentrifugalConcentratorBlockEntityRenderer.java
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
…KilnBlockEntityRenderer.java

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
…KilnBlockEntityRenderer.java

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Added item and particles to Centrifuge BER
# Conflicts:
#	src/client/java/dev/turtywurty/industria/renderer/block/CentrifugalConcentratorBlockEntityRenderer.java
#	src/client/java/dev/turtywurty/industria/renderer/block/RotaryKilnBlockEntityRenderer.java
#	src/main/java/dev/turtywurty/industria/blockentity/RotaryKilnControllerBlockEntity.java
@coderabbitai
Copy link

coderabbitai bot commented Aug 17, 2025

Walkthrough

Added a base renderer API (renderModel and matrixStackToWorldPosition), renamed/redirected many block renderers to a renderModel entry point, refactored fluid rendering APIs, added collision-driven smoke to the rotary kiln, and made a small WrappedFluidStorage deserialization fallback change.

Changes

Cohort / File(s) Summary
Base renderer & utilities
src/client/java/dev/turtywurty/industria/renderer/block/IndustriaBlockEntityRenderer.java
Added abstract protected renderModel(...), added matrixStackToWorldPosition(MatrixStack) and reorganized render flow to call renderModel, onRender, optional wireframe and postRender. setupBlockEntityTransformations no longer applies 180° X rotation.
In-world fluid rendering API
src/client/java/dev/turtywurty/industria/util/InWorldFluidRenderingComponent.java
Renamed public render(...) overloads to renderFluidTank(...), added new public renderFace(...), replaced per-face tiling with a directional tiling pipeline and defaulted debug/water variant fill logic.
Wrapped fluid deserialization
src/main/java/dev/turtywurty/industria/blockentity/util/fluid/WrappedFluidStorage.java
readData now uses orElse(FluidVariant.blank()) for missing Fluid data instead of orElseThrow().
Many block renderers — renderModel hook & base class change
src/client/.../renderer/block/*
ArcFurnaceBlockEntityRenderer.java, ClarifierBlockEntityRenderer.java, CrusherBlockEntityRenderer.java, CrystallizerBlockEntityRenderer.java, DigesterBlockEntityRenderer.java, DrillBlockEntityRenderer.java, ElectrolyzerBlockEntityRenderer.java, FluidTankBlockEntityRenderer.java, MixerBlockEntityRenderer.java, MotorBlockEntityRenderer.java, MultiblockIOBlockEntityRenderer.java, OilPumpJackBlockEntityRenderer.java, ShakingTableBlockEntityRenderer.java, UpgradeStationBlockEntityRenderer.java, WindTurbineBlockEntityRenderer.java
Introduced/added protected renderModel(...) in many renderers; several classes now extend IndustriaBlockEntityRenderer instead of implementing BlockEntityRenderer; most renderers moved model rendering into renderModel and left onRender as a post-render hook (often no-op).
Centrifugal concentrator refinements
src/client/java/dev/turtywurty/industria/renderer/block/CentrifugalConcentratorBlockEntityRenderer.java
Removed unused imports, added protected renderModel(...) to encapsulate model rendering (bowl rotation), repurposed onRender to compute fluid yOffset via refactored renderInputFluid(...) (now returns yOffset), added renderInputItems(...), switched to matrixStackToWorldPosition(...).
Rotary kiln — physics collision smoke
src/client/java/dev/turtywurty/industria/renderer/block/RotaryKilnBlockEntityRenderer.java
Added Box2D ContactListener and a synchronized collidedBodies set (RendererData) to track item-body collisions, tag item bodies with userData "item", clamp physics step delta, and spawn SMOKE particles at item world positions on collision; renamed render entry to renderModel and adjusted matrixStack usage.

Sequence Diagram(s)

sequenceDiagram
    participant RendererBase as IndustriaBlockEntityRenderer
    participant SubRenderer as Concrete Renderer
    participant Camera as Camera

    RendererBase->>SubRenderer: render(..., matrices, ...)
    note right of RendererBase: push matrices, setupBlockEntityTransformations
    RendererBase->>SubRenderer: renderModel(entity, tickDelta, matrices,...)
    SubRenderer-->>RendererBase: model drawing
    RendererBase->>SubRenderer: onRender(entity, tickDelta, matrices,...)
    note right of RendererBase: wireframe if looked-at, postRender, pop
Loading
sequenceDiagram
    participant GameLoop as Game Loop
    participant KilnRenderer as RotaryKilnBlockEntityRenderer
    participant Box2D as Physics World (RendererData)
    participant Contact as ContactListener
    participant World as Minecraft World

    GameLoop->>KilnRenderer: render(...)
    KilnRenderer->>Box2D: stepPhysics(clamped delta)
    Box2D->>Contact: beginContact(item ↔ any)
    Contact-->>Box2D: collidedBodies.add(itemBody)
    KilnRenderer->>Box2D: for each itemBody in collidedBodies
    alt collision detected
        Box2D->>KilnRenderer: remove body from set
        KilnRenderer->>World: addParticle(SMOKE, worldPosition)
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

I hop through matrices, nose to sky,
I map the origin so sprites can fly.
When kiln bits bump, I puff a smoke bloom—
Little clouds dance in renderer room.
Carrot cheers for functions new and spry. 🥕

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@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 (4)
src/client/java/dev/turtywurty/industria/renderer/block/IndustriaBlockEntityRenderer.java (1)

101-112: Optional: accept cameraPos to avoid client lookup and improve testability.

Minor improvement: overload this helper to accept a cameraPos argument (and have the current method delegate). This avoids repeated MinecraftClient lookups and makes the method usable in non-standard render contexts.

Example:

// Overload
protected Vector3f matrixStackToWorldPosition(MatrixStack matrices, Vec3d cameraPos) {
    Vector3f pos = matrices.peek().getPositionMatrix().transformPosition(0, 0, 0, new Vector3f());
    return new Vector3f((float) (pos.x() + cameraPos.x), (float) (pos.y() + cameraPos.y), (float) (pos.z() + cameraPos.z));
}

// Current method delegates
protected Vector3f matrixStackToWorldPosition(MatrixStack matrices) {
    Vec3d cam = MinecraftClient.getInstance().gameRenderer.getCamera().getPos();
    return matrixStackToWorldPosition(matrices, cam);
}
src/client/java/dev/turtywurty/industria/renderer/block/CentrifugalConcentratorBlockEntityRenderer.java (1)

113-115: Throttle particle emission to avoid excessive client-side load.

As written, each frame spawns NUM_SPINNING_ITEMS bubbles. Consider a simple time-based throttle so effects remain visible without overwhelming particles.

Apply within this hunk:

-            Vector3f pos = matrixStackToWorldPosition(matrices);
-            entity.getWorld().addParticleClient(ParticleTypes.BUBBLE, pos.x, pos.y + 0.25, pos.z, 0, 0, 0);
+            // Emit roughly every 3 ticks per item; offset by i to stagger
+            if ((entity.getWorld().getTime() + i) % 3 == 0) {
+                Vector3f pos = matrixStackToWorldPosition(matrices);
+                entity.getWorld().addParticleClient(ParticleTypes.BUBBLE, pos.x, pos.y + 0.25, pos.z, 0, 0, 0);
+            }
src/client/java/dev/turtywurty/industria/renderer/block/RotaryKilnBlockEntityRenderer.java (2)

118-121: Collision-driven smoke is good; consider tying intensity to collision strength.

Currently, every begin-contact triggers one smoke puff. You can make effects feel more physical by emitting only for impactful contacts (or emitting more smoke for larger impulses).

Option A (minimal): gate puffs by linear speed

if (rendererData.collidedBodies.remove(body)) {
    Vec2 v = body.getLinearVelocity();
    if (v.lengthSquared() > 0.25f) { // speed > 0.5 m/s
        Vector3f position = matrixStackToWorldPosition(matrices);
        entity.getWorld().addParticleClient(ParticleTypes.SMOKE, position.x, position.y, position.z, 0, 0, 0);
    }
}

Option B (preferred): use contact impulse in postSolve (see next comment) to decide when to add to collidedBodies based on an impulse threshold.


174-195: Use postSolve impulse to gate smoke on “significant” collisions.

Leverage ContactImpulse to add bodies to collidedBodies only when the normal impulse exceeds a threshold. This avoids puffs from grazing contacts.

Apply within this hunk:

             box2dWorld.setContactListener(new ContactListener() {
                 @Override
                 public void beginContact(Contact contact) {
-                    Body a = contact.getFixtureA().getBody();
-                    Body b = contact.getFixtureB().getBody();
-
-                    if (a.getUserData() != null && a.getUserData().equals("item"))
-                        collidedBodies.add(a);
-
-                    if (b.getUserData() != null && b.getUserData().equals("item"))
-                        collidedBodies.add(b);
+                    // No-op: we’ll decide in postSolve based on impulse magnitude
                 }
 
                 @Override
                 public void endContact(Contact contact) {}
 
                 @Override
                 public void preSolve(Contact contact, Manifold oldManifold) {}
 
                 @Override
-                public void postSolve(Contact contact, ContactImpulse impulse) {}
+                public void postSolve(Contact contact, ContactImpulse impulse) {
+                    // Compute max normal impulse across contact points
+                    float maxImpulse = 0f;
+                    float[] normals = impulse.normalImpulses;
+                    for (int i = 0; i < normals.length; i++) {
+                        maxImpulse = Math.max(maxImpulse, normals[i]);
+                    }
+
+                    // Threshold tuneable; ~0.75–1.5 feels reasonable
+                    if (maxImpulse > 1.0f) {
+                        Body a = contact.getFixtureA().getBody();
+                        Body b = contact.getFixtureB().getBody();
+                        if ("item".equals(a.getUserData())) collidedBodies.add(a);
+                        if ("item".equals(b.getUserData())) collidedBodies.add(b);
+                    }
+                }
             });

This produces smoke when collisions are “hard” enough and avoids visual noise.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 43d33d3 and d3fc8c0.

📒 Files selected for processing (3)
  • src/client/java/dev/turtywurty/industria/renderer/block/CentrifugalConcentratorBlockEntityRenderer.java (1 hunks)
  • src/client/java/dev/turtywurty/industria/renderer/block/IndustriaBlockEntityRenderer.java (1 hunks)
  • src/client/java/dev/turtywurty/industria/renderer/block/RotaryKilnBlockEntityRenderer.java (5 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build (21)
🔇 Additional comments (4)
src/client/java/dev/turtywurty/industria/renderer/block/IndustriaBlockEntityRenderer.java (1)

101-112: LGTM: Shared world-position helper is correct and useful.

Transforming the local origin and adding the camera position is the right way to derive a world-space coordinate from the current MatrixStack state in BE renderers. Good extraction for reuse.

src/client/java/dev/turtywurty/industria/renderer/block/CentrifugalConcentratorBlockEntityRenderer.java (1)

113-115: LGTM: Using the shared helper removes duplication and clarifies intent.

The switch to matrixStackToWorldPosition(matrices) is correct and aligns with the new base utility.

src/client/java/dev/turtywurty/industria/renderer/block/RotaryKilnBlockEntityRenderer.java (2)

86-87: LGTM: Clamping deltaTime stabilizes the Box2D simulation.

The [0.01, 0.1]s clamp guards against tunneling and unstable steps on slow/fast frames.


137-138: LGTM: Tagging item bodies with userData simplifies contact filtering.

Marking item bodies with "item" is a clean, low-overhead way to identify them in the contact listener.

private final Body barrelBody;

private long lastRenderTime = System.nanoTime();
private final Set<Body> collidedBodies = Collections.synchronizedSet(new HashSet<>());
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix memory leak: collidedBodies can retain destroyed Body references.

Bodies removed from recipeToBodyMap and destroyed are never pruned from collidedBodies, so those Body references leak. Ensure you remove the body from collidedBodies when destroying and periodically prune stale entries.

Proposed fixes (outside this hunk):

  • When destroying a body (in renderItems(), during map cleanup), also drop it from collidedBodies:
if (!entity.getRecipes().contains(entry.getKey())) {
    rendererData.collidedBodies.remove(entry.getValue()); // prune from signal set
    box2dWorld.destroyBody(entry.getValue());
    iterator.remove();
}
  • Additionally, add a safety prune early in renderItems() after you get recipeToBodyMap:
rendererData.collidedBodies.retainAll(recipeToBodyMap.values());

These two changes prevent unbounded growth in collidedBodies across recipe churn.

🤖 Prompt for AI Agents
In
src/client/java/dev/turtywurty/industria/renderer/block/RotaryKilnBlockEntityRenderer.java
around line 170, collidedBodies currently holds Body references that are
destroyed/removed from recipeToBodyMap causing a memory leak; when you destroy
or remove a body you must also remove it from collidedBodies (call
collidedBodies.remove(body) at the same point you destroy the body and remove it
from the map), and additionally prune stale entries at the start of
renderItems() after obtaining recipeToBodyMap by retaining only current bodies
(call collidedBodies.retainAll(recipeToBodyMap.values())) to prevent unbounded
growth.

Started rework in IndustriaBlockEntityRenderer to have 2 render functions for flipped model and the rest
Copy link

@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: 9

🔭 Outside diff range comments (1)
src/client/java/dev/turtywurty/industria/renderer/block/CentrifugalConcentratorBlockEntityRenderer.java (1)

31-49: Rotation math mixes units (revolutions vs radians) and will render incorrectly.

entity.bowlRotation is used as an angle (radians) later (e.g., with Math.TAU and as yaw), but it is incremented in revolutions and wrapped by 360f (degrees). This produces wrong rotation speeds and jitter across frames.

Fix by accumulating radians and wrapping by τ:

-        if (progress == 0 || Double.isNaN(progress)) {
-            entity.bowlRotation = 0f;
-        } else {
-            entity.bowlRotation = (entity.bowlRotation + (rpm / 60f / 20f) * tickDelta) % 360f;
-        }
+        if (progress == 0 || Double.isNaN(progress)) {
+            entity.bowlRotation = 0f;
+        } else {
+            // rpm = revolutions per minute; per tick = rpm / 1200; convert to radians and scale by tickDelta
+            float deltaRad = (rpm / 60f) * ((float) Math.TAU) / 20f * tickDelta;
+            entity.bowlRotation = (entity.bowlRotation + deltaRad) % (float) Math.TAU;
+        }
🧹 Nitpick comments (23)
src/client/java/dev/turtywurty/industria/renderer/block/IndustriaBlockEntityRenderer.java (1)

110-121: Use the already-available cameraPos to avoid global lookups and improve correctness.

matrixStackToWorldPosition queries the camera via MinecraftClient even though render(...) already receives cameraPos. Prefer an overload that takes cameraPos (and have the current method delegate), which also makes the utility usable in non-standard call sites.

Suggested addition:

protected Vector3f matrixStackToWorldPosition(MatrixStack matrices, Vec3d cameraPos) {
    Vector3f pos = matrices.peek().getPositionMatrix().transformPosition(0, 0, 0, new Vector3f());
    return new Vector3f((float) (pos.x() + cameraPos.x), (float) (pos.y() + cameraPos.y), (float) (pos.z() + cameraPos.z));
}

// Backward-compatible delegate
protected Vector3f matrixStackToWorldPosition(MatrixStack matrices) {
    Vec3d cameraPos = MinecraftClient.getInstance().gameRenderer.getCamera().getPos();
    return matrixStackToWorldPosition(matrices, cameraPos);
}
src/client/java/dev/turtywurty/industria/renderer/block/MultiblockIOBlockEntityRenderer.java (1)

72-85: Avoid per-frame allocations for colors

getColor creates a new float[] on every call. Cache constant colors to reduce GC pressure when hitboxes are visible.

Proposed approach (add these constants near the top of the class):

private static final float[] COLOR_ITEM   = {0.0F, 0.0F, 0.0F};
private static final float[] COLOR_ENERGY = {1.0F, 1.0F, 51 / 255F};
private static final float[] COLOR_FLUID  = {135 / 255F, 206 / 255F, 250 / 255F};
private static final float[] COLOR_SLURRY = {139 / 255F, 69 / 255F, 19 / 255F};
private static final float[] COLOR_HEAT   = {1.0F, 127 / 255F, 80 / 255F};
private static final float[] COLOR_GAS    = {58 / 255F, 159 / 255F, 2 / 255F};

Then change getColor to return these constants:

-        if (type == TransferType.ITEM) {
-            return new float[]{0.0F, 0.0F, 0.0F};
-        } else if (type == TransferType.ENERGY) {
-            return new float[]{1.0F, 1.0F, 51 / 255F};
-        } else if (type == TransferType.FLUID) {
-            return new float[]{135 / 255F, 206 / 255F, 250 / 255F};
-        } else if (type == TransferType.SLURRY) {
-            return new float[]{139 / 255F, 69 / 255F, 19 / 255F};
-        } else if (type == TransferType.HEAT) {
-            return new float[]{1.0F, 127 / 255F, 80 / 255F};
-        } else if (type == TransferType.GAS) {
-            return new float[]{58 / 255F, 159 / 255F, 2 / 255F};
-        }
+        if (type == TransferType.ITEM) {
+            return COLOR_ITEM;
+        } else if (type == TransferType.ENERGY) {
+            return COLOR_ENERGY;
+        } else if (type == TransferType.FLUID) {
+            return COLOR_FLUID;
+        } else if (type == TransferType.SLURRY) {
+            return COLOR_SLURRY;
+        } else if (type == TransferType.HEAT) {
+            return COLOR_HEAT;
+        } else if (type == TransferType.GAS) {
+            return COLOR_GAS;
+        }
src/client/java/dev/turtywurty/industria/renderer/block/MotorBlockEntityRenderer.java (1)

23-30: Restore spinRod pitch to previous value and keep code consistent

Small robustness/readability improvement: save and restore the previous pitch instead of hard-coding 0. Also prefer using this.model consistently.

Apply this diff:

     protected void renderModel(MotorBlockEntity entity, float tickDelta, MatrixStack matrices, VertexConsumerProvider vertexConsumers, int light, int overlay) {
         float rotationSpeed = entity.getRotationSpeed();
         entity.rodRotation += rotationSpeed * tickDelta;
 
-        this.model.getMotorParts().spinRod().pitch = entity.rodRotation;
-        model.render(matrices, vertexConsumers.getBuffer(model.getLayer(MotorModel.TEXTURE_LOCATION)), light, overlay);
-        this.model.getMotorParts().spinRod().pitch = 0;
+        float prevSpinRodPitch = this.model.getMotorParts().spinRod().pitch;
+        this.model.getMotorParts().spinRod().pitch = entity.rodRotation;
+        this.model.render(matrices, vertexConsumers.getBuffer(this.model.getLayer(MotorModel.TEXTURE_LOCATION)), light, overlay);
+        this.model.getMotorParts().spinRod().pitch = prevSpinRodPitch;
     }

Optional: bound entity.rodRotation to [0, 2π) each frame to avoid unbounded growth over long sessions:

entity.rodRotation = (entity.rodRotation % ((float) (Math.PI * 2)));
src/client/java/dev/turtywurty/industria/renderer/block/CrusherBlockEntityRenderer.java (1)

27-47: Model state save/restore and progressive roll — good

Capturing and restoring roll on all four parts prevents state bleed between frames. Using entity.getProgress()/100.0F is clear.

If getProgress can exceed 100 or be negative, consider clamping to [0, 100] before dividing to keep roll in a predictable range.

src/client/java/dev/turtywurty/industria/renderer/block/DrillBlockEntityRenderer.java (4)

62-80: Clarify motor rotation update and avoid compound assignment gotchas

The chained assignment obscures intent. Compute delta explicitly, update clientMotorRotation, then apply to parts. This keeps behavior and improves readability.

Apply this diff:

-        if (!entity.isDrilling() && !entity.isRetracting()) {
-            entity.clientMotorRotation = 0;
-        }
-
-        parts.rodGear().pitch += entity.clientMotorRotation += (entity.isDrilling() ? 0.03f : entity.isRetracting() ? -0.03f : 0);
-        parts.connectingGear().pitch = -entity.clientMotorRotation;
+        if (!entity.isDrilling() && !entity.isRetracting()) {
+            entity.clientMotorRotation = 0f;
+        }
+        float delta = entity.isDrilling() ? 0.03f : (entity.isRetracting() ? -0.03f : 0f);
+        entity.clientMotorRotation += delta;
+        parts.rodGear().pitch += entity.clientMotorRotation;
+        parts.connectingGear().pitch = -entity.clientMotorRotation;

Optional: consider bounding entity.clientMotorRotation to [0, 2π) to avoid runaway values over time:

entity.clientMotorRotation = (float) (entity.clientMotorRotation % (Math.PI * 2));

95-110: Restore cable scaling using previous absolute scales to prevent drift

Using +/- cableScaleFactor assumes a known base scale. Save and restore exact scales to avoid cumulative floating-point drift or unexpected base changes.

Apply this diff:

-        float prevCableMainPitch = cableMain.pitch;
-        cableMain.pitch = entity.clientMotorRotation;
-        float cableScaleFactor = 0.5f - (progress / 2f);
-
-        cableMain.xScale -= cableScaleFactor;
-        cableMain.yScale -= cableScaleFactor;
-        cableMain.zScale -= cableScaleFactor;
+        float prevCableMainPitch = cableMain.pitch;
+        cableMain.pitch = entity.clientMotorRotation;
+        float cableScaleFactor = 0.5f - (progress / 2f);
+        float prevXScale = cableMain.xScale, prevYScale = cableMain.yScale, prevZScale = cableMain.zScale;
+        cableMain.xScale = prevXScale - cableScaleFactor;
+        cableMain.yScale = prevYScale - cableScaleFactor;
+        cableMain.zScale = prevZScale - cableScaleFactor;
         this.cableModel.render(matrices, vertexConsumers.getBuffer(this.cableModel.getLayer(DrillCableModel.TEXTURE_LOCATION)), light, overlay);
-        cableMain.xScale += cableScaleFactor;
-        cableMain.yScale += cableScaleFactor;
-        cableMain.zScale += cableScaleFactor;
+        cableMain.xScale = prevXScale;
+        cableMain.yScale = prevYScale;
+        cableMain.zScale = prevZScale;
         cableMain.pitch = prevCableMainPitch;

121-130: Simplify angleOffset logic (nested ternary is redundant)

Both branches of the outer ternary yield the same outcome; the logic can be reduced for clarity without changing behavior.

Apply this diff:

-        float angleOffset = (float) (entity.isRetracting() ?
-                -entity.clientMotorRotation < 0 ? -Math.PI / 4 : -Math.PI / 4 - Math.PI / 2 :
-                -entity.clientMotorRotation < 0 ? -3 * Math.PI / 4 + Math.PI / 2 : -3 * Math.PI / 4);
+        float quarterPi = (float) (Math.PI / 4.0);
+        float angleOffset = (-entity.clientMotorRotation < 0) ? -quarterPi : -3.0f * quarterPi;
 
-        float angle = (float) (-entity.clientMotorRotation % (Math.PI / 2f)) + angleOffset;
+        float angle = (float) ((-entity.clientMotorRotation) % (Math.PI / 2.0)) + angleOffset;

156-159: onRender visibility differs from other renderers

This override is public while others are protected. It compiles (wider visibility is allowed), but consider making it protected for consistency with the base API.

src/client/java/dev/turtywurty/industria/renderer/block/CentrifugalConcentratorBlockEntityRenderer.java (1)

89-129: Clamp fill percent and minor robustness.

  • Add a clamp to keep fillPercent within [0,1] to avoid overflow rendering on sync glitches.
  • Early returns are fine, but also consider guarding entity.getWorld() to avoid NPE when computing fluidColor.
-        float fillPercent = fluidTank.amount / (float) fluidTank.getCapacity();
+        float fillPercent = MathHelper.clamp(fluidTank.amount / (float) fluidTank.getCapacity(), 0f, 1f);
@@
-        int fluidColor = FluidVariantRendering.getColor(fluidVariant, entity.getWorld(), entity.getPos());
+        int fluidColor = FluidVariantRendering.getColor(fluidVariant, entity.getWorld(), entity.getPos());

(If entity.getWorld() can be null during early client init, add a null check and default color.)

src/client/java/dev/turtywurty/industria/renderer/block/WindTurbineBlockEntityRenderer.java (1)

25-34: Propeller rotation is frame-rate dependent; incorporate tickDelta or time-based rotation.

Accumulating a fixed amount per render call speeds up with FPS. Use tickDelta or world time to stabilize.

-        float outputPercentage = getEnergyPerTickPercent(entity);
-        entity.setPropellerRotation(entity.getPropellerRotation() + (outputPercentage * 0.25f));
+        float outputPercentage = getEnergyPerTickPercent(entity);
+        float delta = outputPercentage * 0.25f * tickDelta; // scale by partial tick
+        entity.setPropellerRotation((entity.getPropellerRotation() + delta) % (float) Math.TAU);
src/client/java/dev/turtywurty/industria/renderer/block/OilPumpJackBlockEntityRenderer.java (1)

30-76: Client rotation accumulates per frame; stabilize by ticks or time.

clientRotation += 0.1f * tickDelta inside render accumulates based on render calls, not ticks, causing FPS-dependent speed and inconsistent 2π crossing logic for reverseCounterWeights.

Two options:

  • Minimum change: scale by ticks using world time and keep crossing detection.
-        if (entity.isRunning()) {
-            clientRotation = clientRotation + 0.1f * tickDelta;
+        if (entity.isRunning()) {
+            // Advance by a fixed rad/tick scaled by tickDelta. Consider moving this to a client tick instead.
+            clientRotation = clientRotation + 0.1f * tickDelta;
             if (clientRotation > Math.PI * 2) {
                 clientRotation -= (float) (Math.PI * 2);
                 entity.reverseCounterWeights = !entity.reverseCounterWeights;
             }
             entity.clientRotation = clientRotation;
         }
  • Better: update clientRotation on a per-tick client-side hook (block entity client tick) and only interpolate in renderer.

If you want, I can provide a small client-tick updater to keep renderer purely presentational.

src/client/java/dev/turtywurty/industria/renderer/block/FluidTankBlockEntityRenderer.java (1)

19-22: Empty renderModel hook — add a brief note or remove override if intentionally unused

If this renderer has no model to draw (fluid-only), consider adding a short comment to clarify intent or remove the override to avoid confusion for future readers.

Apply this diff to add an intent comment:

 @Override
 protected void renderModel(FluidTankBlockEntity entity, float tickDelta, MatrixStack matrices, VertexConsumerProvider vertexConsumers, int light, int overlay) {
-
+    // Intentionally left empty: fluid tank renders via InWorldFluidRenderingComponent only.
 }
src/client/java/dev/turtywurty/industria/renderer/block/ShakingTableBlockEntityRenderer.java (3)

29-37: Use tickDelta and proper angular frequency (2πf) for smooth, FPS-independent shaking

Current code uses world time in ticks and πf, which both skews the frequency (by ~2x) and ignores tickDelta. Compute using seconds and 2πf, and handle null world gracefully.

Apply this diff:

-private float getShakeOffset(ShakingTableBlockEntity entity) {
-    float shakesPerSecond = entity.getRecipeFrequency();
-    float time = entity.getWorld().getTime();
-    float frequency = shakesPerSecond * (float) Math.PI;
-    float shakeAmount = 2f;
-    return (float) Math.sin(time * frequency) * shakeAmount;
-}
+private float getShakeOffset(ShakingTableBlockEntity entity, float tickDelta) {
+    var world = entity.getWorld();
+    if (world == null) return 0f;
+
+    float shakesPerSecond = entity.getRecipeFrequency(); // f in Hz
+    float timeSeconds = (world.getTime() + tickDelta) / 20.0f; // ticks -> seconds
+    float angularFrequency = (float) (Math.PI * 2.0) * shakesPerSecond; // ω = 2πf
+    float shakeAmount = 2f;
+    return MathHelper.sin(timeSeconds * angularFrequency) * shakeAmount;
+}

Note: This switches to MathHelper.sin for consistency with other math in this class.


46-48: Update call site to pass tickDelta to getShakeOffset

Apply this diff:

-        if (progress > 0 && progress < maxProgress) {
-            this.model.getModelParts().table().originZ += getShakeOffset(entity);
-        }
+        if (progress > 0 && progress < maxProgress) {
+            this.model.getModelParts().table().originZ += getShakeOffset(entity, tickDelta);
+        }

55-57: Pass tickDelta through for consistent shake application across all sub-renders

Apply this diff:

-    protected void onRender(ShakingTableBlockEntity entity, float tickDelta, MatrixStack matrices, VertexConsumerProvider vertexConsumers, int light, int overlay) {
-        float shakeOffset = getShakeOffset(entity);
+    protected void onRender(ShakingTableBlockEntity entity, float tickDelta, MatrixStack matrices, VertexConsumerProvider vertexConsumers, int light, int overlay) {
+        float shakeOffset = getShakeOffset(entity, tickDelta);
src/client/java/dev/turtywurty/industria/renderer/block/MixerBlockEntityRenderer.java (3)

27-33: Avoid frame-rate–dependent stirring rotation; derive yaw from world time

Mutating entity.stirringRotation in the renderer ties speed to frame rate. Compute yaw from world time (with tickDelta) for determinism and remove the side effect. Also gate rotation on isMixing to avoid spinning when idle.

Apply this diff:

-@Override
-protected void renderModel(MixerBlockEntity entity, float tickDelta, MatrixStack matrices, VertexConsumerProvider vertexConsumers, int light, int overlay) {
-    this.model.getMixerParts().stirring_rods().yaw = entity.stirringRotation;
-    this.model.getMixerParts().main().render(matrices, vertexConsumers.getBuffer(this.model.getLayer(MixerModel.TEXTURE_LOCATION)), light, overlay);
-    this.model.getMixerParts().stirring_rods().yaw = 0.0F;
-}
+@Override
+protected void renderModel(MixerBlockEntity entity, float tickDelta, MatrixStack matrices, VertexConsumerProvider vertexConsumers, int light, int overlay) {
+    var world = entity.getWorld();
+    float time = world != null ? world.getTime() + tickDelta : 0f;  // ticks + partials
+    boolean isMixing = entity.isMixing();
+    float rotationPerTick = 0.1f; // radians per tick (matches previous visual speed)
+    float yaw = isMixing ? time * rotationPerTick : 0f;
+
+    this.model.getMixerParts().stirring_rods().yaw = yaw;
+    this.model.getMixerParts().main().render(matrices, vertexConsumers.getBuffer(this.model.getLayer(MixerModel.TEXTURE_LOCATION)), light, overlay);
+    this.model.getMixerParts().stirring_rods().yaw = 0.0F;
+}

35-41: Remove per-frame mutation of entity.stirringRotation in the renderer

Once yaw derives from time, this side effect is no longer needed and avoids FPS dependence.

Apply this diff:

-    boolean isMixing = entity.isMixing();
-    if (isMixing) {
-        entity.stirringRotation = (entity.stirringRotation + 0.1f) % 360;
-    }
+    boolean isMixing = entity.isMixing();

85-91: Rotation units: rotation() expects radians; confirm intended spin rate

You’re passing worldTime * 0.25f into rotation(), which is radians, i.e., ~14.3° per tick (~286°/s). If you intended degrees, use rotationDegrees(...). Otherwise, all good.

Apply this alternative if degrees were intended:

-    matrices.multiply(RotationAxis.POSITIVE_Y.rotation(entity.getWorld().getTime() * 0.25f));
-    matrices.multiply(RotationAxis.POSITIVE_X.rotation(entity.getWorld().getTime() * 0.25f));
-    matrices.multiply(RotationAxis.POSITIVE_Z.rotation(entity.getWorld().getTime() * 0.25f));
+    float deg = entity.getWorld().getTime() * 0.25f;
+    matrices.multiply(RotationAxis.POSITIVE_Y.rotationDegrees(deg));
+    matrices.multiply(RotationAxis.POSITIVE_X.rotationDegrees(deg));
+    matrices.multiply(RotationAxis.POSITIVE_Z.rotationDegrees(deg));
src/client/java/dev/turtywurty/industria/renderer/block/ClarifierBlockEntityRenderer.java (5)

71-92: Hard-coded output stack (DIAMOND_BLOCK x64) — replace with actual state or gate behind debug
This looks like placeholder/debug content. Consider wiring it to entity.getOutputInventory() (or similar) before merge, or annotate with a clear TODO and debug flag to avoid shipping obvious placeholders.

If you want, I can help wire this to the real inventory API and keep a fallback for empty stacks.


95-137: Current output item uses world-time animation and DIAMOND_BLOCK — hook into real progress
This is a nice animation scaffold, but “nextOutput” and “itemProgress” should come from the block entity’s actual next output and processing progress. Passing tickDelta through allows smooth interpolation.

Proposed changes:

  • Pass tickDelta as a parameter into renderCurrentOutputItem.
  • Replace world-time progress with (float) entity.getProgress() / entity.getMaxProgress().
  • Replace hard-coded item with entity.getNextOutputItemStack().

I can provide a patch if you confirm the appropriate getters.


139-162: Input fluid rendering is commented out and replaced with a water placeholder — migrate to the new API
The previous top-face/tiled calls are commented. Align with the new InWorldFluidRenderingComponent API used elsewhere (renderFluidTank(...), or the new directional method if available) and drive the variant/amount from the entity’s SyncingFluidStorage.

Would you like me to draft the migration to renderFluidTank with the correct bounds for the input basin?


164-194: Output fluid rendering likewise uses a placeholder — re-enable real output fluid
Same as input fluid: re-enable using the actual output tank and migrate to the new API method rather than leaving placeholders/comments.

As a checklist:

  • Use entity.getOutputFluidTank()
  • Guard for isResourceBlank/amount <= 0
  • Compute fluid height from capacity and amount
  • Call renderFluidTank(...) with the clarifier’s output channel bounds

101-133: Consider tickDelta for item animation smoothing
If you keep time-based animation here, using (world.getTime() + tickDelta) will reduce judder at low FPS.

Apply this diff to the progress calculation:

-        float itemProgress = (blockEntity.getWorld().getTime() / 100f) % 1;
+        float itemProgress = ((blockEntity.getWorld().getTime() + tickDelta) / 100f) % 1;

Note: This requires passing tickDelta into renderCurrentOutputItem.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between d3fc8c0 and 35c0a6e.

📒 Files selected for processing (20)
  • src/client/java/dev/turtywurty/industria/renderer/block/ArcFurnaceBlockEntityRenderer.java (1 hunks)
  • src/client/java/dev/turtywurty/industria/renderer/block/CentrifugalConcentratorBlockEntityRenderer.java (2 hunks)
  • src/client/java/dev/turtywurty/industria/renderer/block/ClarifierBlockEntityRenderer.java (5 hunks)
  • src/client/java/dev/turtywurty/industria/renderer/block/CrusherBlockEntityRenderer.java (2 hunks)
  • src/client/java/dev/turtywurty/industria/renderer/block/CrystallizerBlockEntityRenderer.java (2 hunks)
  • src/client/java/dev/turtywurty/industria/renderer/block/DigesterBlockEntityRenderer.java (1 hunks)
  • src/client/java/dev/turtywurty/industria/renderer/block/DrillBlockEntityRenderer.java (2 hunks)
  • src/client/java/dev/turtywurty/industria/renderer/block/ElectrolyzerBlockEntityRenderer.java (1 hunks)
  • src/client/java/dev/turtywurty/industria/renderer/block/FluidTankBlockEntityRenderer.java (2 hunks)
  • src/client/java/dev/turtywurty/industria/renderer/block/IndustriaBlockEntityRenderer.java (3 hunks)
  • src/client/java/dev/turtywurty/industria/renderer/block/MixerBlockEntityRenderer.java (2 hunks)
  • src/client/java/dev/turtywurty/industria/renderer/block/MotorBlockEntityRenderer.java (1 hunks)
  • src/client/java/dev/turtywurty/industria/renderer/block/MultiblockIOBlockEntityRenderer.java (3 hunks)
  • src/client/java/dev/turtywurty/industria/renderer/block/OilPumpJackBlockEntityRenderer.java (3 hunks)
  • src/client/java/dev/turtywurty/industria/renderer/block/RotaryKilnBlockEntityRenderer.java (7 hunks)
  • src/client/java/dev/turtywurty/industria/renderer/block/ShakingTableBlockEntityRenderer.java (7 hunks)
  • src/client/java/dev/turtywurty/industria/renderer/block/UpgradeStationBlockEntityRenderer.java (1 hunks)
  • src/client/java/dev/turtywurty/industria/renderer/block/WindTurbineBlockEntityRenderer.java (1 hunks)
  • src/client/java/dev/turtywurty/industria/util/InWorldFluidRenderingComponent.java (2 hunks)
  • src/main/java/dev/turtywurty/industria/blockentity/util/fluid/WrappedFluidStorage.java (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/client/java/dev/turtywurty/industria/renderer/block/RotaryKilnBlockEntityRenderer.java
🧰 Additional context used
🧬 Code Graph Analysis (13)
src/client/java/dev/turtywurty/industria/renderer/block/DigesterBlockEntityRenderer.java (1)
src/client/java/dev/turtywurty/industria/model/DigesterModel.java (1)
  • DigesterModel (13-31)
src/client/java/dev/turtywurty/industria/renderer/block/ElectrolyzerBlockEntityRenderer.java (1)
src/client/java/dev/turtywurty/industria/model/ElectrolyzerModel.java (1)
  • ElectrolyzerModel (13-32)
src/client/java/dev/turtywurty/industria/renderer/block/UpgradeStationBlockEntityRenderer.java (1)
src/client/java/dev/turtywurty/industria/model/UpgradeStationModel.java (1)
  • UpgradeStationModel (13-28)
src/client/java/dev/turtywurty/industria/renderer/block/ShakingTableBlockEntityRenderer.java (1)
src/client/java/dev/turtywurty/industria/model/ShakingTableModel.java (1)
  • ShakingTableModel (9-181)
src/client/java/dev/turtywurty/industria/renderer/block/CrystallizerBlockEntityRenderer.java (1)
src/client/java/dev/turtywurty/industria/model/CrystallizerModel.java (1)
  • CrystallizerModel (9-38)
src/client/java/dev/turtywurty/industria/renderer/block/WindTurbineBlockEntityRenderer.java (1)
src/client/java/dev/turtywurty/industria/model/WindTurbineModel.java (1)
  • WindTurbineModel (9-58)
src/client/java/dev/turtywurty/industria/renderer/block/OilPumpJackBlockEntityRenderer.java (1)
src/client/java/dev/turtywurty/industria/model/OilPumpJackModel.java (1)
  • OilPumpJackModel (9-343)
src/client/java/dev/turtywurty/industria/renderer/block/MotorBlockEntityRenderer.java (1)
src/client/java/dev/turtywurty/industria/model/MotorModel.java (1)
  • MotorModel (9-53)
src/client/java/dev/turtywurty/industria/renderer/block/MultiblockIOBlockEntityRenderer.java (1)
src/main/java/dev/turtywurty/industria/multiblock/TransferType.java (1)
  • TransferType (40-287)
src/client/java/dev/turtywurty/industria/renderer/block/ClarifierBlockEntityRenderer.java (1)
src/main/java/dev/turtywurty/industria/init/list/TagList.java (2)
  • Fluids (30-38)
  • Items (13-20)
src/client/java/dev/turtywurty/industria/util/InWorldFluidRenderingComponent.java (1)
src/main/java/dev/turtywurty/industria/init/list/TagList.java (1)
  • Fluids (30-38)
src/client/java/dev/turtywurty/industria/renderer/block/MixerBlockEntityRenderer.java (1)
src/client/java/dev/turtywurty/industria/model/MixerModel.java (1)
  • MixerModel (9-64)
src/client/java/dev/turtywurty/industria/renderer/block/DrillBlockEntityRenderer.java (3)
src/client/java/dev/turtywurty/industria/model/DrillMotorModel.java (1)
  • DrillMotorModel (9-70)
src/client/java/dev/turtywurty/industria/model/DrillFrameModel.java (1)
  • DrillFrameModel (9-92)
src/client/java/dev/turtywurty/industria/model/DrillCableModel.java (1)
  • DrillCableModel (9-36)
🔇 Additional comments (28)
src/client/java/dev/turtywurty/industria/renderer/block/IndustriaBlockEntityRenderer.java (2)

84-85: New renderModel hook — LGTM.

Good separation of concerns: transforms orchestrated in base render(...), static model draw lives in renderModel(...).


186-196: Javadoc parameter rename: looks good.

The updated parameter names (“modelPart”) improve clarity and match the code.

src/client/java/dev/turtywurty/industria/renderer/block/ElectrolyzerBlockEntityRenderer.java (2)

18-21: Migration to renderModel is correct.

Rendering is now encapsulated in renderModel(...) and onRender(...) is intentionally empty. This aligns with the new base renderer flow.


22-24: Confirm that onRender being empty is intentional.

If any dynamic overlays/effects were previously drawn in onRender(...), they won’t appear now unless moved to renderModel(...) or kept in onRender(...).

src/client/java/dev/turtywurty/industria/renderer/block/DigesterBlockEntityRenderer.java (2)

19-21: Migration to renderModel is correct.

Model rendering moved into renderModel(...) using the model’s root part and correct layer; onRender(...) is intentionally empty.


23-25: Confirm that onRender being empty is intentional.

If there were dynamic extras previously, ensure they are either retained in onRender(...) or folded into renderModel(...).

src/client/java/dev/turtywurty/industria/renderer/block/MultiblockIOBlockEntityRenderer.java (2)

23-26: No-op renderModel override aligns with new renderer lifecycle

Leaving renderModel empty here is fine since this renderer only draws debug visuals in onRender.


30-30: Hitbox guard refactor is fine

Single-line early return improves readability without changing behavior.

src/client/java/dev/turtywurty/industria/renderer/block/UpgradeStationBlockEntityRenderer.java (4)

13-14: Migration to IndustriaBlockEntityRenderer looks good

Extending the common base matches the new renderModel/onRender lifecycle used across the PR.


18-20: Constructor now defers to base context — good

Constructing the model via context layer is correct and consistent.


23-26: Confirm transforms are handled by the base renderer

renderModel now only renders geometry; ensure IndustriaBlockEntityRenderer applies the expected push/pop, translation, and facing rotation that used to live here.

Would you confirm that IndustriaBlockEntityRenderer performs the block-facing transform before calling renderModel?


28-31: onRender hook added

No-op hook is fine; provides a place for future overlays or debug visuals without polluting model rendering.

src/client/java/dev/turtywurty/industria/renderer/block/MotorBlockEntityRenderer.java (2)

13-14: Renderer migrated to the new base — LGTM

Adopts the same lifecycle as other renderers in this PR.


32-35: onRender hook present

No-op hook aligns with the new lifecycle and keeps model rendering focused.

src/client/java/dev/turtywurty/industria/renderer/block/CrusherBlockEntityRenderer.java (2)

16-24: Base-class migration and model init — good

Renderer now extends IndustriaBlockEntityRenderer and initializes the model via the layer; consistent with the rest of the refactor.


49-53: onRender hook added

No-op is fine for now; consistent lifecycle.

src/client/java/dev/turtywurty/industria/renderer/block/DrillBlockEntityRenderer.java (2)

45-60: Render flow decomposition is solid

Splitting motor/frame/cable/drill into helpers improves readability and testability. Sequence in renderModel is coherent.


140-146: Line rendering sequence: verify intended segments

You push three vertices before the translate (A -> B -> B). If the duplicate B is only to “end” the previous segment, consider either omitting it or adding a short comment to make intent clear for future maintainers.

Is the duplicate vertex at Line 142 intentional to reset the segment for the next cable section? If not, dropping it will avoid a degenerate segment.

src/client/java/dev/turtywurty/industria/renderer/block/OilPumpJackBlockEntityRenderer.java (1)

84-88: Confirm base-class handles block facing transforms.

Previous versions rotated by Properties.HORIZONTAL_FACING; this renderer no longer applies facing transforms. If IndustriaBlockEntityRenderer now standardizes transforms, all good; otherwise this model may render un-rotated.

src/client/java/dev/turtywurty/industria/renderer/block/CrystallizerBlockEntityRenderer.java (2)

68-72: Model rendering hook looks good.

Adopts the new renderModel entry point cleanly and delegates buffer retrieval correctly.


104-118: Switched to renderFluidTank API correctly; verify upstream fluid renderer fixes.

The calls and bounds look consistent. Note that current InWorldFluidRenderingComponent has guards disabled and forces water; once fixed per my comment there, these calls should render the correct fluids and fill amounts.

src/client/java/dev/turtywurty/industria/renderer/block/FluidTankBlockEntityRenderer.java (1)

27-33: Verify fluid-tank translation & bounds

Comparing with the other block-entity renderers’ renderFluidTank calls, this one is unique in that it does:

matrices.translate(-0.5, -1.5, -0.5);
this.fluidRenderingComponent.renderFluidTank(
    …,
    3/16f, 0, 3/16f,
    13/16f, 15, 13/16f
);

No other renderer applies a (-0.5, -1.5, -0.5) shift, and the other calls all pass their Y-max as a float in block-space (e.g. 1.999f), not an integer “15”. Please verify that:

  • The BE-renderer’s incoming matrices origin is where you expect (typically block-origin at the bottom northwest corner).
  • You actually want to move down 1.5 blocks and back 0.5 blocks—if not, flip the signs on Y (and/or Z) to position the tank correctly.
  • The min/max bounds (3/16f → 13/16f) are in the intended [0…1] block-space region of the tank.
  • The “15” height isn’t meant to be 15/16f. If the API expects block-space floats, change it to 15/16f.

If any of these are off, consider reverting to something like:

-matrices.translate(-0.5, -1.5, -0.5);
+matrices.translate(-0.5f,  1.5f, -0.5f); // or adjust Z sign as needed-       3/16f, 0, 3/16f,
-       13/16f, 15, 13/16f);
+       3/16f, 0, 3/16f,
+       13/16f, 15/16f, 13/16f);

…once you confirm how your fluid-rendering component interprets those coordinates.

src/client/java/dev/turtywurty/industria/renderer/block/ShakingTableBlockEntityRenderer.java (2)

106-138: Surface fluid math uses coordinates outside the usual ±1 block range — confirm intent

minX is set to 1 + 1/16f and z2 to 1 + 2/16f, taking coordinates outside the conventional [−1, 1] cube. If renderFluidTank expects block-local coordinates within the model’s bounds, this could cull or misplace the fluid. If intentional (to match the model’s geometry), ignore. Otherwise, re-evaluate the bounds and height derivation.


78-104: Item rendering path looks solid; consistent push/pop and world-seeded randomness
LGTM. The per-item transforms are nicely isolated, and using ItemDisplayContext.GROUND fits the presentation.

src/client/java/dev/turtywurty/industria/renderer/block/MixerBlockEntityRenderer.java (2)

62-80: Guard against index mismatch between input stacks and mixingItemPositions

Accessing entity.mixingItemPositions.get(index) assumes that list is at least inputInventory.heldStacks.size(). If they diverge (e.g., slots > configured positions), this will throw.

If needed, clamp the index or pre-size mixingItemPositions to the inventory’s size before rendering.


97-104: Fluid rendering call aligns with new API
LGTM. The bounds/maxHeightPixels usage is consistent with other renderers in this PR.

src/client/java/dev/turtywurty/industria/renderer/block/ClarifierBlockEntityRenderer.java (2)

55-58: Model render split looks good
The renderModel hook cleanly renders the static root part, consistent with your refactor across renderers.


61-69: Good early-return guard for null world
Avoids NPEs in downstream per-frame rendering. Keep this pattern where world can be null.

Comment on lines +13 to +16
@Override
protected void renderModel(ArcFurnaceBlockEntity entity, float tickDelta, MatrixStack matrices, VertexConsumerProvider vertexConsumers, int light, int overlay) {

}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Empty renderModel causes the Arc Furnace to render nothing.

With the base class now rendering via renderModel(...), leaving this method empty results in no visual output for the block entity (and likely its item form, once renderForItem is fixed).

Port the previous model draw into renderModel(...). If you need a temporary stopgap while migrating, delegate to onRender(...) (assuming it still contains the prior rendering code), e.g.:

 @Override
 protected void renderModel(ArcFurnaceBlockEntity entity, float tickDelta, MatrixStack matrices, VertexConsumerProvider vertexConsumers, int light, int overlay) {
- 
+    // TEMP: delegate to onRender until the model is fully migrated here
+    onRender(entity, tickDelta, matrices, vertexConsumers, light, overlay);
 }

However, since onRender(...) is also empty here, the real fix is to move the actual model rendering code into this method (similar to Electrolyzer/Digester).

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@Override
protected void renderModel(ArcFurnaceBlockEntity entity, float tickDelta, MatrixStack matrices, VertexConsumerProvider vertexConsumers, int light, int overlay) {
}
@Override
protected void renderModel(ArcFurnaceBlockEntity entity, float tickDelta, MatrixStack matrices, VertexConsumerProvider vertexConsumers, int light, int overlay) {
// TEMP: delegate to onRender until the model is fully migrated here
onRender(entity, tickDelta, matrices, vertexConsumers, light, overlay);
}
🤖 Prompt for AI Agents
In
src/client/java/dev/turtywurty/industria/renderer/block/ArcFurnaceBlockEntityRenderer.java
around lines 13–16, renderModel(...) is empty which prevents the Arc Furnace
from being drawn; port the previous model-draw code into this method (or, as a
temporary measure, call the old onRender(...) implementation if it still
contains the drawing logic) making sure to perform the same matrix push/pop,
translations/rotations, and use the provided VertexConsumerProvider, light and
overlay parameters so the model renders correctly for both block entity and item
contexts.

Comment on lines +62 to +86
private void renderInputItems(CentrifugalConcentratorBlockEntity entity, float tickDelta, MatrixStack matrices, VertexConsumerProvider vertexConsumers, int light, int overlay) {
ItemStack stackInSlot = Items.DIAMOND.getDefaultStack(); //entity.getInputInventory().getStackInSlot(0);
if(stackInSlot.isEmpty()) return;

float radius = 1.1f;

for (int i = 0; i < NUM_SPINNING_ITEMS; i++) {
float angle = -entity.bowlRotation + (float) Math.TAU / NUM_SPINNING_ITEMS * i;

float x = (float) Math.cos(angle) * radius;
float y = (float) Math.sin(angle * 3) * 0.02f - 0.2f;
float z = (float) Math.sin(angle) * radius;

matrices.push();
matrices.translate(x, y, z);
matrices.multiply(RotationAxis.POSITIVE_Y.rotation(angle + (float) Math.PI / 2f));
matrices.scale(0.5f, 0.5f, 0.5f);
this.context.getItemRenderer().renderItem(stackInSlot, ItemDisplayContext.NONE, light, overlay, matrices, vertexConsumers, entity.getWorld(), 0);

Vector3f pos = matrixStackToWorldPosition(matrices);
entity.getWorld().addParticleClient(ParticleTypes.BUBBLE, pos.x, pos.y + 0.3f, pos.z, 0, 0, 0);

matrices.pop();
}
}
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Remove debug item and throttle particles to avoid permanent diamonds and heavy client load.

  • Items.DIAMOND.getDefaultStack() forces diamonds to render always; use the actual inventory stack.
  • Spawning bubbles every frame per item is expensive. Add a light throttle.
  • Guard against null world.
-        ItemStack stackInSlot = Items.DIAMOND.getDefaultStack(); //entity.getInputInventory().getStackInSlot(0);
+        if (entity.getWorld() == null) return;
+        ItemStack stackInSlot = entity.getInputInventory().getStackInSlot(0);
         if(stackInSlot.isEmpty()) return;
@@
-            Vector3f pos = matrixStackToWorldPosition(matrices);
-            entity.getWorld().addParticleClient(ParticleTypes.BUBBLE, pos.x, pos.y + 0.3f, pos.z, 0, 0, 0);
+            // Light throttle: 1 particle per item every 4 ticks, staggered by index
+            long time = entity.getWorld().getTime();
+            if (((time + i) & 3) == 0) {
+                Vector3f pos = matrixStackToWorldPosition(matrices);
+                entity.getWorld().addParticleClient(ParticleTypes.BUBBLE, pos.x, pos.y + 0.3f, pos.z, 0, 0, 0);
+            }

🏁 Script executed:

#!/bin/bash
# Verify the availability of getInputInventory and inputInventory fields/methods in the codebase
rg -n "getInputInventory" -C3
rg -n "inputInventory" -C3

Length of output: 44259


Remove debug diamond, use real input stack, throttle particles, and guard null world

Please update src/client/java/dev/turtywurty/industria/renderer/block/CentrifugalConcentratorBlockEntityRenderer.java as follows:

• At the top of renderInputItems(...), bail out if entity.getWorld() is null and grab the real stack:

-   ItemStack stackInSlot = Items.DIAMOND.getDefaultStack(); //entity.getInputInventory().getStackInSlot(0);
+   if (entity.getWorld() == null) return;
+   ItemStack stackInSlot = entity.getInputInventory().getStackInSlot(0);
    if (stackInSlot.isEmpty()) return;

• Around the bubble spawn, throttle to once per item every 4 ticks:

-           Vector3f pos = matrixStackToWorldPosition(matrices);
-           entity.getWorld().addParticleClient(ParticleTypes.BUBBLE,
-               pos.x, pos.y + 0.3f, pos.z,
-               0, 0, 0);
+           long time = entity.getWorld().getTime();
+           if (((time + i) & 3) == 0) {  // ¼ frequency, staggered by index
+               Vector3f pos = matrixStackToWorldPosition(matrices);
+               entity.getWorld().addParticleClient(ParticleTypes.BUBBLE,
+                   pos.x, pos.y + 0.3f, pos.z,
+                   0, 0, 0);
+           }

– Lines to update:
• 63–65: Replace debug DIAMOND stack & add null‐world guard
• 74–80: Wrap addParticleClient in time‐based throttle

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private void renderInputItems(CentrifugalConcentratorBlockEntity entity, float tickDelta, MatrixStack matrices, VertexConsumerProvider vertexConsumers, int light, int overlay) {
ItemStack stackInSlot = Items.DIAMOND.getDefaultStack(); //entity.getInputInventory().getStackInSlot(0);
if(stackInSlot.isEmpty()) return;
float radius = 1.1f;
for (int i = 0; i < NUM_SPINNING_ITEMS; i++) {
float angle = -entity.bowlRotation + (float) Math.TAU / NUM_SPINNING_ITEMS * i;
float x = (float) Math.cos(angle) * radius;
float y = (float) Math.sin(angle * 3) * 0.02f - 0.2f;
float z = (float) Math.sin(angle) * radius;
matrices.push();
matrices.translate(x, y, z);
matrices.multiply(RotationAxis.POSITIVE_Y.rotation(angle + (float) Math.PI / 2f));
matrices.scale(0.5f, 0.5f, 0.5f);
this.context.getItemRenderer().renderItem(stackInSlot, ItemDisplayContext.NONE, light, overlay, matrices, vertexConsumers, entity.getWorld(), 0);
Vector3f pos = matrixStackToWorldPosition(matrices);
entity.getWorld().addParticleClient(ParticleTypes.BUBBLE, pos.x, pos.y + 0.3f, pos.z, 0, 0, 0);
matrices.pop();
}
}
private void renderInputItems(CentrifugalConcentratorBlockEntity entity, float tickDelta, MatrixStack matrices, VertexConsumerProvider vertexConsumers, int light, int overlay) {
if (entity.getWorld() == null) return;
ItemStack stackInSlot = entity.getInputInventory().getStackInSlot(0);
if (stackInSlot.isEmpty()) return;
float radius = 1.1f;
for (int i = 0; i < NUM_SPINNING_ITEMS; i++) {
float angle = -entity.bowlRotation + (float) Math.TAU / NUM_SPINNING_ITEMS * i;
float x = (float) Math.cos(angle) * radius;
float y = (float) Math.sin(angle * 3) * 0.02f - 0.2f;
float z = (float) Math.sin(angle) * radius;
matrices.push();
matrices.translate(x, y, z);
matrices.multiply(RotationAxis.POSITIVE_Y.rotation(angle + (float) Math.PI / 2f));
matrices.scale(0.5f, 0.5f, 0.5f);
this.context.getItemRenderer().renderItem(stackInSlot, ItemDisplayContext.NONE, light, overlay, matrices, vertexConsumers, entity.getWorld(), 0);
long time = entity.getWorld().getTime();
if (((time + i) & 3) == 0) { // ¼ frequency, staggered by index
Vector3f pos = matrixStackToWorldPosition(matrices);
entity.getWorld().addParticleClient(ParticleTypes.BUBBLE,
pos.x, pos.y + 0.3f, pos.z,
0, 0, 0);
}
matrices.pop();
}
}

Comment on lines +50 to 57
World world = entity.getWorld();

this.motorModel.render(matrices, vertexConsumers.getBuffer(this.motorModel.getLayer(DrillMotorModel.TEXTURE_LOCATION)), light, overlay);
int worldBottom = world == null ? 0 : world.getBottomY();
int startY = entity.getPos().getY() + 2;
float currentY = entity.getDrillYOffset() - 1 + startY;

parts.rodGear().pitch = prevRodGear;
parts.connectingGear().pitch = connectingGear;
}
}
float progress = 1 - (startY - currentY) / (startY - worldBottom);

Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Guard against divide-by-zero and clamp progress to [0,1]

In extremely shallow worlds (or future level settings), startY - worldBottom could be 0; also, progress can drift outside [0,1]. Clamp and guard to avoid NaN/Infinity affecting downstream math and scaling.

Apply this diff:

-        int worldBottom = world == null ? 0 : world.getBottomY();
-        int startY = entity.getPos().getY() + 2;
-        float currentY = entity.getDrillYOffset() - 1 + startY;
-
-        float progress = 1 - (startY - currentY) / (startY - worldBottom);
+        int worldBottom = world == null ? 0 : world.getBottomY();
+        int startY = entity.getPos().getY() + 2;
+        float currentY = entity.getDrillYOffset() - 1 + startY;
+        int denom = Math.max(1, startY - worldBottom);
+        float progress = 1f - (startY - currentY) / (float) denom;
+        progress = Math.max(0f, Math.min(1f, progress));
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
World world = entity.getWorld();
this.motorModel.render(matrices, vertexConsumers.getBuffer(this.motorModel.getLayer(DrillMotorModel.TEXTURE_LOCATION)), light, overlay);
int worldBottom = world == null ? 0 : world.getBottomY();
int startY = entity.getPos().getY() + 2;
float currentY = entity.getDrillYOffset() - 1 + startY;
parts.rodGear().pitch = prevRodGear;
parts.connectingGear().pitch = connectingGear;
}
}
float progress = 1 - (startY - currentY) / (startY - worldBottom);
World world = entity.getWorld();
int worldBottom = world == null ? 0 : world.getBottomY();
int startY = entity.getPos().getY() + 2;
float currentY = entity.getDrillYOffset() - 1 + startY;
int denom = Math.max(1, startY - worldBottom);
float progress = 1f - (startY - currentY) / (float) denom;
progress = Math.max(0f, Math.min(1f, progress));
🤖 Prompt for AI Agents
In
src/client/java/dev/turtywurty/industria/renderer/block/DrillBlockEntityRenderer.java
around lines 50 to 57, the computation of progress uses (startY - worldBottom)
as a denominator which can be zero in very shallow worlds and the resulting
progress may fall outside [0,1]; update the code to compute the denominator
safely (use a max(1, startY - worldBottom) or check for zero and fall back to 1)
and then clamp the resulting progress to the range [0.0f, 1.0f] before any
further math so you never produce NaN/Infinity or values outside the expected
bounds.

Comment on lines +59 to +82

@Override
public final void render(T entity, float tickDelta, MatrixStack matrices, VertexConsumerProvider vertexConsumers, int light, int overlay, Vec3d cameraPos) {
matrices.push();
setupBlockEntityTransformations(matrices, entity);

matrices.push();
matrices.multiply(RotationAxis.POSITIVE_X.rotationDegrees(180));
renderModel(entity, tickDelta, matrices, vertexConsumers, light, overlay);
matrices.pop();

onRender(entity, tickDelta, matrices, vertexConsumers, light, overlay);

if (isPlayerLookingAt(entity.getPos())) {
List<ModelPart> wireframe = getModelParts();
if (!wireframe.isEmpty()) {
boolean isHighContrast = isHighContrast();
renderWireframe(wireframe, matrices, vertexConsumers, isHighContrast);
}
}

matrices.pop();
postRender(entity, tickDelta, matrices, vertexConsumers, light, overlay);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

renderForItem currently renders nothing for migrated renderers and also has a push/pop imbalance.

With the new pattern, subclasses moved their rendering into renderModel(...) and often left onRender(...) empty. renderForItem(...) still calls only onRender(...), so items won’t render. Additionally, renderForItem(...) does a matrices.pop() without a corresponding push() in that method.

Proposed fix (replace the whole method; note this change occurs outside the shown hunk):

public final void renderForItem(T entity, float tickDelta, MatrixStack matrices, VertexConsumerProvider vertexConsumers, int light, int overlay) {
    matrices.push();
    setupBlockEntityTransformations(matrices, entity);

    matrices.push();
    matrices.multiply(RotationAxis.POSITIVE_X.rotationDegrees(180));
    renderModel(entity, tickDelta, matrices, vertexConsumers, light, overlay);
    matrices.pop();

    // Optionally keep this if item renders need extra overlays/effects
    onRender(entity, tickDelta, matrices, vertexConsumers, light, overlay);

    matrices.pop();
    postRender(entity, tickDelta, matrices, vertexConsumers, light, overlay);
}

This mirrors the world render path and restores item rendering for classes that implemented renderModel(...).

🤖 Prompt for AI Agents
In
src/client/java/dev/turtywurty/industria/renderer/block/IndustriaBlockEntityRenderer.java
around lines 59-82, renderForItem currently calls only onRender (so migrated
renderers that moved drawing into renderModel render nothing) and contains a
stray matrices.pop() without a matching push; replace renderForItem with a
method that mirrors the world render path: push the matrices, call
setupBlockEntityTransformations(matrices, entity), push again, rotate 180° on X,
call renderModel(entity, tickDelta, matrices, vertexConsumers, light, overlay),
pop the inner matrix, optionally call onRender for extra overlays/effects, pop
the outer matrix, then call postRender(entity, tickDelta, matrices,
vertexConsumers, light, overlay) so item rendering works and push/pop are
balanced.

Comment on lines 61 to 66
float alpha = (float) (Math.sin(entity.getWorld().getTime() % 20) * 0.5 + 0.5F);
VertexRendering.drawBox(
matrices,
vertexConsumer,
-0.1F + xOffset,
-0.1F + yOffset,
-0.1F + zOffset,
0.1F + xOffset,
0.1F + yOffset,
0.1F + zOffset,
1.0F,
1.0F,
1.0F,
alpha);
VertexRendering.drawBox(matrices, vertexConsumer,
-0.1F + xOffset, -0.1F + yOffset, -0.1F + zOffset,
0.1F + xOffset, 0.1F + yOffset, 0.1F + zOffset,
1.0F, 1.0F, 1.0F, alpha);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Potential NPE when computing alpha from world time

entity.getWorld() can be null on the client in edge cases (e.g., very early lifecycle). Use a null-safe read for the time.

Apply this diff:

-                float alpha = (float) (Math.sin(entity.getWorld().getTime() % 20) * 0.5 + 0.5F);
+                long time = entity.getWorld() != null ? entity.getWorld().getTime() : 0L;
+                float alpha = (float) (Math.sin(time % 20) * 0.5 + 0.5F);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
float alpha = (float) (Math.sin(entity.getWorld().getTime() % 20) * 0.5 + 0.5F);
VertexRendering.drawBox(
matrices,
vertexConsumer,
-0.1F + xOffset,
-0.1F + yOffset,
-0.1F + zOffset,
0.1F + xOffset,
0.1F + yOffset,
0.1F + zOffset,
1.0F,
1.0F,
1.0F,
alpha);
VertexRendering.drawBox(matrices, vertexConsumer,
-0.1F + xOffset, -0.1F + yOffset, -0.1F + zOffset,
0.1F + xOffset, 0.1F + yOffset, 0.1F + zOffset,
1.0F, 1.0F, 1.0F, alpha);
}
// Null-safe world time for alpha calculation
long time = entity.getWorld() != null ? entity.getWorld().getTime() : 0L;
float alpha = (float) (Math.sin(time % 20) * 0.5 + 0.5F);
VertexRendering.drawBox(matrices, vertexConsumer,
-0.1F + xOffset, -0.1F + yOffset, -0.1F + zOffset,
0.1F + xOffset, 0.1F + yOffset, 0.1F + zOffset,
1.0F, 1.0F, 1.0F, alpha);
🤖 Prompt for AI Agents
In
src/client/java/dev/turtywurty/industria/renderer/block/MultiblockIOBlockEntityRenderer.java
around lines 61 to 66, computing alpha uses entity.getWorld().getTime() which
can NPE if getWorld() is null; replace with a null-safe read (e.g., read world
into a local variable and use its getTime() only when non-null, otherwise use a
safe fallback like 0L or client tick count) and then compute alpha from that
safe time value so the renderer never dereferences a null world.

Comment on lines +43 to 55
public void renderFluidTank(@Nullable SingleFluidStorage fluidTank, VertexConsumerProvider vertexConsumers, MatrixStack matrices, int light, int overlay, World world, BlockPos pos, float x1, float y1, float z1, float x2, float maxHeightPixels, float z2, int color, ColorMode colorMode, IndeterminateBoolean drawTopFace) {
// if (fluidTank == null || fluidTank.isResourceBlank() || fluidTank.amount <= 0) return;

FluidVariant fluidVariant = fluidTank.getResource();
FluidVariant fluidVariant = FluidVariant.of(Fluids.WATER); //fluidTank.getResource();
long amount = fluidTank.amount;
long capacity = fluidTank.getCapacity();
float fillPercentage = (float) amount / capacity;
float fillPercentage = (float) (Math.sin(world.getTime() / 20.0) * 0.5 + 0.5);
; //(float) amount / capacity;
fillPercentage = MathHelper.clamp(fillPercentage, 0.0F, 1.0F);

if(this.shouldDebugAmount) {
fillPercentage = (float) (Math.sin(world.getTime() / 64.0) * 0.5 + 0.5);
if (this.shouldDebugAmount) {
fillPercentage = (float) (Math.sin(world.getTime() / 64f) * 0.5 + 0.5);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Null/resource guards removed and tank values ignored—this will NPE and always render water.

  • The early guard is commented out, but fluidTank.amount and fluidTank.getCapacity() are still read—NPE if fluidTank is null.
  • FluidVariant fluidVariant = FluidVariant.of(Fluids.WATER) ignores the actual tank resource.
  • fillPercentage is driven by world.getTime() instead of the tank value, except under debug.

Restore guards and use tank contents; keep the sine path only for debug.

-        // if (fluidTank == null || fluidTank.isResourceBlank() || fluidTank.amount <= 0) return;
-
-        FluidVariant fluidVariant = FluidVariant.of(Fluids.WATER); //fluidTank.getResource();
-        long amount = fluidTank.amount;
-        long capacity = fluidTank.getCapacity();
-        float fillPercentage = (float) (Math.sin(world.getTime() / 20.0) * 0.5 + 0.5);
-        ; //(float) amount / capacity;
+        if (fluidTank == null || fluidTank.isResourceBlank() || fluidTank.amount <= 0) return;
+
+        FluidVariant fluidVariant = fluidTank.getResource();
+        long amount = fluidTank.amount;
+        long capacity = fluidTank.getCapacity();
+        float fillPercentage = (float) amount / (float) capacity;
         fillPercentage = MathHelper.clamp(fillPercentage, 0.0F, 1.0F);
 
         if (this.shouldDebugAmount) {
             fillPercentage = (float) (Math.sin(world.getTime() / 64f) * 0.5 + 0.5);
         }

If amount is not publicly accessible for your storage type, replace with its accessor (e.g., getAmount()).

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public void renderFluidTank(@Nullable SingleFluidStorage fluidTank, VertexConsumerProvider vertexConsumers, MatrixStack matrices, int light, int overlay, World world, BlockPos pos, float x1, float y1, float z1, float x2, float maxHeightPixels, float z2, int color, ColorMode colorMode, IndeterminateBoolean drawTopFace) {
// if (fluidTank == null || fluidTank.isResourceBlank() || fluidTank.amount <= 0) return;
FluidVariant fluidVariant = fluidTank.getResource();
FluidVariant fluidVariant = FluidVariant.of(Fluids.WATER); //fluidTank.getResource();
long amount = fluidTank.amount;
long capacity = fluidTank.getCapacity();
float fillPercentage = (float) amount / capacity;
float fillPercentage = (float) (Math.sin(world.getTime() / 20.0) * 0.5 + 0.5);
; //(float) amount / capacity;
fillPercentage = MathHelper.clamp(fillPercentage, 0.0F, 1.0F);
if(this.shouldDebugAmount) {
fillPercentage = (float) (Math.sin(world.getTime() / 64.0) * 0.5 + 0.5);
if (this.shouldDebugAmount) {
fillPercentage = (float) (Math.sin(world.getTime() / 64f) * 0.5 + 0.5);
}
public void renderFluidTank(@Nullable SingleFluidStorage fluidTank, VertexConsumerProvider vertexConsumers, MatrixStack matrices, int light, int overlay, World world, BlockPos pos, float x1, float y1, float z1, float x2, float maxHeightPixels, float z2, int color, ColorMode colorMode, IndeterminateBoolean drawTopFace) {
if (fluidTank == null || fluidTank.isResourceBlank() || fluidTank.amount <= 0) return;
FluidVariant fluidVariant = fluidTank.getResource();
long amount = fluidTank.amount;
long capacity = fluidTank.getCapacity();
float fillPercentage = (float) amount / (float) capacity;
fillPercentage = MathHelper.clamp(fillPercentage, 0.0F, 1.0F);
if (this.shouldDebugAmount) {
fillPercentage = (float) (Math.sin(world.getTime() / 64f) * 0.5 + 0.5);
}
🤖 Prompt for AI Agents
In
src/client/java/dev/turtywurty/industria/util/InWorldFluidRenderingComponent.java
around lines 43 to 55, restore the early null/resource guards and stop
hardcoding WATER and time-based fill: check if fluidTank is null or
resource-blank (or amount/capacity invalid) and return early to avoid NPE;
obtain the actual FluidVariant from the tank via its accessor (e.g.,
fluidTank.getResource() or equivalent) instead of FluidVariant.of(Fluids.WATER);
read amount and capacity via their public accessors (e.g.,
fluidTank.getAmount()/getCapacity() if fields are not public), compute
fillPercentage as (float)amount / (float)capacity and clamp it, and only
override fillPercentage with the sine/world-time debug value when
shouldDebugAmount is true. Ensure you handle zero capacity to avoid division by
zero.

Comment on lines +136 to 163
public void renderFace(Direction direction, @Nullable FluidVariant fluidVariant, VertexConsumerProvider vertexConsumers, MatrixStack matrices, int light, int overlay, World world, BlockPos pos, float left, float right, float up, float down, float depth, int color, ColorMode colorMode, UnaryOperator<RenderLayer> wrapRenderLayer) {
if (fluidVariant == null) return;

Sprite stillSprite = FluidVariantRendering.getSprite(fluidVariant);
if (stillSprite == null) return;

int fluidColor = FluidVariantRendering.getColor(fluidVariant, world, pos);
fluidColor = ColorMode.modifyColor(fluidColor, color, colorMode);

Sprite stillSprite = FluidVariantRendering.getSprite(fluidVariant);
if(stillSprite == null)
return;

RenderLayer renderLayer = RenderLayer.getItemEntityTranslucentCull(stillSprite.getAtlasId());
renderLayer = wrapRenderLayer.apply(renderLayer);
VertexConsumer vertexConsumer = vertexConsumers.getBuffer(renderLayer);

matrices.push();
matrices.multiply(RotationAxis.POSITIVE_X.rotationDegrees(180));

if (FluidVariantAttributes.isLighterThanAir(fluidVariant)) {
matrices.multiply(RotationAxis.POSITIVE_X.rotationDegrees(180));
}

MatrixStack.Entry entry = matrices.peek();

int blockLight = (light >> 4) & 0xF;
int luminosity = Math.max(blockLight, FluidVariantAttributes.getLuminance(fluidVariant));
light = (light & 0xF00000) | (luminosity << 4);

drawTiledXYQuad(vertexConsumer, entry, x1, y1, z1, x2, y2, z2, stillSprite, fluidColor, light, overlay, 0.0F, 1.0F, -1.0F);
renderDirectionalTiledQuad(direction, vertexConsumer, matrices, left, depth, up, right, down, stillSprite, fluidColor, light, overlay);

matrices.pop();
}

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Parameter order bug when delegating to renderDirectionalTiledQuad leads to warped geometry.

renderDirectionalTiledQuad(direction, ..., left, depth, up, right, down, ...) mixes up right/down/depth. It must be left, right, up, down, depth.

-        renderDirectionalTiledQuad(direction, vertexConsumer, matrices, left, depth, up, right, down, stillSprite, fluidColor, light, overlay);
+        renderDirectionalTiledQuad(direction, vertexConsumer, matrices, left, right, up, down, depth, stillSprite, fluidColor, light, overlay);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public void renderFace(Direction direction, @Nullable FluidVariant fluidVariant, VertexConsumerProvider vertexConsumers, MatrixStack matrices, int light, int overlay, World world, BlockPos pos, float left, float right, float up, float down, float depth, int color, ColorMode colorMode, UnaryOperator<RenderLayer> wrapRenderLayer) {
if (fluidVariant == null) return;
Sprite stillSprite = FluidVariantRendering.getSprite(fluidVariant);
if (stillSprite == null) return;
int fluidColor = FluidVariantRendering.getColor(fluidVariant, world, pos);
fluidColor = ColorMode.modifyColor(fluidColor, color, colorMode);
Sprite stillSprite = FluidVariantRendering.getSprite(fluidVariant);
if(stillSprite == null)
return;
RenderLayer renderLayer = RenderLayer.getItemEntityTranslucentCull(stillSprite.getAtlasId());
renderLayer = wrapRenderLayer.apply(renderLayer);
VertexConsumer vertexConsumer = vertexConsumers.getBuffer(renderLayer);
matrices.push();
matrices.multiply(RotationAxis.POSITIVE_X.rotationDegrees(180));
if (FluidVariantAttributes.isLighterThanAir(fluidVariant)) {
matrices.multiply(RotationAxis.POSITIVE_X.rotationDegrees(180));
}
MatrixStack.Entry entry = matrices.peek();
int blockLight = (light >> 4) & 0xF;
int luminosity = Math.max(blockLight, FluidVariantAttributes.getLuminance(fluidVariant));
light = (light & 0xF00000) | (luminosity << 4);
drawTiledXYQuad(vertexConsumer, entry, x1, y1, z1, x2, y2, z2, stillSprite, fluidColor, light, overlay, 0.0F, 1.0F, -1.0F);
renderDirectionalTiledQuad(direction, vertexConsumer, matrices, left, depth, up, right, down, stillSprite, fluidColor, light, overlay);
matrices.pop();
}
public void renderFace(Direction direction, @Nullable FluidVariant fluidVariant, VertexConsumerProvider vertexConsumers, MatrixStack matrices, int light, int overlay, World world, BlockPos pos, float left, float right, float up, float down, float depth, int color, ColorMode colorMode, UnaryOperator<RenderLayer> wrapRenderLayer) {
if (fluidVariant == null) return;
Sprite stillSprite = FluidVariantRendering.getSprite(fluidVariant);
if (stillSprite == null) return;
int fluidColor = FluidVariantRendering.getColor(fluidVariant, world, pos);
fluidColor = ColorMode.modifyColor(fluidColor, color, colorMode);
RenderLayer renderLayer = RenderLayer.getItemEntityTranslucentCull(stillSprite.getAtlasId());
renderLayer = wrapRenderLayer.apply(renderLayer);
VertexConsumer vertexConsumer = vertexConsumers.getBuffer(renderLayer);
matrices.push();
if (FluidVariantAttributes.isLighterThanAir(fluidVariant)) {
matrices.multiply(RotationAxis.POSITIVE_X.rotationDegrees(180));
}
int blockLight = (light >> 4) & 0xF;
int luminosity = Math.max(blockLight, FluidVariantAttributes.getLuminance(fluidVariant));
light = (light & 0xF00000) | (luminosity << 4);
renderDirectionalTiledQuad(direction, vertexConsumer, matrices, left, right, up, down, depth, stillSprite, fluidColor, light, overlay);
matrices.pop();
}
🤖 Prompt for AI Agents
In
src/client/java/dev/turtywurty/industria/util/InWorldFluidRenderingComponent.java
around lines 136 to 163, the call to renderDirectionalTiledQuad passes
parameters in the wrong order (left, depth, up, right, down), which warps
geometry; change the argument order to left, right, up, down, depth so it
matches the method signature (left, right, up, down, depth) when delegating the
values to renderDirectionalTiledQuad.

Comment on lines +289 to 293
for (int k = 0; k < 4; k++) {
vertexConsumer.vertex(entry, vertices[k][0], vertices[k][1], vertices[k][2])
.color(color).texture(uvs[k][0], uvs[k][1]).light(light).overlay(overlay).normal(entry, normal);
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Incorrect VertexConsumer.normal(...) overload used; likely a compile error.

normal(entry, normal) does not match the Yarn/vanilla overloads. Pass components explicitly.

-                    vertexConsumer.vertex(entry, vertices[k][0], vertices[k][1], vertices[k][2])
-                            .color(color).texture(uvs[k][0], uvs[k][1]).light(light).overlay(overlay).normal(entry, normal);
+                    vertexConsumer.vertex(entry, vertices[k][0], vertices[k][1], vertices[k][2])
+                            .color(color)
+                            .texture(uvs[k][0], uvs[k][1])
+                            .light(light)
+                            .overlay(overlay)
+                            .normal(entry, normal.x(), normal.y(), normal.z());
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for (int k = 0; k < 4; k++) {
vertexConsumer.vertex(entry, vertices[k][0], vertices[k][1], vertices[k][2])
.color(color).texture(uvs[k][0], uvs[k][1]).light(light).overlay(overlay).normal(entry, normal);
}
}
for (int k = 0; k < 4; k++) {
vertexConsumer.vertex(entry, vertices[k][0], vertices[k][1], vertices[k][2])
.color(color)
.texture(uvs[k][0], uvs[k][1])
.light(light)
.overlay(overlay)
.normal(entry, normal.x(), normal.y(), normal.z());
}
🤖 Prompt for AI Agents
In
src/client/java/dev/turtywurty/industria/util/InWorldFluidRenderingComponent.java
around lines 289 to 293, the call to vertexConsumer.normal(entry, normal) uses
the wrong overload and won't compile; extract the normal's X/Y/Z components and
call the normal overload that takes three floats (or three floats with the
proper matrix if your API requires), e.g. compute float nx, ny, nz from the
existing normal object and replace .normal(entry, normal) with the appropriate
.normal(nx, ny, nz) (or .normal(entry, nx, ny, nz) if your VertexConsumer
expects the matrix first) so the explicit components are passed to the correct
overload.

Comment on lines 54 to 57
if (storage instanceof SingleFluidStorage singleFluidStorage) {
singleFluidStorage.amount = view.getLong("Amount", 0L);
singleFluidStorage.variant = view.read("Fluid", FluidVariant.CODEC).orElseThrow();
singleFluidStorage.variant = view.read("Fluid", FluidVariant.CODEC).orElse(FluidVariant.blank());
} else {
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Prevent inconsistent state: zero amount when Fluid is missing (blank variant).

Defaulting to FluidVariant.blank() avoids crashes, but leaving a non-zero amount with a blank fluid is an inconsistent state that can break consumers (e.g., renderers assuming amount>0 implies non-blank fluid). Set amount to 0 when the read variant is blank.

Apply this diff:

-                singleFluidStorage.amount = view.getLong("Amount", 0L);
-                singleFluidStorage.variant = view.read("Fluid", FluidVariant.CODEC).orElse(FluidVariant.blank());
+                FluidVariant variant = view.read("Fluid", FluidVariant.CODEC).orElse(FluidVariant.blank());
+                long amount = view.getLong("Amount", 0L);
+                if (variant.isBlank()) {
+                    amount = 0L; // ensure no stray amount for "empty" fluid
+                }
+                singleFluidStorage.variant = variant;
+                singleFluidStorage.amount = amount;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (storage instanceof SingleFluidStorage singleFluidStorage) {
singleFluidStorage.amount = view.getLong("Amount", 0L);
singleFluidStorage.variant = view.read("Fluid", FluidVariant.CODEC).orElseThrow();
singleFluidStorage.variant = view.read("Fluid", FluidVariant.CODEC).orElse(FluidVariant.blank());
} else {
if (storage instanceof SingleFluidStorage singleFluidStorage) {
FluidVariant variant = view.read("Fluid", FluidVariant.CODEC).orElse(FluidVariant.blank());
long amount = view.getLong("Amount", 0L);
if (variant.isBlank()) {
amount = 0L; // ensure no stray amount for "empty" fluid
}
singleFluidStorage.variant = variant;
singleFluidStorage.amount = amount;
} else {
🤖 Prompt for AI Agents
In
src/main/java/dev/turtywurty/industria/blockentity/util/fluid/WrappedFluidStorage.java
around lines 54 to 57, when deserializing a SingleFluidStorage you currently
default to FluidVariant.blank() but still restore a non-zero amount which yields
an inconsistent state; read the FluidVariant first, if it is blank then set
singleFluidStorage.amount = 0L and singleFluidStorage.variant =
FluidVariant.blank(), otherwise set singleFluidStorage.variant to the read
variant and restore amount from view.getLong("Amount", 0L).

@DaRealTurtyWurty DaRealTurtyWurty marked this pull request as draft August 19, 2025 10:44
@DaRealTurtyWurty DaRealTurtyWurty changed the title Particles for items in RotaryKiln Rewrite block entity renderers Aug 19, 2025
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.

2 participants